diff --git a/.eslintignore b/.eslintignore index a1da9ba1c1..276f662f6a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,6 +7,9 @@ node_modules /coverage /widget /schema +/playground/dist /playwright/playwright/.cache/ playwright +/playwright-report +/scripts diff --git a/.gitignore b/.gitignore index 634d33791c..784e648fef 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,8 @@ playwright-report* # Skill eval artifacts (workspaces are local-only test outputs) .agents/skills/*-workspace/ .claude/skills/*-workspace/ + +# Playground +/playground/node_modules +/playground/.next +/playground/.env diff --git a/.prettierignore b/.prettierignore index c31cfb90ac..c837294137 100644 --- a/.prettierignore +++ b/.prettierignore @@ -15,8 +15,12 @@ CONTRIBUTING.md /widget /schema + /playwright/playwright/.cache/ playwright # npm files -package.json \ No newline at end of file +package.json + +# Playground +/playground/.next diff --git a/.storybook/addons/form-addon/constants.ts b/.storybook/addons/form-addon/constants.ts new file mode 100644 index 0000000000..b3855c1586 --- /dev/null +++ b/.storybook/addons/form-addon/constants.ts @@ -0,0 +1,3 @@ +export const ADDON_ID = 'formaddon'; +export const TOOL_ID = `${ADDON_ID}/tool`; +export const GLOBAL_KEY = 'formAddon'; diff --git a/.storybook/addons/form-addon/preset.ts b/.storybook/addons/form-addon/preset.ts new file mode 100644 index 0000000000..ecf18e1ff4 --- /dev/null +++ b/.storybook/addons/form-addon/preset.ts @@ -0,0 +1,9 @@ +function managerEntries(entry: string[] = []) { + return [...entry, require.resolve('./register')]; +} + +function previewAnnotations(entry: string[] = []) { + return [...entry, require.resolve('./preview')]; +} + +module.exports = {managerEntries, previewAnnotations}; diff --git a/.storybook/addons/form-addon/preview.tsx b/.storybook/addons/form-addon/preview.tsx new file mode 100644 index 0000000000..9a193d00c3 --- /dev/null +++ b/.storybook/addons/form-addon/preview.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; + +import {ThemeProvider} from '@gravity-ui/uikit'; +import {useArgs} from '@storybook/preview-api'; +import {Decorator} from '@storybook/react'; + +import FormGenerator from '../../../src/form-generator-v2/FormGenerator'; +import type {Fields} from '../../../src/form-generator-v2/types'; + +import {GLOBAL_KEY} from './constants'; + +export const withFormAddon: Decorator = (Story, context) => { + const [args, updateArgs] = useArgs(); + const inputs = context.parameters.inputs as Fields | undefined; + const isActive = Boolean(context.globals[GLOBAL_KEY]); + + if (!isActive || !inputs) { + return ; + } + + return ( + +
+
+ +
+
+

+ Form +

+ +
+
+
+ ); +}; + +export const decorators = [withFormAddon]; diff --git a/.storybook/addons/form-addon/register.tsx b/.storybook/addons/form-addon/register.tsx new file mode 100644 index 0000000000..8b0aace564 --- /dev/null +++ b/.storybook/addons/form-addon/register.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; + +import {IconButton} from '@storybook/components'; +import {EditIcon} from '@storybook/icons'; +import {addons, types, useGlobals} from '@storybook/manager-api'; +const STORY_PREPARED = 'storyPrepared'; +const STORY_CHANGED = 'storyChanged'; + +import {ADDON_ID, GLOBAL_KEY, TOOL_ID} from './constants'; + +addons.register(ADDON_ID, () => { + addons.add(TOOL_ID, { + type: types.TOOL, + title: 'Form editor', + render: () => , + }); +}); + +function FormTool() { + const [globals, updateGlobals] = useGlobals(); + const isActive = Boolean(globals[GLOBAL_KEY]); + const [hasInputs, setHasInputs] = React.useState(false); + + React.useEffect(() => { + const channel = addons.getChannel(); + + const onPrepared = ({parameters}: {parameters: Record}) => { + setHasInputs(Boolean(parameters?.inputs)); + }; + + const onChanged = () => setHasInputs(false); + + channel.on(STORY_PREPARED, onPrepared); + channel.on(STORY_CHANGED, onChanged); + + return () => { + channel.off(STORY_PREPARED, onPrepared); + channel.off(STORY_CHANGED, onChanged); + }; + }, []); + + if (!hasInputs) { + return null; + } + + return ( + updateGlobals({[GLOBAL_KEY]: !isActive})} + > + + + ); +} diff --git a/.storybook/addons/result-addon/AddonResult.css b/.storybook/addons/result-addon/AddonResult.css new file mode 100644 index 0000000000..48909e65b9 --- /dev/null +++ b/.storybook/addons/result-addon/AddonResult.css @@ -0,0 +1,13 @@ +.result-addon { + padding: 11px; +} + +.result-addon pre { + margin: 10px 0 0; + padding: 0; +} + +.result-addon__empty { + opacity: 0.5; + font-style: italic; +} diff --git a/.storybook/addons/result-addon/constants.ts b/.storybook/addons/result-addon/constants.ts new file mode 100644 index 0000000000..0c47dc5a14 --- /dev/null +++ b/.storybook/addons/result-addon/constants.ts @@ -0,0 +1,3 @@ +export const ADDON_ID = 'resultaddon'; +export const PANEL_ID = `${ADDON_ID}/panel`; +export const EVENT_ID = `${ADDON_ID}/update`; diff --git a/.storybook/addons/result-addon/preset.ts b/.storybook/addons/result-addon/preset.ts new file mode 100644 index 0000000000..e2c27714b5 --- /dev/null +++ b/.storybook/addons/result-addon/preset.ts @@ -0,0 +1,5 @@ +function managerEntries(entry: string[] = []) { + return [...entry, require.resolve('./register')]; +} + +module.exports = {managerEntries}; diff --git a/.storybook/addons/result-addon/register.tsx b/.storybook/addons/result-addon/register.tsx new file mode 100644 index 0000000000..34e9543023 --- /dev/null +++ b/.storybook/addons/result-addon/register.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import {AddonPanel} from '@storybook/components'; +import {addons, types, useChannel, useGlobals} from '@storybook/manager-api'; +import {ClipboardButton, ThemeProvider} from '@gravity-ui/uikit'; + +import {ADDON_ID, EVENT_ID, PANEL_ID} from './constants'; + +import './AddonResult.css'; + +const ResultPanel = () => { + const [globals] = useGlobals(); + const [content, setContent] = React.useState(null); + + useChannel({ + [EVENT_ID]: (value: unknown) => { + setContent(value); + }, + }); + + const json = React.useMemo( + () => (content !== null ? JSON.stringify(content, null, 2) : ''), + [content], + ); + + return ( + +
+ {json ? : null} +
{json || No data yet}
+
+
+ ); +}; + +addons.register(ADDON_ID, () => { + addons.add(PANEL_ID, { + type: types.PANEL, + title: 'Result', + paramKey: 'resultPanel', + render: ({active, key}) => ( + + + + ), + }); +}); diff --git a/.storybook/addons/result-addon/useResultPanel.ts b/.storybook/addons/result-addon/useResultPanel.ts new file mode 100644 index 0000000000..3d28e4f2d5 --- /dev/null +++ b/.storybook/addons/result-addon/useResultPanel.ts @@ -0,0 +1,32 @@ +import * as React from 'react'; +import {useChannel} from '@storybook/preview-api'; + +import {EVENT_ID} from './constants'; + +/** + * Drop-in replacement for useState that also mirrors the value to the Result panel. + * + * Usage in a story: + * const [content, setContent] = useResultPanel({}); + */ +export function useResultPanel(initial: T): [T, (value: T) => void] { + const [value, setValue] = React.useState(initial); + + const emit = useChannel({}); + + const update = React.useCallback( + (next: T) => { + setValue(next); + emit(EVENT_ID, next); + }, + [emit], + ); + + // Emit initial value so the panel is not empty on first render. + React.useEffect(() => { + emit(EVENT_ID, initial); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return [value, update]; +} diff --git a/.storybook/decorators/docs/index.tsx b/.storybook/decorators/docs/index.tsx index 6b40e7cb69..e84cd94839 100644 --- a/.storybook/decorators/docs/index.tsx +++ b/.storybook/decorators/docs/index.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import {ThemeProvider} from '@gravity-ui/uikit'; import {themes} from '../../theme'; -import {MobileContext} from '../../../src/context/mobileContext'; + import {cn} from '../../../src/utils/cn'; import './DocsDecorator.scss'; @@ -20,9 +20,7 @@ export function DocsDecorator({children, context}: DocsDecoratorProps) { return (
- - {children} - + {children}
); diff --git a/.storybook/decorators/withPageConstructorProvider.tsx b/.storybook/decorators/withPageConstructorProvider.tsx index 7d437a068f..7f01b6348f 100644 --- a/.storybook/decorators/withPageConstructorProvider.tsx +++ b/.storybook/decorators/withPageConstructorProvider.tsx @@ -4,11 +4,7 @@ import {PageConstructorProvider} from '../../src/containers/PageConstructor/Prov export const withPageConstructorProvider: Decorator = (Story, context) => { return ( - + ); diff --git a/.storybook/main.ts b/.storybook/main.ts index 2dc61a45de..537b679f56 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -31,6 +31,8 @@ const config = { }, }, './addons/yaml-addon/preset', + './addons/result-addon/preset', + './addons/form-addon/preset', './addons/theme-addon/register.tsx', '@storybook/addon-mdx-gfm', '@storybook/addon-webpack5-compiler-babel', diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 738e229182..1fe4c7c84b 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -19,6 +19,8 @@ const preview: Preview = { decorators: [withLang, withMobile, withContextProvider, withPageConstructorProvider], parameters: { + inputs: {disable: true}, + resultPanel: {disable: true}, layout: 'fullscreen', docs: { theme: themeLight, @@ -55,6 +57,9 @@ const preview: Preview = { }, globalTypes: { + formAddon: { + defaultValue: false, + }, theme: { name: 'Theme', description: 'Global theme for components', diff --git a/.storybook/utils.ts b/.storybook/utils.ts index 37e832cd7c..bba294afaa 100644 --- a/.storybook/utils.ts +++ b/.storybook/utils.ts @@ -1,6 +1,6 @@ import yfm from '@diplodoc/transform'; import {ConstructorBlock} from '../src'; -import {contentTransformer} from '../src/text-transform'; +import {contentTransformer} from '../src/gravity-blocks/text-transform'; export const yfmTransform = (content: string) => yfm(content).result.html; diff --git a/README.md b/README.md index 0e2e305005..0e535e97a0 100644 --- a/README.md +++ b/README.md @@ -540,7 +540,7 @@ If you want to release a new version in previous major after commit it to the ma 7. Check your changes in CHANGELOG.md and approve robot's PR. 8. Squash and merge PR. You can see release process on [the Actions tab](https://github.com/gravity-ui/page-constructor/actions). -## Page constructor editor +## Page Constructor Editor v1 Editor provides user interface for page content management with realtime preview. @@ -593,6 +593,36 @@ When working with AI agents on this project, the Memory Bank serves as a compreh AI agents can read these files to quickly get up to speed with the project context and make more informed decisions about code changes and implementations. +## Page Constructor Editor v2 + +Editor provides user interface for page content management with realtime preview. + +Based on Iframe postMessage communication. + +How to use: + +```tsx +import {Editor} from '@gravity-ui/page-constructor/editor-v2'; + +export const MyAppEditor = ({initialContent, onUpdate, disableUrlField}: MyAppEditorProps) => ( + +); +``` + +### How to develop + +```shell +npm run deps:install +npm run dev:playground +``` + +Directory `/playground` contains NextJS service with integrated PC and Editor for development purposes. + ## Tests Comprehensive documentation is available at the provided [link](./test-utils/docs/README.md). diff --git a/gulpfile.js b/gulpfile.js index 3436f80f10..ba43dfddbe 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -261,6 +261,7 @@ task('styles-global', () => { sass .sync({ loadPaths: ['node_modules'], + silenceDeprecations: ['legacy-js-api', 'import', 'global-builtin'], }) .on('error', sass.logError), ) @@ -270,10 +271,15 @@ task('styles-global', () => { task('styles-components', () => { return src([`src/**/*.scss`, `!src/**/__stories__/**/*.scss`, '!src/widget/**/*.scss']) .pipe( - sass.sync({loadPaths: ['node_modules']}).on('error', function (error) { - sass.logError.call(this, error); - process.exit(1); - }), + sass + .sync({ + loadPaths: ['node_modules'], + silenceDeprecations: ['legacy-js-api', 'import'], + }) + .on('error', function (error) { + sass.logError.call(this, error); + process.exit(1); + }), ) .pipe(dest(path.resolve(BUILD_CLIENT_DIR, ESM_DIR))) .pipe(dest(path.resolve(BUILD_CLIENT_DIR, CJS_DIR))); diff --git a/import-graph.html b/import-graph.html new file mode 100644 index 0000000000..5ce9fee90b --- /dev/null +++ b/import-graph.html @@ -0,0 +1,656 @@ + + + + + + Import Dependencies Graph - Page Constructor + + + + +
+ +
+ +
+
+
Connection Types
+
+
+ Normal import +
+
+
+ Circular / Selected +
+
+
+ + +
+
+
+ + + + \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 19c3e98609..a9c6ef3334 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,7 +20,7 @@ module.exports = { ], }, transformIgnorePatterns: [ - 'node_modules/(?!(@gravity-ui|react-github-btn|tinygesture|swiper)/)', + 'node_modules/(?!(@gravity-ui|@uiw|@dnd-kit|@preact|colors-named|colors-named-hex|react-github-btn|tinygesture|swiper)/)', ], coverageDirectory: './coverage', collectCoverageFrom: [ @@ -37,6 +37,8 @@ module.exports = { // Mock CSS imports '^swiper/css.*': 'jest-transform-css', '\\.(css|less|scss|sass)$': 'jest-transform-css', + // Mock SVG imports + '\\.svg$': '/test-utils/svg-mock.js', }, testMatch: ['**/*.test.[jt]s?(x)'], testPathIgnorePatterns: [ diff --git a/package-lock.json b/package-lock.json index 997cab8284..3415315a01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,21 +10,27 @@ "license": "MIT", "dependencies": { "@bem-react/classname": "^1.6.0", + "@dnd-kit/helpers": "^0.3.2", + "@dnd-kit/react": "^0.3.2", "@gravity-ui/components": "^4.0.1", "@gravity-ui/dynamic-forms": "^5.0.0", "@gravity-ui/i18n": "^1.7.0", "@gravity-ui/icons": "^2.18.0", + "@gravity-ui/navigation": "^3.11.1", "@react-spring/web": "^9.7.3", "ajv": "^8.12.0", "ajv-keywords": "^5.1.0", + "deep-object-diff": "^1.1.9", "final-form": "^4.20.9", "github-buttons": "2.23.0", + "immutable": "^4.3.7", "js-yaml-source-map": "^0.2.2", "lodash": "^4.17.21", "monaco-editor": "^0.52.2", "react-final-form": "^6.5.9", "react-monaco-editor": "^0.53.0", "react-player": "^2.9.0", + "react-resizable-panels": "^2.1.3", "react-slick": "^0.29.0", "react-transition-group": "^4.4.2", "react-waypoint": "^10.1.0", @@ -33,7 +39,8 @@ "swiper": "^10.2.0", "typograf": "^7.4.1", "utility-types": "^3.10.0", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "zustand": "^4.5.2" }, "devDependencies": { "@babel/core": "^7.22.8", @@ -48,7 +55,7 @@ "@gravity-ui/prettier-config": "^1.1.0", "@gravity-ui/stylelint-config": "^4.0.1", "@gravity-ui/tsconfig": "^1.0.0", - "@gravity-ui/uikit": "^7.13.1", + "@gravity-ui/uikit": "^7.29.0", "@playwright/experimental-ct-react": "^1.45.3", "@playwright/test": "^1.45.3", "@storybook/addon-actions": "^8.6.11", @@ -77,12 +84,15 @@ "@types/uuid": "^9.0.0", "@types/webpack-env": "^1.18.1", "@types/youtube-player": "^5.5.11", + "@vitejs/plugin-react": "^4.5.0", "autoprefixer": "^10.4.14", "babel-jest": "^30.2.0", "babel-loader": "^8.3.0", + "bem-cn-lite": "^4.1.0", "css-loader": "^5.2.7", "es5-ext": "0.10.53", "esbuild": "^0.25.11", + "esbuild-sass-plugin": "^3.7.0", "eslint": "^8.57.1", "eslint-plugin-no-not-accumulator-reassign": "^0.1.0", "eslint-plugin-react": "^7.37.4", @@ -113,6 +123,7 @@ "react": "^18.3.1", "react-docgen-typescript": "^2.2.2", "react-dom": "^18.3.1", + "react-router": "^7.6.1", "resolve-url-loader": "^3.1.5", "rimraf": "^6.0.1", "sass": "^1.63.6", @@ -125,12 +136,17 @@ "ts-jest": "^29.2.5", "tslib": "^2.4.0", "typescript": "^5.7.3", + "vite": "^6.3.5", "vite-plugin-commonjs": "^0.10.1", - "vite-plugin-svgr": "^4.2.0", + "vite-plugin-svgr": "^4.3.0", "webpack": "^5.98.0", "webpack-cli": "^6.0.1", "webpack-shell-plugin-next": "^2.3.1" }, + "optionalDependencies": { + "@rollup/rollup-darwin-arm64": "^4.60.1", + "@rollup/rollup-linux-x64-gnu": "^4.60.1" + }, "peerDependencies": { "@diplodoc/transform": "^4.28.2", "@gravity-ui/uikit": "^7.1.1", @@ -2050,9 +2066,9 @@ "license": "MIT" }, "node_modules/@bem-react/classname": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@bem-react/classname/-/classname-1.6.0.tgz", - "integrity": "sha512-SFBwUHMcb7TFFK5ld88+JhecoEun3/kHZ6KvLDjj3w5hv/tfRV8mtGHA8N42uMctXLF4bPEcr96xwXXcRFuweg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@bem-react/classname/-/classname-1.8.0.tgz", + "integrity": "sha512-uazGRFKTMNbGR8KLvEh5qT+aP0yuym+sB+YFFaAyCkFodtjI0SskV6BfkiZ9+E3FloIFYPkwqJ1owxp48WCS9g==", "license": "MPL-2.0" }, "node_modules/@commitlint/cli": { @@ -3590,6 +3606,87 @@ "node": ">=14.17.0" } }, + "node_modules/@dnd-kit/abstract": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/abstract/-/abstract-0.3.2.tgz", + "integrity": "sha512-uvPVK+SZYD6Viddn9M0K0JQdXknuVSxA/EbMlFRanve3P/XTc18oLa5zGftKSGjfQGmuzkZ34E26DSbly1zi3Q==", + "license": "MIT", + "dependencies": { + "@dnd-kit/geometry": "^0.3.2", + "@dnd-kit/state": "^0.3.2", + "tslib": "^2.6.2" + } + }, + "node_modules/@dnd-kit/collision": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/collision/-/collision-0.3.2.tgz", + "integrity": "sha512-pNmNSLCI8S9fNQ7QJ3fBCDjiT0sqBhUFcKgmyYaGvGCAU+kq0AP8OWlh0JSisc9k5mFyxmRpmFQcnJpILz/RPA==", + "license": "MIT", + "dependencies": { + "@dnd-kit/abstract": "^0.3.2", + "@dnd-kit/geometry": "^0.3.2", + "tslib": "^2.6.2" + } + }, + "node_modules/@dnd-kit/dom": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/dom/-/dom-0.3.2.tgz", + "integrity": "sha512-cIUAVgt2szQyz6JRy7I+0r+xeyOAGH21Y15hb5bIyHoDEaZBvIDH+OOlD9eoLjCbsxDLN9WloU2CBi3OE6LYDg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/abstract": "^0.3.2", + "@dnd-kit/collision": "^0.3.2", + "@dnd-kit/geometry": "^0.3.2", + "@dnd-kit/state": "^0.3.2", + "tslib": "^2.6.2" + } + }, + "node_modules/@dnd-kit/geometry": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/geometry/-/geometry-0.3.2.tgz", + "integrity": "sha512-3UBPuIS7E3oGiHxOE8h810QA+0pnrnCtGxl4Os1z3yy5YkC/BEYGY+TxWPTQaY1/OMV7GCX7ZNMlama2QN3n3w==", + "license": "MIT", + "dependencies": { + "@dnd-kit/state": "^0.3.2", + "tslib": "^2.6.2" + } + }, + "node_modules/@dnd-kit/helpers": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/helpers/-/helpers-0.3.2.tgz", + "integrity": "sha512-pj7pCE6BiysNetpPnzb3BJOrcKiqueUr1LFg6wYoi2fIFYpz66n2Ojd7HTwfwkpv0oyC3QlvA6Dk8cOmi6VavA==", + "license": "MIT", + "dependencies": { + "@dnd-kit/abstract": "^0.3.2", + "tslib": "^2.6.2" + } + }, + "node_modules/@dnd-kit/react": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/react/-/react-0.3.2.tgz", + "integrity": "sha512-1Opg1xw6I75Z95c+rF2NJa0pdGb8rLAENtuopKtJ1J0PudWlz+P6yL137xy/6DV43uaRmNGtsdbMbR0yRYJ72g==", + "license": "MIT", + "dependencies": { + "@dnd-kit/abstract": "^0.3.2", + "@dnd-kit/dom": "^0.3.2", + "@dnd-kit/state": "^0.3.2", + "tslib": "^2.6.2" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@dnd-kit/state": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/state/-/state-0.3.2.tgz", + "integrity": "sha512-dLUIkoYrIJhGXfF2wGLTfb46vUokEsO/OoE21TSfmahYrx7ysTmnwbePsznFaHlwgZhQEh6AlLvthLCeY21b1A==", + "license": "MIT", + "dependencies": { + "@preact/signals-core": "^1.10.0", + "tslib": "^2.6.2" + } + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -4346,35 +4443,32 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", - "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", - "dev": true, + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.9" + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz", - "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", - "dev": true, + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.0", - "@floating-ui/utils": "^0.2.9" + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/react": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.8.tgz", - "integrity": "sha512-EQJ4Th328y2wyHR3KzOUOoTW2UKjFk53fmyahfwExnFQ8vnsMYqKc+fFPOkeYtj5tcp1DUMiNJ7BFhed7e9ONw==", - "dev": true, + "version": "0.27.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", + "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.1.2", - "@floating-ui/utils": "^0.2.9", + "@floating-ui/react-dom": "^2.1.8", + "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { @@ -4383,13 +4477,12 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", - "dev": true, + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", @@ -4397,10 +4490,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", - "dev": true, + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, "node_modules/@gravity-ui/components": { @@ -4542,6 +4634,24 @@ } } }, + "node_modules/@gravity-ui/navigation": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@gravity-ui/navigation/-/navigation-3.11.1.tgz", + "integrity": "sha512-qrvSyPHe0c18yg5QdoVV7ZQCixqhdiT6o5KAEpcebQlTSvMcvsR9CQ2zNACdryV2PUuSmmSgdZ9HH0ikyjDIDw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.4", + "react-transition-group": "^4.4.5", + "tslib": "^2.8.1" + }, + "peerDependencies": { + "@bem-react/classname": "^1.6.0", + "@gravity-ui/icons": "^2.2.0", + "@gravity-ui/uikit": "^7.2.0", + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@gravity-ui/prettier-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@gravity-ui/prettier-config/-/prettier-config-1.1.0.tgz", @@ -4583,25 +4693,28 @@ "license": "MIT" }, "node_modules/@gravity-ui/uikit": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-7.13.1.tgz", - "integrity": "sha512-GTVe718b86m8lxrjfAz/L3aV9ReXWJpKJxksYt7hAkkMbPLVUxwdRfZe5E1je3IEwTDAM7iVHX8QxlH1yymqDw==", + "version": "7.41.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-7.41.0.tgz", + "integrity": "sha512-nJbeAzOVrEiqMzFAo/hN3oa4YCO+8jIrT6z1q2rdl/MJDGXNHkkEveQYWGNUqz6x8mUatnpjfGPBTdhm0NN1UQ==", "dev": true, + "license": "MIT", "dependencies": { - "@bem-react/classname": "^1.6.0", - "@floating-ui/react": "^0.27.7", + "@bem-react/classname": "^1.7.0", + "@floating-ui/react": "^0.27.16", "@gravity-ui/i18n": "^1.8.0", - "@gravity-ui/icons": "^2.13.0", - "@tanstack/react-virtual": "^3.13.6", + "@gravity-ui/icons": "^2.16.0", + "@hello-pangea/dnd": "^18.0.1", + "@tanstack/react-virtual": "^3.13.12", + "@uiw/react-color": "^2.9.2", "blueimp-md5": "^2.19.0", "lodash": "^4.17.21", - "rc-slider": "^11.1.8", - "react-beautiful-dnd": "^13.1.1", + "rc-slider": "^11.1.9", "react-transition-group": "^4.4.5", "react-virtualized-auto-sizer": "^1.0.26", "react-window": "^1.8.11", "tabbable": "^6.2.0", - "tslib": "^2.8.1" + "tslib": "^2.8.1", + "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -4735,6 +4848,24 @@ "node": ">=10.13.0" } }, + "node_modules/@hello-pangea/dnd": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", + "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.26.7", + "css-box-model": "^1.2.1", + "raf-schd": "^4.0.3", + "react-redux": "^9.2.0", + "redux": "^5.0.1" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -5771,193 +5902,257 @@ "node": ">=12.4.0" } }, - "node_modules/@pkgr/core": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", - "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": ">= 10.0.0" }, "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@playwright/experimental-ct-core": { - "version": "1.45.3", - "resolved": "https://registry.npmjs.org/@playwright/experimental-ct-core/-/experimental-ct-core-1.45.3.tgz", - "integrity": "sha512-uYcWBxRPu2G2Mj2e+XUxRBzRNnG/Yz0A5DVWFewiG3qEfC92MaGYGxmzKeFeU9NcMA2fWwaqB3XWHXjn9qSM5Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.45.3", - "playwright-core": "1.45.3", - "vite": "^5.2.8" + "type": "opencollective", + "url": "https://opencollective.com/parcel" }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=18" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@playwright/experimental-ct-react": { - "version": "1.45.3", - "resolved": "https://registry.npmjs.org/@playwright/experimental-ct-react/-/experimental-ct-react-1.45.3.tgz", - "integrity": "sha512-cEgiZ2+DqVCeFJWJdHgOUwhlEof41Rg1GX48FtMX/xrjAnxs2ccqqAXMILD+mVV1ftsC2jeS1EiRLoAmf8QXgA==", + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@playwright/experimental-ct-core": "1.45.3", - "@vitejs/plugin-react": "^4.2.1" - }, - "bin": { - "playwright": "cli.js" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@playwright/test": { - "version": "1.45.3", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.3.tgz", - "integrity": "sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==", + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.45.3" - }, - "bin": { - "playwright": "cli.js" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@react-spring/animated": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", - "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@react-spring/core": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", - "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/react-spring/donate" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "url": "https://opencollective.com/parcel" } }, - "node_modules/@react-spring/rafz": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", - "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", - "license": "MIT" - }, - "node_modules/@react-spring/shared": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", - "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@react-spring/rafz": "~9.7.5", - "@react-spring/types": "~9.7.5" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@react-spring/types": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", - "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", - "license": "MIT" - }, - "node_modules/@react-spring/web": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", - "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/core": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", - "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + "node": ">= 10.0.0" }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">= 10.0.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", - "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ - "arm" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" - ] + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", - "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -5965,27 +6160,41 @@ "license": "MIT", "optional": true, "os": [ - "android" - ] + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", - "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" - ] + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", - "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -5993,55 +6202,79 @@ "license": "MIT", "optional": true, "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", - "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", - "cpu": [ - "arm64" + "win32" ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", - "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", - "cpu": [ - "x64" - ], + "node_modules/@pkgr/core": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", + "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", - "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", + "node_modules/@playwright/experimental-ct-core": { + "version": "1.45.3", + "resolved": "https://registry.npmjs.org/@playwright/experimental-ct-core/-/experimental-ct-core-1.45.3.tgz", + "integrity": "sha512-uYcWBxRPu2G2Mj2e+XUxRBzRNnG/Yz0A5DVWFewiG3qEfC92MaGYGxmzKeFeU9NcMA2fWwaqB3XWHXjn9qSM5Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.45.3", + "playwright-core": "1.45.3", + "vite": "^5.2.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ - "arm" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "aix" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", - "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -6049,13 +6282,16 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] + "android" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", - "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -6063,247 +6299,971 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] + "android" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", - "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "android" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", - "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", - "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ - "ppc64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", - "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", - "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ - "riscv64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", - "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ - "s390x" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", - "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", - "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ - "x64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", - "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ - "arm64" + "loong64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ] + "linux" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", - "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ - "ia32" + "mips64el" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ] + "linux" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", - "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ - "x64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ] - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@storybook/addon-actions": { - "version": "8.6.14", - "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.6.14.tgz", - "integrity": "sha512-mDQxylxGGCQSK7tJPkD144J8jWh9IU9ziJMHfB84PKpI/V5ZgqMDnpr2bssTrUaGDqU5e1/z8KcRF+Melhs9pQ==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "@types/uuid": "^9.0.1", - "dequal": "^2.0.2", - "polished": "^4.2.2", - "uuid": "^9.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^8.6.14" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@storybook/addon-backgrounds": { - "version": "8.6.14", - "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.6.14.tgz", - "integrity": "sha512-l9xS8qWe5n4tvMwth09QxH2PmJbCctEvBAc1tjjRasAfrd69f7/uFK4WhwJAstzBTNgTc8VXI4w8ZR97i1sFbg==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "memoizerific": "^1.11.3", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^8.6.14" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@storybook/addon-controls": { - "version": "8.6.14", - "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.6.14.tgz", - "integrity": "sha512-IiQpkNJdiRyA4Mq9mzjZlvQugL/aE7hNgVxBBGPiIZG6wb6Ht9hNnBYpap5ZXXFKV9p2qVI0FZK445ONmAa+Cw==", + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "dequal": "^2.0.2", - "ts-dedent": "^2.0.0" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@playwright/experimental-ct-core/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@playwright/experimental-ct-core/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/@playwright/experimental-ct-core/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/@playwright/experimental-ct-react": { + "version": "1.45.3", + "resolved": "https://registry.npmjs.org/@playwright/experimental-ct-react/-/experimental-ct-react-1.45.3.tgz", + "integrity": "sha512-cEgiZ2+DqVCeFJWJdHgOUwhlEof41Rg1GX48FtMX/xrjAnxs2ccqqAXMILD+mVV1ftsC2jeS1EiRLoAmf8QXgA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@playwright/experimental-ct-core": "1.45.3", + "@vitejs/plugin-react": "^4.2.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@playwright/test": { + "version": "1.45.3", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.3.tgz", + "integrity": "sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.45.3" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@preact/signals-core": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.2.tgz", + "integrity": "sha512-RZHdBj9ZF4n40Rp4jS052EHHjBWf96P9oNdXPfhQTovCuWY9iQn3Gq+gOTJSgBO9A/JBuPfMOWsSX/lIU9Pc/A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@react-spring/animated": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", + "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", + "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", + "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", + "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", + "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", + "license": "MIT" + }, + "node_modules/@react-spring/web": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", + "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/core": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", + "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", + "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", + "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", + "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", + "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", + "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", + "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", + "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", + "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", + "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", + "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", + "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", + "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", + "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", + "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", + "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", + "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", + "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@storybook/addon-actions": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.6.14.tgz", + "integrity": "sha512-mDQxylxGGCQSK7tJPkD144J8jWh9IU9ziJMHfB84PKpI/V5ZgqMDnpr2bssTrUaGDqU5e1/z8KcRF+Melhs9pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@types/uuid": "^9.0.1", + "dequal": "^2.0.2", + "polished": "^4.2.2", + "uuid": "^9.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.6.14" + } + }, + "node_modules/@storybook/addon-backgrounds": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.6.14.tgz", + "integrity": "sha512-l9xS8qWe5n4tvMwth09QxH2PmJbCctEvBAc1tjjRasAfrd69f7/uFK4WhwJAstzBTNgTc8VXI4w8ZR97i1sFbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "memoizerific": "^1.11.3", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.6.14" + } + }, + "node_modules/@storybook/addon-controls": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.6.14.tgz", + "integrity": "sha512-IiQpkNJdiRyA4Mq9mzjZlvQugL/aE7hNgVxBBGPiIZG6wb6Ht9hNnBYpap5ZXXFKV9p2qVI0FZK445ONmAa+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "dequal": "^2.0.2", + "ts-dedent": "^2.0.0" }, "funding": { "type": "opencollective", @@ -7061,45 +8021,283 @@ "webpack": ">= 4" } }, - "node_modules/@storybook/react-dom-shim": { - "version": "8.6.14", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.6.14.tgz", - "integrity": "sha512-0hixr3dOy3f3M+HBofp3jtMQMS+sqzjKNgl7Arfuj3fvjmyXOks/yGjDImySR4imPtEllvPZfhiQNlejheaInw==", + "node_modules/@storybook/react-dom-shim": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.6.14.tgz", + "integrity": "sha512-0hixr3dOy3f3M+HBofp3jtMQMS+sqzjKNgl7Arfuj3fvjmyXOks/yGjDImySR4imPtEllvPZfhiQNlejheaInw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.6.14" + } + }, + "node_modules/@storybook/react-webpack5": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-8.6.14.tgz", + "integrity": "sha512-ka0q9tQBLruhO38sybP/MkZzejqAltce7HJTJ2KKbUYUlbvuG7m56tBX7DVC5JaImbsO3b8fqOrKH7gRt4KYrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/builder-webpack5": "8.6.14", + "@storybook/preset-react-webpack": "8.6.14", + "@storybook/react": "8.6.14" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.6.14", + "typescript": ">= 4.2.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/theming": { + "version": "8.6.14", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.6.14.tgz", + "integrity": "sha512-r4y+LsiB37V5hzpQo+BM10PaCsp7YlZ0YcZzQP1OCkPlYXmUAFy2VvDKaFRpD8IeNPKug2u4iFm/laDEbs03dg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "engines": { + "node": ">=10" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^8.6.14" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@storybook/react-webpack5": { - "version": "8.6.14", - "resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-8.6.14.tgz", - "integrity": "sha512-ka0q9tQBLruhO38sybP/MkZzejqAltce7HJTJ2KKbUYUlbvuG7m56tBX7DVC5JaImbsO3b8fqOrKH7gRt4KYrQ==", + "node_modules/@svgr/core/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/builder-webpack5": "8.6.14", - "@storybook/preset-react-webpack": "8.6.14", - "@storybook/react": "8.6.14" + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=14" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^8.6.14", - "typescript": ">= 4.2.x" + "typescript": ">=4.9.5" }, "peerDependenciesMeta": { "typescript": { @@ -7107,43 +8305,46 @@ } } }, - "node_modules/@storybook/theming": { - "version": "8.6.14", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.6.14.tgz", - "integrity": "sha512-r4y+LsiB37V5hzpQo+BM10PaCsp7YlZ0YcZzQP1OCkPlYXmUAFy2VvDKaFRpD8IeNPKug2u4iFm/laDEbs03dg==", + "node_modules/@svgr/core/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" + "engines": { + "node": ">=8" } }, - "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "node_modules/@svgr/hast-util-to-babel-ast": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", - "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, "engines": { "node": ">=14" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", - "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, "engines": { "node": ">=14" }, @@ -7152,1184 +8353,1362 @@ "url": "https://github.com/sponsors/gregberge" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@svgr/core": "*" } }, - "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", - "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "node_modules/@tanstack/react-virtual": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.2.tgz", + "integrity": "sha512-IpWnmCLvuymRfeeLNVXIzNEYBFLpd3drVIS91sqV78VTZFyldlChkOocZRCPp1B+Wnk09bcLNme8WaMU/9/9bQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=14" + "dependencies": { + "@tanstack/virtual-core": "3.17.0" }, "funding": { "type": "github", - "url": "https://github.com/sponsors/gregberge" + "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", - "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "node_modules/@tanstack/virtual-core": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.0.tgz", + "integrity": "sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=14" + "node": ">=8" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@svgr/babel-plugin-svg-dynamic-title": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", - "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "node_modules/@testing-library/dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": ">=14" + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", + "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.0.1", + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=8", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=8" } }, - "node_modules/@svgr/babel-plugin-svg-em-dimensions": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", - "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "node_modules/@testing-library/jest-dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=14" + "dependencies": { + "@babel/runtime": "^7.12.5" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "engines": { + "node": ">=18" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@svgr/babel-plugin-transform-react-native-svg": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", - "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", "dev": true, "license": "MIT", "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "node": ">=12", + "npm": ">=6" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@testing-library/dom": ">=7.21.4" } }, - "node_modules/@svgr/babel-plugin-transform-svg-component": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", - "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">= 10" } }, - "node_modules/@svgr/babel-preset": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", - "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", "dev": true, - "license": "MIT", - "dependencies": { - "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", - "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", - "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", - "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", - "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", - "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", - "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", - "@svgr/babel-plugin-transform-svg-component": "8.0.0" - }, + "license": "ISC", "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=10.13.0" } }, - "node_modules/@svgr/core": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", - "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@babel/core": "^7.21.3", - "@svgr/babel-preset": "8.1.0", - "camelcase": "^6.2.0", - "cosmiconfig": "^8.1.3", - "snake-case": "^3.0.4" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "tslib": "^2.4.0" } }, - "node_modules/@svgr/core/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "node_modules/@svgr/core/node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, "license": "MIT", "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@babel/types": "^7.0.0" } }, - "node_modules/@svgr/core/node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "node_modules/@svgr/hast-util-to-babel-ast": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", - "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.21.3", - "entities": "^4.4.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "@babel/types": "^7.20.7" } }, - "node_modules/@svgr/plugin-jsx": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", - "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz", + "integrity": "sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.21.3", - "@svgr/babel-preset": "8.1.0", - "@svgr/hast-util-to-babel-ast": "8.0.0", - "svg-parser": "^2.0.4" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@svgr/core": "*" + "@types/node": "*" } }, - "node_modules/@tanstack/react-virtual": { - "version": "3.13.9", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.9.tgz", - "integrity": "sha512-SPWC8kwG/dWBf7Py7cfheAPOxuvIv4fFQ54PdmYbg7CpXfsKxkucak43Q0qKsxVthhUJQ1A7CIMAIplq4BjVwA==", + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "dev": true, "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.13.9" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "@types/ms": "*" } }, - "node_modules/@tanstack/virtual-core": { - "version": "3.13.9", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.9.tgz", - "integrity": "sha512-3jztt0jpaoJO5TARe2WIHC1UQC3VMLAFUW5mmMo0yrkwtDB2AQP0+sh10BVUpWrnvHjSLvzFizydtEGLCJKFoQ==", + "node_modules/@types/doctrine": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" + "@types/eslint": "*", + "@types/estree": "*" } }, - "node_modules/@testing-library/dom/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/expect": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", + "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "@types/node": "*" } }, - "node_modules/@testing-library/dom/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "@types/istanbul-lib-coverage": "*" } }, - "node_modules/@testing-library/dom/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "@types/istanbul-lib-report": "*" } }, - "node_modules/@testing-library/dom/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/jest-dom": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", - "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==", + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, "license": "MIT", "dependencies": { - "@adobe/css-tools": "^4.0.1", - "@babel/runtime": "^7.9.2", - "@types/testing-library__jest-dom": "^5.9.1", - "aria-query": "^5.0.0", - "chalk": "^3.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.5.6", - "lodash": "^4.17.15", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=8", - "npm": ">=6", - "yarn": ">=1" + "expect": "^29.0.0", + "pretty-format": "^29.0.0" } }, - "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@testing-library/jest-dom/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@testing-library/jest-dom/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } + "license": "MIT" }, - "node_modules/@testing-library/jest-dom/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", "dev": true, "license": "MIT" }, - "node_modules/@testing-library/react": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", - "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" } }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } + "license": "MIT" }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz", + "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 10" + "dependencies": { + "@types/unist": "*" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } + "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", - "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } + "license": "MIT" }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "dev": true, "license": "MIT" }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "node_modules/@types/node": { + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "undici-types": "~6.21.0" } }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.22", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz", + "integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.0.0" + "@types/prop-types": "*", + "csstype": "^3.0.2" } }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "peerDependencies": { + "@types/react": "^18.0.0" } }, - "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "node_modules/@types/react-slick": { + "version": "0.23.13", + "resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.13.tgz", + "integrity": "sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@types/react": "*" } }, - "node_modules/@types/conventional-commits-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz", - "integrity": "sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==", + "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", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" + "peerDependencies": { + "@types/react": "*" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "node_modules/@types/resolve": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", + "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", + "dev": true, "license": "MIT" }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "node_modules/@types/sanitize-html": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.6.1.tgz", + "integrity": "sha512-+JLlZdJkIHdgvlFnMorJeLv5WIORNcI+jHsAoPBWMnhGx5Rbz/D1QN5D4qvmoA7fooDlEVy6zlioWSgqbU0KeQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/ms": "*" + "htmlparser2": "^6.0.0" } }, - "node_modules/@types/doctrine": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", - "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", "dev": true, "license": "MIT" }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } + "license": "MIT" }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "node_modules/@types/testing-library__jest-dom": { + "version": "5.14.9", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", + "integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==", "dev": true, "license": "MIT", "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" + "@types/jest": "*" } }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true, "license": "MIT" }, - "node_modules/@types/expect": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", - "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "dev": true, "license": "MIT" }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vinyl": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz", + "integrity": "sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==", "dev": true, "license": "MIT", "dependencies": { + "@types/expect": "^1.20.4", "@types/node": "*" } }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", - "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==", + "node_modules/@types/webpack-env": { + "version": "1.18.8", + "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.8.tgz", + "integrity": "sha512-G9eAoJRMLjcvN4I08wB5I7YofOb/kaJNd5uoCMX+LbKXTPCF+ZIHuqTnFaK9Jz1rgs035f9JUPUhNFtqgucy/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "license": "MIT", "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" + "@types/yargs-parser": "*" } }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true, "license": "MIT" }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "node_modules/@types/youtube-player": { + "version": "5.5.11", + "resolved": "https://registry.npmjs.org/@types/youtube-player/-/youtube-player-5.5.11.tgz", + "integrity": "sha512-pM41CDBqJqBmTeJWnF7NOGz82IQoYOhqzMYXv5vKCXBqGiYSLldxMtpCk6KAEtADTy49S45AriYaCaZyeUX38Q==", "dev": true, "license": "MIT" }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", + "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/type-utils": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", + "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", "dev": true, "license": "MIT", "dependencies": { - "@types/istanbul-lib-coverage": "*" + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", "dev": true, "license": "MIT", "dependencies": { - "@types/istanbul-lib-report": "*" + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", + "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", "dev": true, "license": "MIT", "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@types/jest/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/@typescript-eslint/types": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@types/jest/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@types/jest/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jsdom": { - "version": "20.0.1", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", - "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz", - "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "node_modules/@typescript-eslint/utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", "dev": true, "license": "MIT", "dependencies": { - "@types/unist": "*" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@types/mdx": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", - "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.15.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", - "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "@typescript-eslint/types": "8.32.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.22", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz", - "integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "node_modules/@uiw/color-convert": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.10.3.tgz", + "integrity": "sha512-5tIjb4CZzGR7K3Sshswsuuc6FOAFNFwjtF0hkhKH3f+CMauC4Akv7LPq6o9v68S7dIAeKvfj8qWg5Tc2I1TVSA==", "dev": true, "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, "peerDependencies": { - "@types/react": "^18.0.0" + "@babel/runtime": ">=7.19.0" } }, - "node_modules/@types/react-redux": { - "version": "7.1.34", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", - "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", + "node_modules/@uiw/react-color": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color/-/react-color-2.10.3.tgz", + "integrity": "sha512-OezqWY4Fo8yJf8yn2sfdDS3xmAir6FBDSiDJ0tR/CoXwQ2RaQl0mZlarfb/I0aaGiJ/jAORAGStTX/G5XoB2jw==", "dev": true, "license": "MIT", "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" + "@uiw/color-convert": "2.10.3", + "@uiw/react-color-alpha": "2.10.3", + "@uiw/react-color-block": "2.10.3", + "@uiw/react-color-chrome": "2.10.3", + "@uiw/react-color-circle": "2.10.3", + "@uiw/react-color-colorful": "2.10.3", + "@uiw/react-color-compact": "2.10.3", + "@uiw/react-color-editable-input": "2.10.3", + "@uiw/react-color-editable-input-hsla": "2.10.3", + "@uiw/react-color-editable-input-rgba": "2.10.3", + "@uiw/react-color-github": "2.10.3", + "@uiw/react-color-hue": "2.10.3", + "@uiw/react-color-material": "2.10.3", + "@uiw/react-color-name": "2.10.3", + "@uiw/react-color-saturation": "2.10.3", + "@uiw/react-color-shade-slider": "2.10.3", + "@uiw/react-color-sketch": "2.10.3", + "@uiw/react-color-slider": "2.10.3", + "@uiw/react-color-swatch": "2.10.3", + "@uiw/react-color-wheel": "2.10.3" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@types/react-slick": { - "version": "0.23.13", - "resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.13.tgz", - "integrity": "sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==", + "node_modules/@uiw/react-color-alpha": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-alpha/-/react-color-alpha-2.10.3.tgz", + "integrity": "sha512-82wtNRmEW5uZV2hmw5SYZKPIZjN6aqm8/CZOg8KXfx8eoIqpbjrRBwABggPO/O5Dj2JNAPC4eYyHslE24Pxy8w==", "dev": true, "license": "MIT", "dependencies": { - "@types/react": "*" + "@uiw/color-convert": "2.10.3", + "@uiw/react-drag-event-interactive": "2.10.3" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.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", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "node_modules/@uiw/react-color-block": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-block/-/react-color-block-2.10.3.tgz", + "integrity": "sha512-TVwU/bfpt51kWqL+CcQmocQrRQHAprJL/1IxjH48ZWfLqIQ1TTZVueopGBadc2CYzDygkyTWsmYpkZJZxM9YjQ==", "dev": true, "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.10.3", + "@uiw/react-color-editable-input": "2.10.3", + "@uiw/react-color-swatch": "2.10.3" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, "peerDependencies": { - "@types/react": "*" + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@types/resolve": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", - "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/sanitize-html": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.6.1.tgz", - "integrity": "sha512-+JLlZdJkIHdgvlFnMorJeLv5WIORNcI+jHsAoPBWMnhGx5Rbz/D1QN5D4qvmoA7fooDlEVy6zlioWSgqbU0KeQ==", + "node_modules/@uiw/react-color-chrome": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-chrome/-/react-color-chrome-2.10.3.tgz", + "integrity": "sha512-28qmnD19YR1wjzWe62OnwvDRhXrGy/Oq2sg/p+PBUPp8nemvq4sBX9mIToAxHtb2TiSWKCr0JapWwHZveu0tjA==", "dev": true, "license": "MIT", "dependencies": { - "htmlparser2": "^6.0.0" + "@uiw/color-convert": "2.10.3", + "@uiw/react-color-alpha": "2.10.3", + "@uiw/react-color-editable-input": "2.10.3", + "@uiw/react-color-editable-input-hsla": "2.10.3", + "@uiw/react-color-editable-input-rgba": "2.10.3", + "@uiw/react-color-github": "2.10.3", + "@uiw/react-color-hue": "2.10.3", + "@uiw/react-color-saturation": "2.10.3" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/testing-library__jest-dom": { - "version": "5.14.9", - "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", - "integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==", + "node_modules/@uiw/react-color-circle": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-circle/-/react-color-circle-2.10.3.tgz", + "integrity": "sha512-aHO0Xf5AziEQ8PnckxIcGskPPx8Ql7BIaC7+Ngya34TDwDa/kUbEhwohn89TxNZ8ZZhG/uldTDIswMXK8Fe4zA==", "dev": true, "license": "MIT", "dependencies": { - "@types/jest": "*" + "@uiw/color-convert": "2.10.3", + "@uiw/react-color-swatch": "2.10.3" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "node_modules/@uiw/react-color-colorful": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-colorful/-/react-color-colorful-2.10.3.tgz", + "integrity": "sha512-hnWr03wKNbJjNEZ4SFnwoeKNMtS2DR7rXsCv0bt17DVdBgYPZmuMTIwAR6nKFExdOwgOwXloP5KGESJYwyp2YQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.10.3", + "@uiw/react-color-alpha": "2.10.3", + "@uiw/react-color-hue": "2.10.3", + "@uiw/react-color-saturation": "2.10.3" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } }, - "node_modules/@types/vinyl": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz", - "integrity": "sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==", + "node_modules/@uiw/react-color-compact": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-compact/-/react-color-compact-2.10.3.tgz", + "integrity": "sha512-iXIG7idhaZ3XNFanKbVfFsL4w2GU5yWRUR+J5L1SzkXar1LTCjx7IDScweEBKnMdpPzDGiS2kTIn5anAif4ZDg==", "dev": true, "license": "MIT", "dependencies": { - "@types/expect": "^1.20.4", - "@types/node": "*" + "@uiw/color-convert": "2.10.3", + "@uiw/react-color-editable-input": "2.10.3", + "@uiw/react-color-editable-input-rgba": "2.10.3", + "@uiw/react-color-swatch": "2.10.3" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@types/webpack-env": { - "version": "1.18.8", - "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.8.tgz", - "integrity": "sha512-G9eAoJRMLjcvN4I08wB5I7YofOb/kaJNd5uoCMX+LbKXTPCF+ZIHuqTnFaK9Jz1rgs035f9JUPUhNFtqgucy/A==", + "node_modules/@uiw/react-color-editable-input": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input/-/react-color-editable-input-2.10.3.tgz", + "integrity": "sha512-QnYNpFI0p8pssYbWxIJwfmUdPwzzX0IXOAl4mU8q1HQNYoXFBGFJ4rXkJSOdauumAfOr4C8RKZlTB4/83EBBPQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "node_modules/@uiw/react-color-editable-input-hsla": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input-hsla/-/react-color-editable-input-hsla-2.10.3.tgz", + "integrity": "sha512-WoWsk52MN6rvxFD1HvNTrrGRmSKwrnp0S7htd4QnKuczlLtn4qfMXVwMARdvnR7ZqC1vn7FXZpVYd+Zh3upd2g==", "dev": true, "license": "MIT", "dependencies": { - "@types/yargs-parser": "*" + "@uiw/color-convert": "2.10.3", + "@uiw/react-color-editable-input-rgba": "2.10.3" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "node_modules/@uiw/react-color-editable-input-rgba": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input-rgba/-/react-color-editable-input-rgba-2.10.3.tgz", + "integrity": "sha512-n0kyNTNicRKASJNWYJexfwwfbbwvOJEYGQO8tX4PivmZYQVuYZq/vqsgYG5y+VGrBYZsHFfY+0LtPS4AXlz+Aw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.10.3", + "@uiw/react-color-editable-input": "2.10.3" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } }, - "node_modules/@types/youtube-player": { - "version": "5.5.11", - "resolved": "https://registry.npmjs.org/@types/youtube-player/-/youtube-player-5.5.11.tgz", - "integrity": "sha512-pM41CDBqJqBmTeJWnF7NOGz82IQoYOhqzMYXv5vKCXBqGiYSLldxMtpCk6KAEtADTy49S45AriYaCaZyeUX38Q==", + "node_modules/@uiw/react-color-github": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-github/-/react-color-github-2.10.3.tgz", + "integrity": "sha512-BLiI12utcJQMT2Ym4OeYplraHLwUtW7SJQ2IbHQA7YPEU9FYqchHv1P/6DJBZi98gFqWFll+7xotEzHCCXZP5g==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.10.3", + "@uiw/react-color-swatch": "2.10.3" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "node_modules/@uiw/react-color-hue": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-hue/-/react-color-hue-2.10.3.tgz", + "integrity": "sha512-dlaLaQEPXvaywc8xS8DgiYIbq4qUuI8fyERR7/bRsRXCTA9x4tFAD+lWenXcZBEhQh8HfZd1MhbvjcLzs7xpFg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@uiw/color-convert": "2.10.3", + "@uiw/react-color-alpha": "2.10.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://jaywcjlove.github.io/#/sponsor" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "node_modules/@uiw/react-color-material": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-material/-/react-color-material-2.10.3.tgz", + "integrity": "sha512-zis1x0VPBCjFmlju0Syubo6ElNIwf91CM9xilL/A3K/LGRRVuvq/PK1mGRAjFonjfTHiTlOg1Lq3NsYCaVg8LA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@uiw/color-convert": "2.10.3", + "@uiw/react-color-editable-input": "2.10.3", + "@uiw/react-color-editable-input-rgba": "2.10.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://jaywcjlove.github.io/#/sponsor" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "node_modules/@uiw/react-color-name": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-name/-/react-color-name-2.10.3.tgz", + "integrity": "sha512-BnqZebCC2lmJezs190vUtzBybjMJypFkTEav/HP3FUzHD0XSsKHOZiHENzXp+TYr0Fb7WJQXxE/dl11zkcOwVg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "colors-named": "^1.0.1", + "colors-named-hex": "^1.0.1" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "node_modules/@uiw/react-color-saturation": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-saturation/-/react-color-saturation-2.10.3.tgz", + "integrity": "sha512-iRIwNx3UTZrpV5hoQ5mcsZ8ahTXcMaGNvg8iS9JUByg+1PljwWcnqq5RNrRZJXPx+vryavefMkJLKHcPb9BLvQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@uiw/color-convert": "2.10.3", + "@uiw/react-drag-event-interactive": "2.10.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://jaywcjlove.github.io/#/sponsor" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "node_modules/@uiw/react-color-shade-slider": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-shade-slider/-/react-color-shade-slider-2.10.3.tgz", + "integrity": "sha512-WgK/JgS3oDyIzah9Jcd8uPSild6WOlYq9+jroBQ3q4ZLk+DADZ6ELaIi+tXsloxx65AaLJvXlqE0By6HAyohaQ==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "dependencies": { + "@uiw/color-convert": "2.10.3", + "@uiw/react-color-alpha": "2.10.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "node_modules/@uiw/react-color-sketch": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-sketch/-/react-color-sketch-2.10.3.tgz", + "integrity": "sha512-4Vua0Ti1K/uOx0E3RV0xdOfLbpc4K8zXfWZtqyjJZ7jIZpB5KNgcai+b4R47VoUpyCYf2B30iCvBuFpN3cfqYA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@uiw/color-convert": "2.10.3", + "@uiw/react-color-alpha": "2.10.3", + "@uiw/react-color-editable-input": "2.10.3", + "@uiw/react-color-editable-input-rgba": "2.10.3", + "@uiw/react-color-hue": "2.10.3", + "@uiw/react-color-saturation": "2.10.3", + "@uiw/react-color-swatch": "2.10.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://jaywcjlove.github.io/#/sponsor" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "node_modules/@uiw/react-color-slider": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-slider/-/react-color-slider-2.10.3.tgz", + "integrity": "sha512-aLJQPGV1/SpAWy6+U7RVqcJmhdtrdCYwvDL0/i5u/HC4VeWXB+w2LSSC63qWHwo+RkwfaJFLXKMO1JUWlQvxXg==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.10.3", + "@uiw/react-color-alpha": "2.10.3" }, - "engines": { - "node": ">=10" + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "node_modules/@uiw/react-color-swatch": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-swatch/-/react-color-swatch-2.10.3.tgz", + "integrity": "sha512-7Q/h9RTeloFnrIo/k6U5g+D/abSIT62FiAQZLaEC34O2eOtu1LQReTXAgx39b1HSQ8piCgBYaQc7oM9WhRTPow==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@uiw/color-convert": "2.10.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://jaywcjlove.github.io/#/sponsor" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "node_modules/@uiw/react-color-wheel": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-color-wheel/-/react-color-wheel-2.10.3.tgz", + "integrity": "sha512-eNFWioQt8Fr3UgHpuCxN4/gniZReOit3FzYxVFVXzQD5Hd6ch6jJDDZFFTl2NGVuTfsXLIepLYe9B5FHajC7AA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@uiw/color-convert": "2.10.3", + "@uiw/react-drag-event-interactive": "2.10.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "node_modules/@uiw/react-drag-event-interactive": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@uiw/react-drag-event-interactive/-/react-drag-event-interactive-2.10.3.tgz", + "integrity": "sha512-veLm9HF1cairiYbGxcALYVu51T4XAzDz8fmtQ0SiLVFHBX74+iUGo2jArS/XALFBnapsLYTAWTnHXxe38mC/Vw==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, + "license": "MIT", "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, "node_modules/@ungap/structured-clone": { @@ -8581,15 +9960,16 @@ ] }, "node_modules/@vitejs/plugin-react": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", - "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.26.10", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, @@ -8597,7 +9977,7 @@ "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/@webassemblyjs/ast": { @@ -10048,6 +11428,23 @@ ], "license": "MIT" }, + "node_modules/bem-cn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bem-cn/-/bem-cn-3.0.1.tgz", + "integrity": "sha512-kWC76a09vSk6cJXDYsH1erjxdBf856HTxl0IHOvYItSmBC6wQCsRCf9bmKR0hmeUDcUP5XPMr8MNXDgKbKJi0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bem-cn-lite": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bem-cn-lite/-/bem-cn-lite-4.1.0.tgz", + "integrity": "sha512-0IEVRYK2MQKQO00P3sY3hNv7vH8P+Z8mR46qFcaiwsQAWp0MuMWpLSuUUhZEjKD2HzTGXMqMsFysWEeeJa1drQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bem-cn": "^3.0.1" + } + }, "node_modules/better-opn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", @@ -10795,6 +12192,32 @@ "dev": true, "license": "MIT" }, + "node_modules/colors-named": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/colors-named/-/colors-named-1.0.5.tgz", + "integrity": "sha512-xaspf9oddAOqP2LYNOgp8E3BwAzugrdO9J1kDNS5ySrzTgV9hrXGBt5w87ioLEr2pM4Ukt+GKedvzaLRxpv8pA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/colors-named-hex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/colors-named-hex/-/colors-named-hex-1.0.4.tgz", + "integrity": "sha512-X+Enw/2fFAgDRhUac69cRO/RJvHnWDBBrP8J1sJuEU16Buiiu8PPpJP4abSo0V+fJbkfwmQITE6zKx/SBJERGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -11803,6 +13226,12 @@ "dev": true, "license": "MIT" }, + "node_modules/deep-object-diff": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", + "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==", + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -11888,6 +13317,17 @@ "node": ">=0.10.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", @@ -12586,6 +14026,21 @@ "esbuild": ">=0.12 <1" } }, + "node_modules/esbuild-sass-plugin": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-3.7.0.tgz", + "integrity": "sha512-vxNSXFx3/0ZFApKo9036ek2iRfsT+yVO99qIYqa+JaDSuJuId2/N4s1TY+xfK+5LRpAMQkfdBVUTxb/1r2bq1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.22.11", + "sass": "^1.97.3" + }, + "peerDependencies": { + "esbuild": ">=0.27.3", + "sass-embedded": "^1.97.3" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -16347,8 +17802,7 @@ "node_modules/immutable": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", - "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", - "dev": true + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==" }, "node_modules/import-fresh": { "version": "3.3.1", @@ -21706,6 +23160,14 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -24252,9 +25714,9 @@ } }, "node_modules/rc-slider": { - "version": "11.1.8", - "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.8.tgz", - "integrity": "sha512-2gg/72YFSpKP+Ja5AjC5DPL1YnV8DEITDQrcc1eASrUYjl0esptaBVJBh5nLTXCCp15eD8EuGjwezVGSHhs9tQ==", + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz", + "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==", "dev": true, "license": "MIT", "dependencies": { @@ -24305,27 +25767,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-beautiful-dnd": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", - "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", - "deprecated": "react-beautiful-dnd is now deprecated. Context and options: https://github.com/atlassian/react-beautiful-dnd/issues/2672", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.9.2", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.2.0", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.5 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-colorful": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", @@ -24450,27 +25891,25 @@ } }, "node_modules/react-redux": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", - "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" }, "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" }, "peerDependenciesMeta": { - "react-dom": { + "@types/react": { "optional": true }, - "react-native": { + "redux": { "optional": true } } @@ -24485,6 +25924,53 @@ "node": ">=0.10.0" } }, + "node_modules/react-resizable-panels": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.9.tgz", + "integrity": "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-router": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", + "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/react-select": { "version": "5.10.1", "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.1.tgz", @@ -24864,14 +26350,11 @@ } }, "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } + "license": "MIT" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -25276,13 +26759,14 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -25698,6 +27182,34 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", + "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", + "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -25859,20 +27371,24 @@ } }, "node_modules/sass": { - "version": "1.63.6", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.63.6.tgz", - "integrity": "sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==", + "version": "1.100.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz", + "integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==", "dev": true, + "license": "MIT", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", + "chokidar": "^5.0.0", + "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { "sass": "sass.js" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" } }, "node_modules/sass-loader": { @@ -25959,24 +27475,61 @@ "ajv-keywords": "^3.5.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/sass-loader/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/sass-loader/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "node_modules/sass/node_modules/immutable": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.6.tgz", + "integrity": "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, + "license": "MIT" + }, + "node_modules/sass/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/saxes": { @@ -26095,6 +27648,13 @@ "randombytes": "^2.1.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "dev": true, + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -27289,2410 +28849,2006 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stylelint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/stylelint/node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stylelint/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "node_modules/stylelint/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stylelint/node_modules/meow": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/meow/-/meow-10.1.5.tgz", - "integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/minimist": "^1.2.2", - "camelcase-keys": "^7.0.0", - "decamelize": "^5.0.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.2", - "read-pkg-up": "^8.0.0", - "redent": "^4.0.0", - "trim-newlines": "^4.0.2", - "type-fest": "^1.2.2", - "yargs-parser": "^20.2.9" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stylelint/node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stylelint/node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/stylelint/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/stylelint/node_modules/redent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", - "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^5.0.0", - "strip-indent": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stylelint/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stylelint/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/stylelint/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stylelint/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/stylelint/node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/stylelint/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/stylelint/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "has-flag": "^4.0.0" + "lru-cache": "^6.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/supports-hyperlinks": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", - "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "node_modules/stylelint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, "engines": { - "node": ">=14.18" - }, - "funding": { - "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + "node": ">= 4" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "node_modules/stylelint/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sver": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", - "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", + "node_modules/stylelint/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true, - "license": "MIT", - "optionalDependencies": { - "semver": "^6.3.0" - } + "license": "ISC" }, - "node_modules/svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "node_modules/stylelint/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, - "license": "MIT" - }, - "node_modules/svg-tags": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", - "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", - "dev": true + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } }, - "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "node_modules/stylelint/node_modules/meow": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/meow/-/meow-10.1.5.tgz", + "integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==", "dev": true, "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.3.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.0.0" - }, - "bin": { - "svgo": "bin/svgo" + "@types/minimist": "^1.2.2", + "camelcase-keys": "^7.0.0", + "decamelize": "^5.0.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.2", + "read-pkg-up": "^8.0.0", + "redent": "^4.0.0", + "trim-newlines": "^4.0.2", + "type-fest": "^1.2.2", + "yargs-parser": "^20.2.9" }, "engines": { - "node": ">=14.0.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "node_modules/stylelint/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/swiper": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/swiper/-/swiper-10.3.1.tgz", - "integrity": "sha512-24Wk3YUdZHxjc9faID97GTu6xnLNia+adMt6qMTZG/HgdSUt4fS0REsGUXJOgpTED0Amh/j+gRGQxsLayJUlBQ==", - "funding": [ - { - "type": "patreon", - "url": "https://www.patreon.com/swiperjs" - }, - { - "type": "open_collective", - "url": "http://opencollective.com/swiper" - } - ], + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, "engines": { - "node": ">= 4.7.0" + "node": ">=10" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/synckit": { - "version": "0.11.6", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.6.tgz", - "integrity": "sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw==", + "node_modules/stylelint/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.4" - }, "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" + "node": ">=8" } }, - "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", - "dev": true, - "license": "MIT" - }, - "node_modules/table": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", - "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "node_modules/stylelint/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">=10.0.0" + "node": ">=4" } }, - "node_modules/table/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/stylelint/node_modules/redent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", + "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "indent-string": "^5.0.0", + "strip-indent": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/table/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/stylelint/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=7.0.0" + "node": ">=10" } }, - "node_modules/table/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/table/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "node_modules/stylelint/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, + "license": "ISC", "engines": { - "node": ">=10" + "node": ">=14" }, "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "node_modules/stylelint/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", "dev": true, - "license": "MIT", + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "node_modules/stylelint/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "streamx": "^2.12.5" + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, - "node_modules/terser": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", - "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "node_modules/stylelint/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, - "license": "BSD-2-Clause", + "license": "ISC", "dependencies": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - }, - "bin": { - "terser": "bin/terser" + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">=6.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "node_modules/stylelint/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, + "license": "ISC" + }, + "node_modules/stylelint/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } + "node": ">=10" } }, - "node_modules/terser-webpack-plugin/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "dev": true, "license": "MIT" }, - "node_modules/terser-webpack-plugin/node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">=8" } }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", "dev": true, "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">=14.18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser-webpack-plugin/node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" } }, - "node_modules/terser-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/terser-webpack-plugin/node_modules/terser": { - "version": "5.39.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", - "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", + "node_modules/sver": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", + "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.14.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" + "license": "MIT", + "optionalDependencies": { + "semver": "^6.3.0" } }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "dev": true, "license": "MIT" }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" }, "engines": { - "node": ">=8" + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" } }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": ">= 10" } }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/swiper": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-10.3.1.tgz", + "integrity": "sha512-24Wk3YUdZHxjc9faID97GTu6xnLNia+adMt6qMTZG/HgdSUt4fS0REsGUXJOgpTED0Amh/j+gRGQxsLayJUlBQ==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "engines": { + "node": ">= 4.7.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.6.tgz", + "integrity": "sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw==", + "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@pkgr/core": "^0.2.4" }, "engines": { - "node": "*" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" } }, - "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, - "license": "Apache-2.0", + "license": "BSD-3-Clause", "dependencies": { - "b4a": "^1.6.4" + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" } }, - "node_modules/text-extensions": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", - "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", + "node_modules/table/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/textextensions": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-3.3.0.tgz", - "integrity": "sha512-mk82dS8eRABNbeVJrEiN5/UMSCliINAuz8mkUwH4SwslkNP//gbEzlWNS5au0z5Dpx40SQxzqZevZkn+WYJ9Dw==", + "node_modules/table/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "color-name": "~1.1.4" }, - "funding": { - "url": "https://bevry.me/fund" + "engines": { + "node": ">=7.0.0" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "node_modules/table/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "node_modules/table/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "license": "MIT", "dependencies": { - "readable-stream": "3" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/through2-filter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", - "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", "dev": true, "license": "MIT", - "dependencies": { - "through2": "~2.0.0", - "xtend": "~4.0.0" + "engines": { + "node": ">=6" } }, - "node_modules/through2-filter/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", "dev": true, "license": "MIT", "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "streamx": "^2.12.5" } }, - "node_modules/through2/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "node_modules/terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/timers-ext": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", - "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", - "dev": true, - "license": "ISC", - "dependencies": { - "es5-ext": "^0.10.64", - "next-tick": "^1.1.0" + "bin": { + "terser": "bin/terser" }, "engines": { - "node": ">=0.12" + "node": ">=6.0.0" } }, - "node_modules/timers-ext/node_modules/es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dev": true, - "hasInstallScript": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { - "node": ">=0.10" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } } }, - "node_modules/timers-ext/node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "node_modules/terser-webpack-plugin/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "license": "MIT" }, - "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": ">= 10.13.0" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, "engines": { - "node": ">=12" + "node": ">= 10.13.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-absolute-glob": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", - "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", + "node_modules/terser-webpack-plugin/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", "dependencies": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/to-through": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", - "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "5.39.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", + "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "streamx": "^2.12.5" + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" }, "engines": { - "node": ">=10.13.0" + "node": ">=10" } }, - "node_modules/toggle-selection": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "license": "MIT" }, - "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, - "license": "BSD-3-Clause", + "license": "ISC", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" }, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 4.0.0" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "punycode": "^2.1.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=12" + "node": "*" } }, - "node_modules/trim-newlines": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", - "integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==", + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-extensions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "license": "MIT" }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "node_modules/textextensions": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-3.3.0.tgz", + "integrity": "sha512-mk82dS8eRABNbeVJrEiN5/UMSCliINAuz8mkUwH4SwslkNP//gbEzlWNS5au0z5Dpx40SQxzqZevZkn+WYJ9Dw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18.12" + "node": ">=8" }, - "peerDependencies": { - "typescript": ">=4.8.4" + "funding": { + "url": "https://bevry.me/fund" } }, - "node_modules/ts-dedent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.10" - } + "license": "MIT" }, - "node_modules/ts-jest": { - "version": "29.3.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", - "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==", + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", "dev": true, "license": "MIT", "dependencies": { - "bs-logger": "^0.2.6", - "ejs": "^3.1.10", - "fast-json-stable-stringify": "^2.1.0", - "jest-util": "^29.0.0", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.7.2", - "type-fest": "^4.41.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", - "typescript": ">=4.3 <6" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "readable-stream": "3" } - }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + }, + "node_modules/through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", "dev": true, "license": "MIT", "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" + "through2": "~2.0.0", + "xtend": "~4.0.0" } }, - "node_modules/tsconfig-paths/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "node_modules/through2-filter/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=4" + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "node_modules/through2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "license": "MIT", "dependencies": { - "tslib": "^1.8.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "license": "0BSD" - }, - "node_modules/type": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", - "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "prelude-ls": "^1.2.1" + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=0.12" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "node_modules/timers-ext/node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "dev": true, - "license": "MIT", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, "engines": { - "node": ">=4" + "node": ">=0.10" } }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "node_modules/timers-ext/node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "ISC" }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } + "license": "MIT" }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" + "fdir": "^6.4.4", + "picomatch": "^4.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">=12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" }, "engines": { - "node": ">=14.17" + "node": ">=8.0" } }, - "node_modules/typograf": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/typograf/-/typograf-7.4.4.tgz", - "integrity": "sha512-/DonkfuPyTMAZMweHfBdB8S8B4tSjc7gXiADs47lloPS2GHVlN+IJypyOqvRkpszfwCjnWv+3q59MUY6BhVcHA==", + "node_modules/to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", + "dev": true, "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + }, "engines": { - "node": ">= 4" + "node": ">=10.13.0" } }, - "node_modules/uc.micro": { + "node_modules/toggle-selection": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", "dev": true, "license": "MIT" }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6" } }, - "node_modules/unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 4.0.0" } }, - "node_modules/undertaker": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", - "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "dev": true, "license": "MIT", "dependencies": { - "bach": "^2.0.1", - "fast-levenshtein": "^3.0.0", - "last-run": "^2.0.0", - "undertaker-registry": "^2.0.0" + "punycode": "^2.1.1" }, "engines": { - "node": ">=10.13.0" + "node": ">=12" } }, - "node_modules/undertaker-registry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", - "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", + "node_modules/trim-newlines": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", + "integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10.13.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/undertaker/node_modules/fast-levenshtein": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", - "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "dev": true, "license": "MIT", - "dependencies": { - "fastest-levenshtein": "^1.0.7" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/undici": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", - "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=18.17" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=6.10" } }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "node_modules/ts-jest": { + "version": "29.3.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", + "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==", "dev": true, "license": "MIT", "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" }, "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } } }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "license": "MIT", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": ">=4" + "node": ">=10" } }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, - "license": "MIT", + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=18" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, "license": "MIT", "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=6" } }, - "node_modules/unique-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", - "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, "license": "MIT", "dependencies": { - "json-stable-stringify-without-jsonify": "^1.0.1", - "through2-filter": "^3.0.0" + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, - "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { - "@types/unist": "^3.0.0" + "prelude-ls": "^1.2.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=4" } }, - "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universal-cookie": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.2.2.tgz", - "integrity": "sha512-fMiOcS3TmzP2x5QV26pIH3mvhexLIT0HmPa3V7Q7knRfT9HG6kTwq02HZGLPw0sAOXrAmotElGRvTLCMbJsvxQ==", - "license": "MIT", - "dependencies": { - "@types/cookie": "^0.6.0", - "cookie": "^0.7.2" + "engines": { + "node": ">= 0.4" } }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, "engines": { - "node": ">= 10.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unplugin": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", - "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.14.0", - "webpack-virtual-modules": "^0.6.2" + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { - "node": ">=14.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unrs-resolver": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.7.2.tgz", - "integrity": "sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A==", + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, - "hasInstallScript": true, "license": "MIT", "dependencies": { - "napi-postinstall": "^0.2.2" + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" }, - "funding": { - "url": "https://github.com/sponsors/JounQin" + "engines": { + "node": ">= 0.4" }, - "optionalDependencies": { - "@unrs/resolver-binding-darwin-arm64": "1.7.2", - "@unrs/resolver-binding-darwin-x64": "1.7.2", - "@unrs/resolver-binding-freebsd-x64": "1.7.2", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.7.2", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.7.2", - "@unrs/resolver-binding-linux-arm64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-arm64-musl": "1.7.2", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-riscv64-musl": "1.7.2", - "@unrs/resolver-binding-linux-s390x-gnu": "1.7.2", - "@unrs/resolver-binding-linux-x64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-x64-musl": "1.7.2", - "@unrs/resolver-binding-wasm32-wasi": "1.7.2", - "@unrs/resolver-binding-win32-arm64-msvc": "1.7.2", - "@unrs/resolver-binding-win32-ia32-msvc": "1.7.2", - "@unrs/resolver-binding-win32-x64-msvc": "1.7.2" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, + "license": "Apache-2.0", "bin": { - "update-browserslist-db": "cli.js" + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, - "peerDependencies": { - "browserslist": ">= 4.21.0" + "engines": { + "node": ">=14.17" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" + "node_modules/typograf": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/typograf/-/typograf-7.4.4.tgz", + "integrity": "sha512-/DonkfuPyTMAZMweHfBdB8S8B4tSjc7gXiADs47lloPS2GHVlN+IJypyOqvRkpszfwCjnWv+3q59MUY6BhVcHA==", + "license": "MIT", + "engines": { + "node": ">= 4" } }, - "node_modules/urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", - "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "dev": true, "license": "MIT" }, - "node_modules/url": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", - "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "license": "MIT", "dependencies": { - "punycode": "^1.4.1", - "qs": "^6.12.3" + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", "dev": true, "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/url/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/use-isomorphic-layout-effect": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", - "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "node_modules/undertaker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", + "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", "dev": true, "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "dependencies": { + "bach": "^2.0.1", + "fast-levenshtein": "^3.0.0", + "last-run": "^2.0.0", + "undertaker-registry": "^2.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=10.13.0" } }, - "node_modules/use-memo-one": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", - "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "node_modules/undertaker-registry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", + "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", "dev": true, "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "engines": { + "node": ">= 10.13.0" } }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "node_modules/undertaker/node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", "dev": true, "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" + "fastest-levenshtein": "^1.0.7" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18.17" + } }, - "node_modules/utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, - "node_modules/utility-types": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", - "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 4" - } - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "node": ">=4" } }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" }, "engines": { - "node": ">=10.12.0" + "node": ">=4" } }, - "node_modules/v8flags": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", - "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10.13.0" + "node": ">=4" } }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "node_modules/value-or-function": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", - "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10.13.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/vfile-message": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "node_modules/unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", "dev": true, "license": "MIT", "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "dev": true, "license": "MIT", "dependencies": { - "clone": "^2.1.2", - "clone-stats": "^1.0.0", - "remove-trailing-separator": "^1.1.0", - "replace-ext": "^2.0.0", - "teex": "^1.0.1" + "@types/unist": "^3.0.0" }, - "engines": { - "node": ">=10.13.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/vinyl-contents": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", - "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", "dev": true, "license": "MIT", "dependencies": { - "bl": "^5.0.0", - "vinyl": "^3.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" }, - "engines": { - "node": ">=10.13.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/vinyl-fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", - "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", "dev": true, "license": "MIT", "dependencies": { - "fs-mkdirp-stream": "^2.0.1", - "glob-stream": "^8.0.0", - "graceful-fs": "^4.2.11", - "iconv-lite": "^0.6.3", - "is-valid-glob": "^1.0.0", - "lead": "^4.0.0", - "normalize-path": "3.0.0", - "resolve-options": "^2.0.0", - "stream-composer": "^1.0.2", - "streamx": "^2.14.0", - "to-through": "^3.0.0", - "value-or-function": "^4.0.0", - "vinyl": "^3.0.0", - "vinyl-sourcemap": "^2.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" }, - "engines": { - "node": ">=10.13.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/vinyl-sourcemap": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", - "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "node_modules/universal-cookie": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.2.2.tgz", + "integrity": "sha512-fMiOcS3TmzP2x5QV26pIH3mvhexLIT0HmPa3V7Q7knRfT9HG6kTwq02HZGLPw0sAOXrAmotElGRvTLCMbJsvxQ==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.7.2" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", - "dependencies": { - "convert-source-map": "^2.0.0", - "graceful-fs": "^4.2.10", - "now-and-later": "^3.0.0", - "streamx": "^2.12.5", - "vinyl": "^3.0.0", - "vinyl-contents": "^2.0.0" - }, "engines": { - "node": ">=10.13.0" + "node": ">= 10.0.0" } }, - "node_modules/vinyl-sourcemaps-apply": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", - "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==", + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "source-map": "^0.5.1" - } - }, - "node_modules/vinyl-sourcemaps-apply/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "license": "BSD-3-Clause", + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">=14.0.0" } }, - "node_modules/vite": { - "version": "5.4.19", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", - "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "node_modules/unrs-resolver": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.7.2.tgz", + "integrity": "sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A==", "dev": true, + "hasInstallScript": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" + "napi-postinstall": "^0.2.2" }, "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" + "url": "https://github.com/sponsors/JounQin" }, "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } + "@unrs/resolver-binding-darwin-arm64": "1.7.2", + "@unrs/resolver-binding-darwin-x64": "1.7.2", + "@unrs/resolver-binding-freebsd-x64": "1.7.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.7.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.7.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.7.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.7.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.7.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.7.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.7.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.7.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.7.2", + "@unrs/resolver-binding-linux-x64-musl": "1.7.2", + "@unrs/resolver-binding-wasm32-wasi": "1.7.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.7.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.7.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.7.2" } }, - "node_modules/vite-plugin-commonjs": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/vite-plugin-commonjs/-/vite-plugin-commonjs-0.10.4.tgz", - "integrity": "sha512-eWQuvQKCcx0QYB5e5xfxBNjQKyrjEWZIR9UOkOV6JAgxVhtbZvCOF+FNC2ZijBJ3U3Px04ZMMyyMyFBVWIJ5+g==", + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "acorn": "^8.12.1", - "magic-string": "^0.30.11", - "vite-plugin-dynamic-import": "^1.6.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "node_modules/vite-plugin-dynamic-import": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.6.0.tgz", - "integrity": "sha512-TM0sz70wfzTIo9YCxVFwS8OA9lNREsh+0vMHGSkWDTZ7bgd1Yjs5RV8EgB634l/91IsXJReg0xtmuQqP0mf+rg==", + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.12.1", - "es-module-lexer": "^1.5.4", - "fast-glob": "^3.3.2", - "magic-string": "^0.30.11" + "punycode": "^2.1.0" } }, - "node_modules/vite-plugin-svgr": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.3.0.tgz", - "integrity": "sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==", + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "dev": true, + "license": "MIT" + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", "dev": true, "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^5.1.3", - "@svgr/core": "^8.1.0", - "@svgr/plugin-jsx": "^8.1.0" + "punycode": "^1.4.1", + "qs": "^6.12.3" }, - "peerDependencies": { - "vite": ">=2.6.0" + "engines": { + "node": ">= 0.4" } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" } }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" } }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", "dev": true, + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=12" + "node": ">= 4" } }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" ], - "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" + "bin": { + "uuid": "dist/bin/uuid" } }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, "engines": { - "node": ">=12" + "node": ">=10.12.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], + "node_modules/v8flags": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">= 10.13.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], + "node_modules/value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">= 10.13.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], + "node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, "engines": { - "node": ">=12" + "node": ">=10.13.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], + "node_modules/vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, "engines": { - "node": ">=12" + "node": ">=10.13.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], + "node_modules/vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + }, "engines": { - "node": ">=12" + "node": ">=10.13.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], + "node_modules/vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, "engines": { - "node": ">=12" + "node": ">=10.13.0" } }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], + "node_modules/vinyl-sourcemaps-apply": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", + "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" + "license": "ISC", + "dependencies": { + "source-map": "^0.5.1" } }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], + "node_modules/vinyl-sourcemaps-apply/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "license": "BSD-3-Clause", "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">=12" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], + "node_modules/vite-plugin-commonjs": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/vite-plugin-commonjs/-/vite-plugin-commonjs-0.10.4.tgz", + "integrity": "sha512-eWQuvQKCcx0QYB5e5xfxBNjQKyrjEWZIR9UOkOV6JAgxVhtbZvCOF+FNC2ZijBJ3U3Px04ZMMyyMyFBVWIJ5+g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "dependencies": { + "acorn": "^8.12.1", + "magic-string": "^0.30.11", + "vite-plugin-dynamic-import": "^1.6.0" } }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], + "node_modules/vite-plugin-dynamic-import": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.6.0.tgz", + "integrity": "sha512-TM0sz70wfzTIo9YCxVFwS8OA9lNREsh+0vMHGSkWDTZ7bgd1Yjs5RV8EgB634l/91IsXJReg0xtmuQqP0mf+rg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "dependencies": { + "acorn": "^8.12.1", + "es-module-lexer": "^1.5.4", + "fast-glob": "^3.3.2", + "magic-string": "^0.30.11" } }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], + "node_modules/vite-plugin-svgr": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.3.0.tgz", + "integrity": "sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@rollup/pluginutils": "^5.1.3", + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0" + }, + "peerDependencies": { + "vite": ">=2.6.0" } }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { "node": ">=12" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/w3c-xmlserializer": { @@ -30397,6 +31553,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index d7fa3ce3e2..46b6b5dd50 100644 --- a/package.json +++ b/package.json @@ -24,14 +24,43 @@ "require": "./build/cjs/editor/index.js", "import": "./build/esm/editor/index.js" }, + "./editor-v2": { + "types": "./build/esm/editor-v2/index.d.ts", + "require": "./build/cjs/editor-v2/index.js", + "import": "./build/esm/editor-v2/index.js" + }, + "./form-generator": { + "types": "./build/esm/form-generator/index.d.ts", + "require": "./build/cjs/form-generator/index.js", + "import": "./build/esm/form-generator/index.js" + }, + "./form-generator-v2": { + "types": "./build/esm/form-generator-v2/index.d.ts", + "require": "./build/cjs/form-generator-v2/index.js", + "import": "./build/esm/form-generator-v2/index.js" + }, + "./form-builder": { + "types": "./build/esm/form-builder/index.d.ts", + "require": "./build/cjs/form-builder/index.js", + "import": "./build/esm/form-builder/index.js" + }, + "./form-builder-v2": { + "types": "./build/esm/form-builder-v2/index.d.ts", + "require": "./build/cjs/form-builder-v2/index.js", + "import": "./build/esm/form-builder-v2/index.js" + }, + "./gravity-blocks": { + "types": "./build/esm/gravity-blocks/index.d.ts", + "require": "./build/cjs/gravity-blocks/index.js", + "import": "./build/esm/gravity-blocks/index.js" + }, "./server": { "types": "./server/index.d.ts", "require": "./server/index.js", "import": "./server/index.js" }, "./styles/*": "./styles/*", - "./widget/*": "./widget/*", - "./schema/*": "./schema/*" + "./widget/*": "./widget/*" }, "main": "./build/cjs/index.js", "module": "./build/esm/index.js", @@ -44,6 +73,24 @@ "editor": [ "./build/esm/editor/index.d.ts" ], + "editor-v2": [ + "./build/esm/editor-v2/index.d.ts" + ], + "form-generator": [ + "./build/esm/form-generator/index.d.ts" + ], + "form-generator-v2": [ + "./build/esm/form-generator-v2/index.d.ts" + ], + "form-builder": [ + "./build/esm/form-builder/index.d.ts" + ], + "form-builder-v2": [ + "./build/esm/form-builder-v2/index.d.ts" + ], + "gravity-blocks": [ + "./build/esm/gravity-blocks/index.d.ts" + ], "server": [ "./server/index.d.ts" ] @@ -53,8 +100,7 @@ "build", "styles", "server", - "widget", - "schema" + "widget" ], "sideEffects": [ "*.css", @@ -73,6 +119,7 @@ "lint": "run-p lint:js lint:styles lint:prettier typecheck", "typecheck": "tsc --noEmit", "dev": "npm run storybook:start", + "dev:playground": "cd playground && vite -c vite.config.mts", "storybook:start": "storybook dev -p 7009", "storybook:build": "storybook build -c .storybook -o storybook-static", "start": "node dist", @@ -82,6 +129,7 @@ "build:widget": "webpack --config widget.webpack.js", "build:schema": "webpack --config schema.webpack.js", "build": "run-p build:client build:server build:widget build:schema", + "build:playground": "cd playground && tsc && vite build -c vite.config.mts", "prepublishOnly": "npm run lint && npm run build", "prepare": "husky install", "test": "jest", @@ -93,26 +141,31 @@ "playwright:docker": "./scripts/playwright-docker.sh 'npm run playwright'", "playwright:docker:update": "./scripts/playwright-docker.sh 'npm run playwright:update'", "playwright:docker:clear-cache": "./scripts/playwright-docker.sh clear-cache", - "playwright:install": "playwright install --with-deps", - "build:playground": "echo wip" + "playwright:install": "playwright install --with-deps" }, "dependencies": { "@bem-react/classname": "^1.6.0", + "@dnd-kit/helpers": "^0.3.2", + "@dnd-kit/react": "^0.3.2", "@gravity-ui/components": "^4.0.1", "@gravity-ui/dynamic-forms": "^5.0.0", "@gravity-ui/i18n": "^1.7.0", + "@gravity-ui/navigation": "^3.11.1", "@gravity-ui/icons": "^2.18.0", "@react-spring/web": "^9.7.3", "ajv": "^8.12.0", "ajv-keywords": "^5.1.0", + "deep-object-diff": "^1.1.9", "final-form": "^4.20.9", "github-buttons": "2.23.0", + "immutable": "^4.3.7", "js-yaml-source-map": "^0.2.2", "lodash": "^4.17.21", "monaco-editor": "^0.52.2", "react-final-form": "^6.5.9", "react-monaco-editor": "^0.53.0", "react-player": "^2.9.0", + "react-resizable-panels": "^2.1.3", "react-slick": "^0.29.0", "react-transition-group": "^4.4.2", "react-waypoint": "^10.1.0", @@ -121,7 +174,8 @@ "swiper": "^10.2.0", "typograf": "^7.4.1", "utility-types": "^3.10.0", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "zustand": "^4.5.2" }, "peerDependencies": { "@diplodoc/transform": "^4.28.2", @@ -141,7 +195,7 @@ "@gravity-ui/prettier-config": "^1.1.0", "@gravity-ui/stylelint-config": "^4.0.1", "@gravity-ui/tsconfig": "^1.0.0", - "@gravity-ui/uikit": "^7.13.1", + "@gravity-ui/uikit": "^7.29.0", "@playwright/experimental-ct-react": "^1.45.3", "@playwright/test": "^1.45.3", "@storybook/addon-actions": "^8.6.11", @@ -170,12 +224,15 @@ "@types/uuid": "^9.0.0", "@types/webpack-env": "^1.18.1", "@types/youtube-player": "^5.5.11", + "@vitejs/plugin-react": "^4.5.0", "autoprefixer": "^10.4.14", "babel-jest": "^30.2.0", "babel-loader": "^8.3.0", + "bem-cn-lite": "^4.1.0", "css-loader": "^5.2.7", "es5-ext": "0.10.53", "esbuild": "^0.25.11", + "esbuild-sass-plugin": "^3.7.0", "eslint": "^8.57.1", "eslint-plugin-no-not-accumulator-reassign": "^0.1.0", "eslint-plugin-react": "^7.37.4", @@ -206,6 +263,7 @@ "react": "^18.3.1", "react-docgen-typescript": "^2.2.2", "react-dom": "^18.3.1", + "react-router": "^7.6.1", "resolve-url-loader": "^3.1.5", "rimraf": "^6.0.1", "sass": "^1.63.6", @@ -218,12 +276,17 @@ "ts-jest": "^29.2.5", "tslib": "^2.4.0", "typescript": "^5.7.3", + "vite": "^6.3.5", "vite-plugin-commonjs": "^0.10.1", - "vite-plugin-svgr": "^4.2.0", + "vite-plugin-svgr": "^4.3.0", "webpack": "^5.98.0", "webpack-cli": "^6.0.1", "webpack-shell-plugin-next": "^2.3.1" }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.60.1", + "@rollup/rollup-darwin-arm64": "^4.60.1" + }, "lint-staged": { "*.{css,scss}": [ "stylelint --fix", diff --git a/playground/.eslintignore b/playground/.eslintignore new file mode 100644 index 0000000000..dffb6a9edf --- /dev/null +++ b/playground/.eslintignore @@ -0,0 +1,3 @@ +build +dist +public diff --git a/playground/.gitignore b/playground/.gitignore new file mode 100644 index 0000000000..cf74115ddb --- /dev/null +++ b/playground/.gitignore @@ -0,0 +1,28 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build +/dist + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# typescript +*.tsbuildinfo diff --git a/playground/.prettierignore b/playground/.prettierignore new file mode 100644 index 0000000000..dffb6a9edf --- /dev/null +++ b/playground/.prettierignore @@ -0,0 +1,3 @@ +build +dist +public diff --git a/playground/index.html b/playground/index.html new file mode 100644 index 0000000000..be673cfe69 --- /dev/null +++ b/playground/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + Page Constructor Playground + + + +
+ + + diff --git a/playground/public/favicon.ico b/playground/public/favicon.ico new file mode 100644 index 0000000000..74cc5d2cdb Binary files /dev/null and b/playground/public/favicon.ico differ diff --git a/playground/public/manifest.json b/playground/public/manifest.json new file mode 100644 index 0000000000..c5e41e11a5 --- /dev/null +++ b/playground/public/manifest.json @@ -0,0 +1,13 @@ +{ + "short_name": "React App", + "name": "Gravity UI – Vite Example", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone" +} diff --git a/playground/public/vite.svg b/playground/public/vite.svg new file mode 100644 index 0000000000..e7b8dfb1b2 --- /dev/null +++ b/playground/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/playground/src/blocks/AccordionBlock/AccordionBlock.tsx b/playground/src/blocks/AccordionBlock/AccordionBlock.tsx new file mode 100644 index 0000000000..e0e8a5adc4 --- /dev/null +++ b/playground/src/blocks/AccordionBlock/AccordionBlock.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; + +import {Accordion} from '@gravity-ui/uikit'; + +export interface AccordionBlockItem { + summary: string; + content: string; +} + +export interface AccordionBlockProps { + items?: AccordionBlockItem[]; + size?: 'm' | 'l' | 'xl'; +} + +const AccordionBlock: React.FC = ({items = [], size = 'm'}) => { + return ( +
+ + {items.map((item, index) => ( + + {item.content} + + ))} + +
+ ); +}; + +export default AccordionBlock; diff --git a/playground/src/blocks/AccordionBlock/index.ts b/playground/src/blocks/AccordionBlock/index.ts new file mode 100644 index 0000000000..24e0c31742 --- /dev/null +++ b/playground/src/blocks/AccordionBlock/index.ts @@ -0,0 +1,58 @@ +import {BlockData} from '../../../../src/constructor-items'; +import {Fields} from '../../../../src/form-generator-v2/types'; + +import AccordionBlock from './AccordionBlock'; + +const AccordionBlockConfig: BlockData = { + type: 'custom/atom-accordion', + component: AccordionBlock, + schema: { + name: 'Accordion', + group: 'custom/atoms', + inputs: [ + { + type: 'section', + title: 'Settings', + opened: true, + fields: [ + { + type: 'segmentedRadioGroup', + name: 'size', + title: 'Size', + options: [ + {value: 'm', content: 'M'}, + {value: 'l', content: 'L'}, + {value: 'xl', content: 'XL'}, + ], + defaultValue: 'm', + }, + ], + }, + { + type: 'section', + title: 'Items', + withAddButton: true, + index: 'index', + itemTitle: 'Item {{index}}', + itemView: 'card', + fields: [ + {type: 'textInput', name: 'items[{{index}}].summary', title: 'Summary'}, + {type: 'textArea', name: 'items[{{index}}].content', title: 'Content'}, + ], + }, + ] as Fields, + default: { + size: 'm', + items: [ + {summary: 'What is this?', content: 'This is the first accordion item content.'}, + { + summary: 'How does it work?', + content: 'It uses the Accordion component from Gravity UI.', + }, + {summary: 'Can I add more?', content: 'Yes! Add as many items as you need.'}, + ], + }, + }, +}; + +export default AccordionBlockConfig; diff --git a/playground/src/blocks/AlertBlock/AlertBlock.tsx b/playground/src/blocks/AlertBlock/AlertBlock.tsx new file mode 100644 index 0000000000..4cb8510ef7 --- /dev/null +++ b/playground/src/blocks/AlertBlock/AlertBlock.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; + +import {Alert} from '@gravity-ui/uikit'; + +export interface AlertBlockProps { + title?: string; + message?: string; + theme?: 'normal' | 'info' | 'success' | 'warning' | 'danger' | 'utility' | 'clear'; + view?: 'filled' | 'outlined'; + corners?: 'rounded' | 'square'; + layout?: 'vertical' | 'horizontal'; +} + +const AlertBlock: React.FC = ({ + title, + message = '', + theme = 'normal', + view = 'filled', + corners = 'rounded', + layout = 'horizontal', +}) => { + return ( +
+ +
+ ); +}; + +export default AlertBlock; diff --git a/playground/src/blocks/AlertBlock/index.ts b/playground/src/blocks/AlertBlock/index.ts new file mode 100644 index 0000000000..ff4d305209 --- /dev/null +++ b/playground/src/blocks/AlertBlock/index.ts @@ -0,0 +1,77 @@ +import {BlockData} from '../../../../src/constructor-items'; +import {Fields} from '../../../../src/form-generator-v2/types'; + +import AlertBlock from './AlertBlock'; + +const AlertBlockConfig: BlockData = { + type: 'custom/atom-alert', + component: AlertBlock, + schema: { + name: 'Alert', + group: 'custom/atoms', + inputs: [ + { + type: 'section', + title: 'Alert', + opened: true, + fields: [ + {type: 'textInput', name: 'title', title: 'Title'}, + {type: 'textArea', name: 'message', title: 'Message'}, + { + type: 'segmentedRadioGroup', + name: 'theme', + title: 'Theme', + options: [ + {value: 'normal', content: 'Normal'}, + {value: 'info', content: 'Info'}, + {value: 'success', content: 'Success'}, + {value: 'warning', content: 'Warning'}, + {value: 'danger', content: 'Danger'}, + ], + defaultValue: 'normal', + }, + { + type: 'segmentedRadioGroup', + name: 'view', + title: 'View', + options: [ + {value: 'filled', content: 'Filled'}, + {value: 'outlined', content: 'Outlined'}, + ], + defaultValue: 'filled', + }, + { + type: 'segmentedRadioGroup', + name: 'corners', + title: 'Corners', + options: [ + {value: 'rounded', content: 'Rounded'}, + {value: 'square', content: 'Square'}, + ], + defaultValue: 'rounded', + }, + { + type: 'segmentedRadioGroup', + name: 'layout', + title: 'Layout', + options: [ + {value: 'horizontal', content: 'Horizontal'}, + {value: 'vertical', content: 'Vertical'}, + ], + defaultValue: 'horizontal', + }, + ], + }, + ] as Fields, + default: { + title: 'Heads up!', + message: 'This is an informational message.', + theme: 'info', + view: 'filled', + corners: 'rounded', + layout: 'horizontal', + }, + }, +}; + +export default AlertBlockConfig; diff --git a/playground/src/blocks/AvatarBlock/AvatarBlock.tsx b/playground/src/blocks/AvatarBlock/AvatarBlock.tsx new file mode 100644 index 0000000000..8f48983549 --- /dev/null +++ b/playground/src/blocks/AvatarBlock/AvatarBlock.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; + +import {Avatar} from '@gravity-ui/uikit'; +import type {AvatarSize} from '@gravity-ui/uikit'; + +export interface AvatarBlockProps { + text?: string; + imgUrl?: string; + size?: AvatarSize; + theme?: 'normal' | 'brand'; + view?: 'filled' | 'outlined'; +} + +const AvatarBlock: React.FC = ({ + text = 'AB', + imgUrl, + size = 'xl', + theme = 'normal', + view = 'filled', +}) => { + const avatarProps = imgUrl ? {imgUrl, text, size, theme, view} : {text, size, theme, view}; + + return ( +
+ +
+ ); +}; + +export default AvatarBlock; diff --git a/playground/src/blocks/AvatarBlock/index.ts b/playground/src/blocks/AvatarBlock/index.ts new file mode 100644 index 0000000000..6a403e5194 --- /dev/null +++ b/playground/src/blocks/AvatarBlock/index.ts @@ -0,0 +1,67 @@ +import {BlockData} from '../../../../src/constructor-items'; +import {Fields} from '../../../../src/form-generator-v2/types'; + +import AvatarBlock from './AvatarBlock'; + +const AvatarBlockConfig: BlockData = { + type: 'custom/atom-avatar', + component: AvatarBlock, + schema: { + name: 'Avatar', + group: 'custom/atoms', + inputs: [ + { + type: 'section', + title: 'Avatar', + opened: true, + fields: [ + {type: 'textInput', name: 'text', title: 'Initials / fallback text'}, + {type: 'textInput', name: 'imgUrl', title: 'Image URL'}, + { + type: 'segmentedRadioGroup', + name: 'size', + title: 'Size', + options: [ + {value: 'xs', content: 'XS'}, + {value: 's', content: 'S'}, + {value: 'm', content: 'M'}, + {value: 'l', content: 'L'}, + {value: 'xl', content: 'XL'}, + {value: '2xl', content: '2XL'}, + {value: '3xl', content: '3XL'}, + ], + defaultValue: 'xl', + }, + { + type: 'segmentedRadioGroup', + name: 'theme', + title: 'Theme', + options: [ + {value: 'normal', content: 'Normal'}, + {value: 'brand', content: 'Brand'}, + ], + defaultValue: 'normal', + }, + { + type: 'segmentedRadioGroup', + name: 'view', + title: 'View', + options: [ + {value: 'filled', content: 'Filled'}, + {value: 'outlined', content: 'Outlined'}, + ], + defaultValue: 'filled', + }, + ], + }, + ] as Fields, + default: { + text: 'AJ', + size: 'xl', + theme: 'brand', + view: 'filled', + }, + }, +}; + +export default AvatarBlockConfig; diff --git a/playground/src/blocks/BreadcrumbsBlock/BreadcrumbsBlock.tsx b/playground/src/blocks/BreadcrumbsBlock/BreadcrumbsBlock.tsx new file mode 100644 index 0000000000..34244d0d71 --- /dev/null +++ b/playground/src/blocks/BreadcrumbsBlock/BreadcrumbsBlock.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; + +import {Breadcrumbs} from '@gravity-ui/uikit'; + +export interface BreadcrumbsItem { + text: string; + href?: string; +} + +export interface BreadcrumbsBlockProps { + items?: BreadcrumbsItem[]; +} + +const BreadcrumbsBlock: React.FC = ({items = []}) => { + if (items.length === 0) return null; + + return ( +
+ + {items.map((item, index) => ( + + {item.text} + + ))} + +
+ ); +}; + +export default BreadcrumbsBlock; diff --git a/playground/src/blocks/BreadcrumbsBlock/index.ts b/playground/src/blocks/BreadcrumbsBlock/index.ts new file mode 100644 index 0000000000..52c333e9ec --- /dev/null +++ b/playground/src/blocks/BreadcrumbsBlock/index.ts @@ -0,0 +1,36 @@ +import {BlockData} from '../../../../src/constructor-items'; +import {Fields} from '../../../../src/form-generator-v2/types'; + +import BreadcrumbsBlock from './BreadcrumbsBlock'; + +const BreadcrumbsBlockConfig: BlockData = { + type: 'custom/atom-breadcrumbs', + component: BreadcrumbsBlock, + schema: { + name: 'Breadcrumbs', + group: 'custom/atoms', + inputs: [ + { + type: 'section', + title: 'Items', + withAddButton: true, + index: 'index', + itemTitle: 'Item {{index}}', + itemView: 'card', + fields: [ + {type: 'textInput', name: 'items[{{index}}].text', title: 'Label'}, + {type: 'textInput', name: 'items[{{index}}].href', title: 'Link URL'}, + ], + }, + ] as Fields, + default: { + items: [ + {text: 'Home', href: '/'}, + {text: 'Products', href: '/products'}, + {text: 'Current page'}, + ], + }, + }, +}; + +export default BreadcrumbsBlockConfig; diff --git a/playground/src/blocks/ButtonBlock/ButtonBlock.tsx b/playground/src/blocks/ButtonBlock/ButtonBlock.tsx new file mode 100644 index 0000000000..13c179b58c --- /dev/null +++ b/playground/src/blocks/ButtonBlock/ButtonBlock.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; + +import {Button} from '@gravity-ui/uikit'; + +export interface ButtonBlockProps { + text?: string; + view?: 'normal' | 'action' | 'outlined' | 'flat' | 'raised'; + size?: 'xs' | 's' | 'm' | 'l' | 'xl'; + href?: string; + target?: '_blank' | '_self'; +} + +const ButtonBlock: React.FC = ({ + text = 'Button', + view = 'action', + size = 'm', + href, + target = '_self', +}) => { + return ( +
+ +
+ ); +}; + +export default ButtonBlock; diff --git a/playground/src/blocks/ButtonBlock/index.ts b/playground/src/blocks/ButtonBlock/index.ts new file mode 100644 index 0000000000..49d6af11cc --- /dev/null +++ b/playground/src/blocks/ButtonBlock/index.ts @@ -0,0 +1,67 @@ +import {BlockData} from '../../../../src/constructor-items'; +import {Fields} from '../../../../src/form-generator-v2/types'; + +import ButtonBlock from './ButtonBlock'; + +const ButtonBlockConfig: BlockData = { + type: 'custom/atom-button', + component: ButtonBlock, + schema: { + name: 'Button', + group: 'custom/atoms', + inputs: [ + { + type: 'section', + title: 'Button', + opened: true, + fields: [ + {type: 'textInput', name: 'text', title: 'Label'}, + { + type: 'segmentedRadioGroup', + name: 'view', + title: 'View', + options: [ + {value: 'action', content: 'Action'}, + {value: 'normal', content: 'Normal'}, + {value: 'outlined', content: 'Outlined'}, + {value: 'flat', content: 'Flat'}, + {value: 'raised', content: 'Raised'}, + ], + defaultValue: 'action', + }, + { + type: 'segmentedRadioGroup', + name: 'size', + title: 'Size', + options: [ + {value: 'xs', content: 'XS'}, + {value: 's', content: 'S'}, + {value: 'm', content: 'M'}, + {value: 'l', content: 'L'}, + {value: 'xl', content: 'XL'}, + ], + defaultValue: 'm', + }, + {type: 'textInput', name: 'href', title: 'Link URL'}, + { + type: 'select', + name: 'target', + title: 'Link target', + hasClear: true, + options: [ + {value: '_blank', content: 'New tab'}, + {value: '_self', content: 'Same tab'}, + ], + }, + ], + }, + ] as Fields, + default: { + text: 'Click me', + view: 'action', + size: 'm', + }, + }, +}; + +export default ButtonBlockConfig; diff --git a/playground/src/blocks/CardContainer/CardContainer.tsx b/playground/src/blocks/CardContainer/CardContainer.tsx new file mode 100644 index 0000000000..9723e555a8 --- /dev/null +++ b/playground/src/blocks/CardContainer/CardContainer.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; + +import {Card} from '@gravity-ui/uikit'; + +import ChildrenItemWrap from '../../../../src/components/editor/ChildrenItemWrap/ChildrenItemWrap'; +import ChildrensWrap from '../../../../src/components/editor/ChildrensWrap/ChildrensWrap'; + +export interface CardContainerProps extends React.PropsWithChildren { + title?: string; + description?: string; + theme?: 'normal' | 'info' | 'success' | 'warning' | 'danger'; + view?: 'outlined' | 'filled' | 'raised'; + size?: 'm' | 'l'; +} + +const CardContainer: React.FC = ({ + title, + description, + theme = 'normal', + view = 'outlined', + size = 'l', + children, +}) => { + return ( +
+ + {(title || description) && ( +
+ {title && ( +

+ {title} +

+ )} + {description && ( +

+ {description} +

+ )} +
+ )} +
+ + {React.Children.map(children, (child, index) => ( + {child} + ))} + +
+
+
+ ); +}; + +export default CardContainer; diff --git a/playground/src/blocks/CardContainer/index.ts b/playground/src/blocks/CardContainer/index.ts new file mode 100644 index 0000000000..353b04718d --- /dev/null +++ b/playground/src/blocks/CardContainer/index.ts @@ -0,0 +1,68 @@ +import {BlockData} from '../../../../src/constructor-items'; +import {Fields} from '../../../../src/form-generator-v2/types'; + +import CardContainer from './CardContainer'; + +const CardContainerConfig: BlockData = { + type: 'custom/container-card', + component: CardContainer, + schema: { + name: 'Card', + group: 'custom/containers', + inputs: [ + { + type: 'section', + title: 'Card', + opened: true, + fields: [ + {type: 'textInput', name: 'title', title: 'Title'}, + {type: 'textArea', name: 'description', title: 'Description'}, + { + type: 'segmentedRadioGroup', + name: 'theme', + title: 'Theme', + options: [ + {value: 'normal', content: 'Normal'}, + {value: 'info', content: 'Info'}, + {value: 'success', content: 'Success'}, + {value: 'warning', content: 'Warning'}, + {value: 'danger', content: 'Danger'}, + ], + defaultValue: 'normal', + }, + { + type: 'segmentedRadioGroup', + name: 'view', + title: 'View', + options: [ + {value: 'outlined', content: 'Outlined'}, + {value: 'filled', content: 'Filled'}, + {value: 'raised', content: 'Raised'}, + ], + defaultValue: 'outlined', + }, + { + type: 'segmentedRadioGroup', + name: 'size', + title: 'Size', + options: [ + {value: 'm', content: 'M'}, + {value: 'l', content: 'L'}, + ], + defaultValue: 'l', + }, + ], + }, + ] as Fields, + default: { + title: 'Card Title', + description: 'Optional card description.', + theme: 'normal', + view: 'outlined', + size: 'l', + children: [], + }, + }, +}; + +export default CardContainerConfig; diff --git a/playground/src/blocks/ColumnsContainer/ColumnsContainer.tsx b/playground/src/blocks/ColumnsContainer/ColumnsContainer.tsx new file mode 100644 index 0000000000..419a32468f --- /dev/null +++ b/playground/src/blocks/ColumnsContainer/ColumnsContainer.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; + +import ChildrenItemWrap from '../../../../src/components/editor/ChildrenItemWrap/ChildrenItemWrap'; +import ChildrensWrap from '../../../../src/components/editor/ChildrensWrap/ChildrensWrap'; + +export type ColumnsCount = 2 | 3 | 4; +export type ColumnsGap = 's' | 'm' | 'l' | 'space-between'; + +const GAP_SIZE: Record, number> = { + s: 8, + m: 16, + l: 32, +}; + +export interface ColumnsContainerProps extends React.PropsWithChildren { + columns?: ColumnsCount; + gap?: ColumnsGap; +} + +const ColumnsContainer: React.FC = ({columns = 2, gap = 'm', children}) => { + const isSpaceBetween = gap === 'space-between'; + const gapPx = isSpaceBetween ? 0 : (GAP_SIZE[gap] ?? GAP_SIZE.m); + + return ( +
+ +
+ {React.Children.map(children, (child, index) => ( + {child} + ))} +
+
+
+ ); +}; + +export default ColumnsContainer; diff --git a/playground/src/blocks/ColumnsContainer/index.ts b/playground/src/blocks/ColumnsContainer/index.ts new file mode 100644 index 0000000000..98314bb4ad --- /dev/null +++ b/playground/src/blocks/ColumnsContainer/index.ts @@ -0,0 +1,52 @@ +import {BlockData} from '../../../../src/constructor-items'; +import {Fields} from '../../../../src/form-generator-v2/types'; + +import ColumnsContainer from './ColumnsContainer'; + +const ColumnsContainerConfig: BlockData = { + type: 'custom/container-columns', + component: ColumnsContainer, + schema: { + name: 'Columns', + group: 'custom/containers', + inputs: [ + { + type: 'section', + title: 'Layout', + opened: true, + fields: [ + { + type: 'segmentedRadioGroup', + name: 'columns', + title: 'Columns', + options: [ + {value: '2', content: '2'}, + {value: '3', content: '3'}, + {value: '4', content: '4'}, + ], + defaultValue: '2', + }, + { + type: 'segmentedRadioGroup', + name: 'gap', + title: 'Gap', + options: [ + {value: 's', content: 'S'}, + {value: 'm', content: 'M'}, + {value: 'l', content: 'L'}, + {value: 'space-between', content: 'Space between'}, + ], + defaultValue: 'm', + }, + ], + }, + ] as Fields, + default: { + columns: 2, + gap: 'm', + children: [], + }, + }, +}; + +export default ColumnsContainerConfig; diff --git a/playground/src/blocks/DefinitionListBlock/DefinitionListBlock.tsx b/playground/src/blocks/DefinitionListBlock/DefinitionListBlock.tsx new file mode 100644 index 0000000000..6e508e3861 --- /dev/null +++ b/playground/src/blocks/DefinitionListBlock/DefinitionListBlock.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; + +import {DefinitionList} from '@gravity-ui/uikit'; + +export interface DefinitionListItem { + name: string; + value: string; + copyText?: string; + note?: string; +} + +export interface DefinitionListBlockProps { + title?: string; + items?: DefinitionListItem[]; + direction?: 'horizontal' | 'vertical'; + responsive?: boolean; + nameMaxWidth?: number; + contentMaxWidth?: number; +} + +const DefinitionListBlock: React.FC = ({ + title, + items = [], + direction = 'horizontal', + responsive = false, + nameMaxWidth, + contentMaxWidth, +}) => { + return ( +
+ {title && ( +

+ {title} +

+ )} + + {items.map((item, index) => ( + + {item.value} + + ))} + +
+ ); +}; + +export default DefinitionListBlock; diff --git a/playground/src/blocks/DefinitionListBlock/index.ts b/playground/src/blocks/DefinitionListBlock/index.ts new file mode 100644 index 0000000000..5f576ed713 --- /dev/null +++ b/playground/src/blocks/DefinitionListBlock/index.ts @@ -0,0 +1,79 @@ +import {BlockData} from '../../../../src/constructor-items'; +import {Fields} from '../../../../src/form-generator-v2/types'; + +import DefinitionListBlock from './DefinitionListBlock'; + +const DefinitionListBlockConfig: BlockData = { + type: 'custom/definition-list-block', + component: DefinitionListBlock, + schema: { + name: 'Definition List Block', + group: 'custom/atoms', + inputs: [ + {type: 'textInput', name: 'title', title: 'Block Title'}, + { + type: 'segmentedRadioGroup', + name: 'direction', + title: 'Direction', + options: [ + {value: 'horizontal', content: 'Horizontal'}, + {value: 'vertical', content: 'Vertical'}, + ], + defaultValue: 'horizontal', + }, + {type: 'switch', name: 'responsive', title: 'Responsive (100% width)'}, + {type: 'textInput', name: 'nameMaxWidth', title: 'Name Max Width (px)'}, + {type: 'textInput', name: 'contentMaxWidth', title: 'Content Max Width (px)'}, + { + type: 'section', + title: 'Definition Items', + index: 'index', + withAddButton: true, + itemTitle: 'Item {{index}}', + itemView: 'card', + fields: [ + {type: 'textInput', name: 'items[{{index}}].name', title: 'Term Name'}, + {type: 'textInput', name: 'items[{{index}}].value', title: 'Definition Value'}, + { + type: 'textInput', + name: 'items[{{index}}].copyText', + title: 'Copy Text (optional)', + }, + { + type: 'textInput', + name: 'items[{{index}}].note', + title: 'Note/Help Text (optional)', + }, + ], + }, + ] as Fields, + default: { + title: 'System Specifications', + direction: 'horizontal', + responsive: false, + items: [ + { + name: 'CPU', + value: 'Intel Core i9-13900K', + copyText: 'Intel Core i9-13900K', + }, + { + name: 'RAM', + value: '64GB DDR5-5600', + note: 'Maximum supported memory', + }, + { + name: 'Storage', + value: '2TB NVMe SSD', + }, + { + name: 'GPU', + value: 'NVIDIA RTX 4090 24GB', + copyText: 'NVIDIA RTX 4090 24GB', + }, + ], + }, + }, +}; + +export default DefinitionListBlockConfig; diff --git a/playground/src/blocks/LabelBlock/LabelBlock.tsx b/playground/src/blocks/LabelBlock/LabelBlock.tsx new file mode 100644 index 0000000000..f9a2a727a0 --- /dev/null +++ b/playground/src/blocks/LabelBlock/LabelBlock.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; + +import {Label} from '@gravity-ui/uikit'; + +export interface LabelBlockProps { + value?: string; + theme?: 'normal' | 'info' | 'danger' | 'warning' | 'success' | 'utility' | 'unknown' | 'clear'; + size?: 'xs' | 's' | 'm'; +} + +const LabelBlock: React.FC = ({value = 'Label', theme = 'normal', size = 'm'}) => { + return ( +
+ +
+ ); +}; + +export default LabelBlock; diff --git a/playground/src/blocks/LabelBlock/index.ts b/playground/src/blocks/LabelBlock/index.ts new file mode 100644 index 0000000000..5ebbad9795 --- /dev/null +++ b/playground/src/blocks/LabelBlock/index.ts @@ -0,0 +1,57 @@ +import {BlockData} from '../../../../src/constructor-items'; +import {Fields} from '../../../../src/form-generator-v2/types'; + +import LabelBlock from './LabelBlock'; + +const LabelBlockConfig: BlockData = { + type: 'custom/atom-label', + component: LabelBlock, + schema: { + name: 'Label', + group: 'custom/atoms', + inputs: [ + { + type: 'section', + title: 'Label', + opened: true, + fields: [ + {type: 'textInput', name: 'value', title: 'Text'}, + { + type: 'select', + name: 'theme', + title: 'Theme', + options: [ + {value: 'normal'}, + {value: 'info'}, + {value: 'success'}, + {value: 'warning'}, + {value: 'danger'}, + {value: 'utility'}, + {value: 'unknown'}, + {value: 'clear'}, + ], + defaultValue: 'normal', + }, + { + type: 'segmentedRadioGroup', + name: 'size', + title: 'Size', + options: [ + {value: 'xs', content: 'XS'}, + {value: 's', content: 'S'}, + {value: 'm', content: 'M'}, + ], + defaultValue: 'm', + }, + ], + }, + ] as Fields, + default: { + value: 'Status', + theme: 'info', + size: 'm', + }, + }, +}; + +export default LabelBlockConfig; diff --git a/playground/src/blocks/ProgressBlock/ProgressBlock.tsx b/playground/src/blocks/ProgressBlock/ProgressBlock.tsx new file mode 100644 index 0000000000..b13147613a --- /dev/null +++ b/playground/src/blocks/ProgressBlock/ProgressBlock.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +import {Progress} from '@gravity-ui/uikit'; + +export interface ProgressBlockProps { + value?: string; + text?: string; + theme?: 'default' | 'success' | 'warning' | 'danger' | 'misc'; + size?: 'xs' | 's' | 'm'; +} + +const ProgressBlock: React.FC = ({ + value = 50, + text, + theme = 'default', + size = 'm', +}) => { + return ( +
+ +
+ ); +}; + +export default ProgressBlock; diff --git a/playground/src/blocks/ProgressBlock/index.ts b/playground/src/blocks/ProgressBlock/index.ts new file mode 100644 index 0000000000..5223f203c8 --- /dev/null +++ b/playground/src/blocks/ProgressBlock/index.ts @@ -0,0 +1,65 @@ +import {BlockData} from '../../../../src/constructor-items'; +import {Fields} from '../../../../src/form-generator-v2/types'; + +import ProgressBlock from './ProgressBlock'; + +const ProgressBlockConfig: BlockData = { + type: 'custom/atom-progress', + component: ProgressBlock, + schema: { + name: 'Progress', + group: 'custom/atoms', + inputs: [ + { + type: 'section', + title: 'Progress', + opened: true, + fields: [ + { + type: 'select', + name: 'value', + title: 'Value (%)', + options: [0, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90, 100].map((v) => ({ + value: String(v), + content: `${v}%`, + })), + defaultValue: '50', + }, + {type: 'textInput', name: 'text', title: 'Label text'}, + { + type: 'segmentedRadioGroup', + name: 'theme', + title: 'Theme', + options: [ + {value: 'default', content: 'Default'}, + {value: 'success', content: 'Success'}, + {value: 'warning', content: 'Warning'}, + {value: 'danger', content: 'Danger'}, + {value: 'misc', content: 'Misc'}, + ], + defaultValue: 'default', + }, + { + type: 'segmentedRadioGroup', + name: 'size', + title: 'Size', + options: [ + {value: 'xs', content: 'XS'}, + {value: 's', content: 'S'}, + {value: 'm', content: 'M'}, + ], + defaultValue: 'm', + }, + ], + }, + ] as Fields, + default: { + value: 65, + text: '65%', + theme: 'success', + size: 'm', + }, + }, +}; + +export default ProgressBlockConfig; diff --git a/playground/src/blocks/SectionContainer/SectionContainer.tsx b/playground/src/blocks/SectionContainer/SectionContainer.tsx new file mode 100644 index 0000000000..cfe4c7ef99 --- /dev/null +++ b/playground/src/blocks/SectionContainer/SectionContainer.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; + +import {Divider} from '@gravity-ui/uikit'; + +import ChildrenItemWrap from '../../../../src/components/editor/ChildrenItemWrap/ChildrenItemWrap'; +import ChildrensWrap from '../../../../src/components/editor/ChildrensWrap/ChildrensWrap'; + +const BACKGROUND: Record = { + none: 'transparent', + subtle: 'var(--g-color-base-generic)', + brand: 'var(--g-color-base-brand)', +}; + +export interface SectionContainerProps extends React.PropsWithChildren { + title?: string; + description?: string; + background?: 'none' | 'subtle' | 'brand'; + withDivider?: boolean; +} + +const SectionContainer: React.FC = ({ + title, + description, + background = 'none', + withDivider = false, + children, +}) => { + const isBrand = background === 'brand'; + + return ( +
+ {(title || description) && ( +
+ {title && ( +

+ {title} +

+ )} + {description && ( +

+ {description} +

+ )} + {withDivider && } +
+ )} +
+ + {React.Children.map(children, (child, index) => ( + {child} + ))} + +
+
+ ); +}; + +export default SectionContainer; diff --git a/playground/src/blocks/SectionContainer/index.ts b/playground/src/blocks/SectionContainer/index.ts new file mode 100644 index 0000000000..d0740c18fe --- /dev/null +++ b/playground/src/blocks/SectionContainer/index.ts @@ -0,0 +1,45 @@ +import {BlockData} from '../../../../src/constructor-items'; +import {Fields} from '../../../../src/form-generator-v2/types'; + +import SectionContainer from './SectionContainer'; + +const SectionContainerConfig: BlockData = { + type: 'custom/container-section', + component: SectionContainer, + schema: { + name: 'Section', + group: 'custom/containers', + inputs: [ + { + type: 'section', + title: 'Section', + opened: true, + fields: [ + {type: 'textInput', name: 'title', title: 'Title'}, + {type: 'textArea', name: 'description', title: 'Description'}, + { + type: 'segmentedRadioGroup', + name: 'background', + title: 'Background', + options: [ + {value: 'none', content: 'None'}, + {value: 'subtle', content: 'Subtle'}, + {value: 'brand', content: 'Brand'}, + ], + defaultValue: 'none', + }, + {type: 'switch', name: 'withDivider', title: 'Show divider'}, + ], + }, + ] as Fields, + default: { + title: 'Section Title', + description: 'Optional section description.', + background: 'none', + withDivider: false, + children: [], + }, + }, +}; + +export default SectionContainerConfig; diff --git a/playground/src/blocks/TabsAtomBlock/TabsAtomBlock.tsx b/playground/src/blocks/TabsAtomBlock/TabsAtomBlock.tsx new file mode 100644 index 0000000000..7e692a2bfe --- /dev/null +++ b/playground/src/blocks/TabsAtomBlock/TabsAtomBlock.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; + +import {Tab, TabList, TabPanel, TabProvider} from '@gravity-ui/uikit'; + +export interface TabsAtomItem { + title: string; + content: string; +} + +export interface TabsAtomBlockProps { + items?: TabsAtomItem[]; + size?: 'm' | 'l' | 'xl'; +} + +const TabsAtomBlock: React.FC = ({items = [], size = 'm'}) => { + const [activeTab, setActiveTab] = React.useState(() => items[0]?.title ?? ''); + + React.useEffect(() => { + if (items.length > 0 && !items.find((i) => i.title === activeTab)) { + setActiveTab(items[0].title); + } + }, [items, activeTab]); + + if (items.length === 0) return null; + + return ( +
+ + + {items.map((item) => ( + + {item.title} + + ))} + + {items.map((item) => ( + +
+ {item.content} +
+
+ ))} +
+
+ ); +}; + +export default TabsAtomBlock; diff --git a/playground/src/blocks/TabsAtomBlock/index.ts b/playground/src/blocks/TabsAtomBlock/index.ts new file mode 100644 index 0000000000..a85a17cb21 --- /dev/null +++ b/playground/src/blocks/TabsAtomBlock/index.ts @@ -0,0 +1,55 @@ +import {BlockData} from '../../../../src/constructor-items'; +import {Fields} from '../../../../src/form-generator-v2/types'; + +import TabsAtomBlock from './TabsAtomBlock'; + +const TabsAtomBlockConfig: BlockData = { + type: 'custom/atom-tabs', + component: TabsAtomBlock, + schema: { + name: 'Tabs', + group: 'custom/atoms', + inputs: [ + { + type: 'section', + title: 'Settings', + opened: true, + fields: [ + { + type: 'segmentedRadioGroup', + name: 'size', + title: 'Size', + options: [ + {value: 'm', content: 'M'}, + {value: 'l', content: 'L'}, + {value: 'xl', content: 'XL'}, + ], + defaultValue: 'm', + }, + ], + }, + { + type: 'section', + title: 'Tabs', + withAddButton: true, + index: 'index', + itemTitle: 'Tab {{index}}', + itemView: 'card', + fields: [ + {type: 'textInput', name: 'items[{{index}}].title', title: 'Tab title'}, + {type: 'textArea', name: 'items[{{index}}].content', title: 'Tab content'}, + ], + }, + ] as Fields, + default: { + size: 'm', + items: [ + {title: 'Overview', content: 'This is the overview tab content.'}, + {title: 'Details', content: 'Here you can find more detailed information.'}, + {title: 'Settings', content: 'Configuration options go here.'}, + ], + }, + }, +}; + +export default TabsAtomBlockConfig; diff --git a/playground/src/blocks/TestEditorBlock/TestEditorBlock.tsx b/playground/src/blocks/TestEditorBlock/TestEditorBlock.tsx new file mode 100644 index 0000000000..f91fa0421e --- /dev/null +++ b/playground/src/blocks/TestEditorBlock/TestEditorBlock.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; + +export interface TestEditorBlockProps extends React.PropsWithChildren {} + +export const TestEditorBlock = (props: TestEditorBlockProps) => { + return
{JSON.stringify(props, null, 2)}
; +}; + +export default TestEditorBlock; diff --git a/playground/src/blocks/TestEditorBlock/form.ts b/playground/src/blocks/TestEditorBlock/form.ts new file mode 100644 index 0000000000..730db9d6b9 --- /dev/null +++ b/playground/src/blocks/TestEditorBlock/form.ts @@ -0,0 +1,114 @@ +import { + ArrayObjectInput, + ArrayTextInput, + BooleanInput, + NumberInput, + ObjectInput, + OneOfInput, + SelectMultipleInput, + SelectSingleInput, + TextAreaInput, + TextInput, +} from '../../../../src/form-generator'; + +const textInput: TextInput = { + type: 'text', + name: 'text', + title: 'Text Input', +}; + +const textAreaInput: TextAreaInput = { + type: 'textarea', + name: 'textarea', + title: 'TextArea Input', +}; + +const booleanInput: BooleanInput = { + type: 'boolean', + name: 'boolean', + title: 'Boolean Input', +}; + +const numberInput: NumberInput = { + type: 'number', + name: 'number', + title: 'Number Input', +}; + +const selectInput: SelectSingleInput = { + type: 'select', + name: 'selectSingle', + title: 'Select Single Input', + mode: 'single', + view: 'select', + enum: [ + {value: 'id_1', content: 'Option 1'}, + {value: 'id_2', content: 'Option 2'}, + ], +}; + +const radioButtonsViewSingleInput: SelectSingleInput = { + ...selectInput, + name: 'radioButtons', + title: 'Radio Button Input', + + view: 'radiobutton', +}; + +// @ts-ignore +const selectMultipleModeInput: SelectMultipleInput = { + ...selectInput, + name: 'selectMultiple', + title: 'Select Multiple Input', + + mode: 'multiple', +}; + +const objectInput: ObjectInput = { + type: 'object', + name: 'object', + title: 'Object Input', + properties: [textInput, textAreaInput, selectInput], +}; + +const arrayTextInput: ArrayTextInput = { + type: 'array', + name: 'arrayText', + title: 'Array Text Input', + buttonText: 'Add Array Item', + arrayType: 'text', +}; + +const arrayObjectInput: ArrayObjectInput = { + type: 'array', + name: 'arrayObject', + title: 'Array Object Input', + buttonText: 'Add Array Item', + arrayType: 'object', + properties: [textInput, textAreaInput, selectInput], +}; + +const oneOfInput: OneOfInput = { + type: 'oneOf', + name: 'oneOf', + key: 'oneOfKey', + title: 'Array Text Input', + options: [ + {value: 'text', title: 'Text', properties: [textInput]}, + {value: 'textarea', title: 'TextArea', properties: [textAreaInput]}, + ], +}; + +export default [ + textInput, + textAreaInput, + booleanInput, + numberInput, + selectInput, + radioButtonsViewSingleInput, + selectMultipleModeInput, + objectInput, + arrayTextInput, + arrayObjectInput, + oneOfInput, +]; diff --git a/playground/src/blocks/TestEditorBlock/index.ts b/playground/src/blocks/TestEditorBlock/index.ts new file mode 100644 index 0000000000..646cc2c1fd --- /dev/null +++ b/playground/src/blocks/TestEditorBlock/index.ts @@ -0,0 +1,18 @@ +import TestEditorBlock from './TestEditorBlock'; +import testEditorBlockInputs from './form'; + +const TestEditorBlockConfig = { + type: 'custom/test-editor-block', + component: TestEditorBlock, + schema: { + name: 'Test Editor Block', + group: 'custom', + inputs: testEditorBlockInputs, + }, +}; + +export const TestEditorBlockSchema = { + ['test-editor-block']: {}, +}; + +export default TestEditorBlockConfig; diff --git a/playground/src/blocks/UserBlock/UserBlock.tsx b/playground/src/blocks/UserBlock/UserBlock.tsx new file mode 100644 index 0000000000..3a5591a337 --- /dev/null +++ b/playground/src/blocks/UserBlock/UserBlock.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; + +import {User} from '@gravity-ui/uikit'; + +export interface UserBlockProps { + name?: string; + description?: string; + imgUrl?: string; + size?: '3xs' | '2xs' | 'xs' | 's' | 'm' | 'l' | 'xl'; +} + +const UserBlock: React.FC = ({ + name = 'John Doe', + description, + imgUrl, + size = 'm', +}) => { + const avatar = imgUrl ? {imgUrl} : {text: name}; + + return ( +
+ +
+ ); +}; + +export default UserBlock; diff --git a/playground/src/blocks/UserBlock/index.ts b/playground/src/blocks/UserBlock/index.ts new file mode 100644 index 0000000000..77f76d09c9 --- /dev/null +++ b/playground/src/blocks/UserBlock/index.ts @@ -0,0 +1,47 @@ +import {BlockData} from '../../../../src/constructor-items'; +import {Fields} from '../../../../src/form-generator-v2/types'; + +import UserBlock from './UserBlock'; + +const UserBlockConfig: BlockData = { + type: 'custom/atom-user', + component: UserBlock, + schema: { + name: 'User', + group: 'custom/atoms', + inputs: [ + { + type: 'section', + title: 'User', + opened: true, + fields: [ + {type: 'textInput', name: 'name', title: 'Name'}, + {type: 'textInput', name: 'description', title: 'Description'}, + {type: 'textInput', name: 'imgUrl', title: 'Avatar image URL'}, + { + type: 'segmentedRadioGroup', + name: 'size', + title: 'Size', + options: [ + {value: '3xs', content: '3XS'}, + {value: '2xs', content: '2XS'}, + {value: 'xs', content: 'XS'}, + {value: 's', content: 'S'}, + {value: 'm', content: 'M'}, + {value: 'l', content: 'L'}, + {value: 'xl', content: 'XL'}, + ], + defaultValue: 'm', + }, + ], + }, + ] as Fields, + default: { + name: 'Alice Johnson', + description: 'Frontend Developer', + size: 'm', + }, + }, +}; + +export default UserBlockConfig; diff --git a/playground/src/custom-plugin/index.tsx b/playground/src/custom-plugin/index.tsx new file mode 100644 index 0000000000..aef7257b7a --- /dev/null +++ b/playground/src/custom-plugin/index.tsx @@ -0,0 +1,352 @@ +import * as React from 'react'; + +import { + Bell, + Flag, + Gear, + Globe, + Heart, + House, + Layers, + Magnifier, + Person, + PlanetEarth, + Star, +} from '@gravity-ui/icons'; +import {AsideHeader} from '@gravity-ui/navigation'; +import type {MenuItem, SubheaderMenuItem} from '@gravity-ui/navigation'; +import type {IconData} from '@gravity-ui/uikit'; +import {ThemeProvider} from '@gravity-ui/uikit'; + +import {useContent} from '../../../src'; +import type {PageConstructorWrapperProps} from '../../../src/common/types'; +import type {PageConstructorExtension} from '../../../src/containers/PageConstructor/PageConstructor'; +import {Fields} from '../../../src/form-generator-v2/types'; +import type {BlockWrapperDataProps} from '../../../src/models'; +import {PageContent} from '../../../src/models'; + +const ICON_MAP: Record = { + House, + Gear, + Person, + Bell, + Flag, + Star, + Globe, + Layers, + Magnifier, + Heart, +}; + +const ICON_OPTIONS = Object.keys(ICON_MAP).map((value) => ({value, content: value})); + +const PADDING_MAP: Record = { + '0': 0, + xs: 4, + s: 8, + m: 16, + l: 24, + xl: 32, +}; + +interface PaddingConfig { + all?: string; + vertical?: string; + horizontal?: string; + top?: string; + right?: string; + bottom?: string; + left?: string; +} + +function resolvePadding(p: PaddingConfig | undefined) { + if (!p) return {}; + const get = (side: string, axis: string) => + PADDING_MAP[ + p[side as keyof PaddingConfig] ?? p[axis as keyof PaddingConfig] ?? p.all ?? '' + ] ?? 0; + return { + paddingTop: get('top', 'vertical'), + paddingRight: get('right', 'horizontal'), + paddingBottom: get('bottom', 'vertical'), + paddingLeft: get('left', 'horizontal'), + }; +} + +interface ExperementalBlocksWrapperProps {} + +interface ExperementalBlocksGlobalConfig { + logo?: { + text?: string; + href?: string; + iconSrc?: string; + }; + headerDecoration?: boolean; + hideCollapseButton?: boolean; + collapseTitle?: string; + expandTitle?: string; + menuItems?: { + id: string; + title: string; + link?: string; + type?: string; + icon?: string; + pinned?: boolean; + }[]; + subheaderItems?: { + id: string; + title: string; + icon?: string; + }[]; +} + +export interface ExperementalPageContent extends PageContent, ExperementalBlocksGlobalConfig {} + +export const ExperementalBlocksContentWrapper: React.FC< + ExperementalBlocksWrapperProps & PageConstructorWrapperProps +> = ({children}) => { + const {content} = useContent(); + const { + menuItems, + subheaderItems, + headerDecoration, + hideCollapseButton, + collapseTitle, + expandTitle, + } = content; + + const [compact, setCompact] = React.useState(true); + + const resolvedMenuItems: MenuItem[] = (menuItems ?? []).map((item) => ({ + id: item.id || '', + title: item.title || '', + link: item.link, + icon: item.icon ? ICON_MAP[item.icon] : Flag, + pinned: item.pinned, + type: (item.type as MenuItem['type']) ?? 'regular', + })); + + const resolvedSubheaderItems: SubheaderMenuItem[] = (subheaderItems ?? []).map((item) => ({ + item: { + id: item.id || '', + title: item.title || '', + icon: item.icon ? ICON_MAP[item.icon] : Flag, + }, + })); + + return ( + +
+ +
{children}
} + /> +
+
+
+ ); +}; + +export const ExperementalBlockWrapper: React.FC< + BlockWrapperDataProps & React.PropsWithChildren +> = ({content, children}) => { + const padding = resolvePadding((content as unknown as {padding?: PaddingConfig}).padding); + return
{children}
; +}; + +export const experementalBlocksExtension = ({ + wrapperProps = {}, + globalDefaults = {}, +}: { + wrapperProps?: ExperementalBlocksWrapperProps; + globalDefaults?: ExperementalBlocksGlobalConfig; +} = {}): PageConstructorExtension< + ExperementalBlocksGlobalConfig, + ExperementalBlocksWrapperProps +> => { + return { + name: 'Experemental Blocks Extension', + id: '@Experemental-ui/page-constructor/Experemental-blocks-extension', + settings: { + ContentWrapper: ExperementalBlocksContentWrapper, + contentWrapperProps: wrapperProps, + blockWrapper: ExperementalBlockWrapper, + blockInputs: [ + { + type: 'section', + title: 'Padding', + fields: [ + { + type: 'segmentedRadioGroup', + name: '_paddingMode', + title: 'Mode', + options: [ + {value: 'all', content: 'All'}, + {value: 'axes', content: 'Axes'}, + {value: 'individual', content: 'Individual'}, + ], + }, + { + type: 'select', + name: 'padding.all', + title: 'All sides', + hasClear: true, + options: ['0', 'xs', 's', 'm', 'l', 'xl'].map((v) => ({ + value: v, + content: v.toUpperCase(), + })), + when: [{field: '_paddingMode', operator: '===', value: 'all'}], + }, + { + type: 'select', + name: 'padding.vertical', + title: 'Vertical (top / bottom)', + hasClear: true, + options: ['0', 'xs', 's', 'm', 'l', 'xl'].map((v) => ({ + value: v, + content: v.toUpperCase(), + })), + when: [{field: '_paddingMode', operator: '===', value: 'axes'}], + }, + { + type: 'select', + name: 'padding.horizontal', + title: 'Horizontal (left / right)', + hasClear: true, + options: ['0', 'xs', 's', 'm', 'l', 'xl'].map((v) => ({ + value: v, + content: v.toUpperCase(), + })), + when: [{field: '_paddingMode', operator: '===', value: 'axes'}], + }, + { + type: 'select', + name: 'padding.top', + title: 'Top', + hasClear: true, + options: ['0', 'xs', 's', 'm', 'l', 'xl'].map((v) => ({ + value: v, + content: v.toUpperCase(), + })), + when: [{field: '_paddingMode', operator: '===', value: 'individual'}], + }, + { + type: 'select', + name: 'padding.right', + title: 'Right', + hasClear: true, + options: ['0', 'xs', 's', 'm', 'l', 'xl'].map((v) => ({ + value: v, + content: v.toUpperCase(), + })), + when: [{field: '_paddingMode', operator: '===', value: 'individual'}], + }, + { + type: 'select', + name: 'padding.bottom', + title: 'Bottom', + hasClear: true, + options: ['0', 'xs', 's', 'm', 'l', 'xl'].map((v) => ({ + value: v, + content: v.toUpperCase(), + })), + when: [{field: '_paddingMode', operator: '===', value: 'individual'}], + }, + { + type: 'select', + name: 'padding.left', + title: 'Left', + hasClear: true, + options: ['0', 'xs', 's', 'm', 'l', 'xl'].map((v) => ({ + value: v, + content: v.toUpperCase(), + })), + when: [{field: '_paddingMode', operator: '===', value: 'individual'}], + }, + ], + }, + ] as Fields, + globalInputs: [ + { + type: 'section', + title: 'Header Settings', + fields: [ + {type: 'switch', name: 'headerDecoration', title: 'Header decoration'}, + {type: 'switch', name: 'hideCollapseButton', title: 'Hide collapse button'}, + { + type: 'textInput', + name: 'collapseTitle', + title: 'Collapse button tooltip', + }, + {type: 'textInput', name: 'expandTitle', title: 'Expand button tooltip'}, + ], + }, + { + type: 'section', + title: 'Menu Items', + withAddButton: true, + index: 'index', + itemTitle: 'Item {{index}}', + itemView: 'card', + fields: [ + {type: 'textInput', name: 'menuItems[{{index}}].id', title: 'ID'}, + {type: 'textInput', name: 'menuItems[{{index}}].title', title: 'Title'}, + {type: 'textInput', name: 'menuItems[{{index}}].link', title: 'Link URL'}, + { + type: 'select', + name: 'menuItems[{{index}}].type', + title: 'Type', + hasClear: true, + options: [ + {value: 'regular', content: 'Regular'}, + {value: 'action', content: 'Action'}, + {value: 'divider', content: 'Divider'}, + ], + }, + { + type: 'select', + name: 'menuItems[{{index}}].icon', + title: 'Icon', + hasClear: true, + options: ICON_OPTIONS, + }, + {type: 'switch', name: 'menuItems[{{index}}].pinned', title: 'Pinned'}, + ], + }, + { + type: 'section', + title: 'Subheader Items', + withAddButton: true, + index: 'index2', + itemTitle: 'Subheader Item {{index2}}', + itemView: 'card', + fields: [ + {type: 'textInput', name: 'subheaderItems[{{index2}}].id', title: 'ID'}, + { + type: 'textInput', + name: 'subheaderItems[{{index2}}].title', + title: 'Title', + }, + { + type: 'select', + name: 'subheaderItems[{{index2}}].icon', + title: 'Icon', + hasClear: true, + options: ICON_OPTIONS, + }, + ], + }, + ] as Fields, + globalDefaults, + }, + }; +}; diff --git a/playground/src/main.tsx b/playground/src/main.tsx new file mode 100644 index 0000000000..54fce1af87 --- /dev/null +++ b/playground/src/main.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; + +import ReactDOM from 'react-dom/client'; +import {BrowserRouter} from 'react-router'; + +import Router from './router'; + +import './styles/globals.scss'; +import '@gravity-ui/uikit/styles/fonts.css'; +import '@gravity-ui/uikit/styles/styles.css'; + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); +root.render( + + + + + , +); diff --git a/playground/src/pages/editor/editor.scss b/playground/src/pages/editor/editor.scss new file mode 100644 index 0000000000..69b14306be --- /dev/null +++ b/playground/src/pages/editor/editor.scss @@ -0,0 +1,13 @@ +$block: '.editor'; + +#{$block} { + height: 100vh; + + &__other { + padding: 16px; + display: flex; + flex-direction: column; + gap: 8px; + align-items: flex-start; + } +} diff --git a/playground/src/pages/editor/editor.tsx b/playground/src/pages/editor/editor.tsx new file mode 100644 index 0000000000..68122013e4 --- /dev/null +++ b/playground/src/pages/editor/editor.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; + +import {Button, Menu, Popup, ThemeProvider} from '@gravity-ui/uikit'; +import block from 'bem-cn-lite'; + +import {EditorProvider, EditorView, usePCEditorSettings} from '../../../../src/editor-v2'; + +import './editor.scss'; + +const b = block('editor'); + +interface NavItem { + label: string; + url: string; +} + +const NAV_ITEMS: NavItem[] = [ + {label: 'Gravity Blocks — page 1', url: import.meta.env.BASE_URL + '?page=gravity-blocks&id=1'}, + {label: 'Gravity Blocks — page 2', url: import.meta.env.BASE_URL + '?page=gravity-blocks&id=2'}, + {label: 'Experimental page', url: import.meta.env.BASE_URL + '?page=experemental'}, +]; + +const NavigateToButton = () => { + const [buttonElement, setButtonElement] = React.useState(null); + const [open, setOpen] = React.useState(false); + const {changeUrl} = usePCEditorSettings(); + + return ( +
+ + + + {NAV_ITEMS.map((item) => ( + { + changeUrl(item.url); + setOpen(false); + }} + > + {item.label} + + ))} + + +
+ ); +}; + +export default function EditorPage() { + const initialUrl = import.meta.env.BASE_URL + '?page=gravity-blocks&id=1'; + + return ( + +
+ + + +
+
+ ); +} diff --git a/playground/src/pages/experemental/example-1/content.json b/playground/src/pages/experemental/example-1/content.json new file mode 100644 index 0000000000..1ae35cb5f4 --- /dev/null +++ b/playground/src/pages/experemental/example-1/content.json @@ -0,0 +1,198 @@ +{ + "logo": {"text": "My App"}, + "menuItems": [ + {"id": "home", "title": "Home", "icon": "House", "link": "/"}, + {"id": "settings", "title": "Settings", "icon": "Gear", "link": "/settings"}, + {"id": "profile", "title": "Profile", "icon": "Person", "link": "/profile"} + ], + "blocks": [ + { + "type": "custom/container-section", + "title": "Atoms", + "description": "Basic Gravity UI components wrapped as page-constructor blocks.", + "background": "subtle", + "withDivider": true + }, + { + "type": "custom/atom-alert", + "title": "Deployment complete", + "message": "All services are running normally. No action required.", + "theme": "success", + "view": "outlined", + "corners": "square" + }, + { + "type": "custom/container-columns", + "columns": 3, + "gap": "m", + "children": [ + { + "type": "custom/atom-button", + "text": "Get started", + "view": "action", + "size": "l" + }, + { + "type": "custom/atom-button", + "text": "Learn more", + "view": "outlined", + "size": "l" + }, + { + "type": "custom/atom-button", + "text": "Documentation", + "view": "flat", + "size": "l", + "href": "https://gravity-ui.com", + "target": "_blank" + } + ] + }, + { + "type": "custom/container-columns", + "columns": 4, + "gap": "m", + "children": [ + {"type": "custom/atom-label", "value": "Production", "theme": "success", "size": "m"}, + {"type": "custom/atom-label", "value": "Staging", "theme": "warning", "size": "m"}, + {"type": "custom/atom-label", "value": "Dev", "theme": "info", "size": "m"}, + {"type": "custom/atom-label", "value": "Deprecated", "theme": "danger", "size": "m"} + ] + }, + { + "type": "custom/atom-progress", + "value": 72, + "text": "72% complete", + "theme": "success", + "size": "l" + }, + { + "type": "custom/atom-breadcrumbs", + "items": [ + {"text": "Home", "href": "/"}, + {"text": "Products", "href": "/products"}, + {"text": "Page Constructor"} + ] + }, + { + "type": "custom/atom-tabs", + "size": "m", + "items": [ + { + "title": "Overview", + "content": "The page-constructor library allows you to build rich pages from composable blocks. Each block is a self-contained UI unit with its own schema and default values." + }, + { + "title": "Installation", + "content": "Install via npm: npm install @gravity-ui/page-constructor. Then import PageConstructor and PageConstructorProvider from the package." + }, + { + "title": "Usage", + "content": "Wrap your page with PageConstructorProvider, passing your custom blocks. Then render PageConstructor with a content object that describes your page structure." + } + ] + }, + { + "type": "custom/atom-accordion", + "size": "m", + "items": [ + { + "summary": "What is Page Constructor?", + "content": "Page Constructor is a library for building content pages from a JSON configuration. It provides a visual editor and a set of pre-built blocks." + }, + { + "summary": "Can I create custom blocks?", + "content": "Yes! Pass your custom block configs to PageConstructorProvider via the blocks prop. Each block needs a type, component, and schema." + }, + { + "summary": "What form fields are available?", + "content": "You can use textInput, textArea, select, segmentedRadioGroup, switch, colorInput, and section (with nesting and repeating group support)." + } + ] + }, + { + "type": "custom/container-columns", + "columns": 3, + "gap": "l", + "children": [ + { + "type": "custom/atom-avatar", + "text": "AJ", + "size": "xl", + "theme": "brand", + "view": "filled" + }, + { + "type": "custom/atom-avatar", + "text": "MK", + "size": "xl", + "theme": "normal", + "view": "outlined" + }, + { + "type": "custom/atom-avatar", + "text": "RP", + "size": "xl", + "theme": "brand", + "view": "outlined" + } + ] + }, + { + "type": "custom/container-columns", + "columns": 2, + "gap": "m", + "children": [ + { + "type": "custom/atom-user", + "name": "Alice Johnson", + "description": "Frontend Developer", + "size": "m" + }, + { + "type": "custom/atom-user", + "name": "Bob Smith", + "description": "Backend Engineer", + "size": "m" + } + ] + }, + { + "type": "custom/container-section", + "title": "Containers", + "description": "Layout containers that accept any page-constructor blocks as children.", + "background": "subtle", + "withDivider": true + }, + { + "type": "custom/container-card", + "title": "Feature Card", + "description": "Cards group related content with optional border, shadow and theme.", + "theme": "info", + "view": "outlined", + "size": "l", + "children": [ + { + "type": "custom/atom-alert", + "title": "Tip", + "message": "You can nest any block inside a card container.", + "theme": "info", + "view": "outlined", + "corners": "square" + } + ] + }, + { + "type": "custom/definition-list-block", + "title": "System Specifications", + "direction": "horizontal", + "responsive": false, + "items": [ + {"name": "CPU", "value": "Intel Core i9-13900K", "copyText": "Intel Core i9-13900K"}, + {"name": "RAM", "value": "64GB DDR5-5600", "note": "Maximum supported memory"}, + {"name": "Storage", "value": "2TB NVMe SSD"}, + {"name": "GPU", "value": "NVIDIA RTX 4090 24GB", "copyText": "NVIDIA RTX 4090 24GB"} + ] + } + ] +} diff --git a/playground/src/pages/experemental/example-2/content.json b/playground/src/pages/experemental/example-2/content.json new file mode 100644 index 0000000000..568c1b8d2d --- /dev/null +++ b/playground/src/pages/experemental/example-2/content.json @@ -0,0 +1,402 @@ +{ + "meta": { + "title": "YDB — распределённая SQL база данных с открытым исходным кодом", + "description": "YDB — это открытая распределённая SQL база данных, которая сочетает высокую доступность и масштабируемость со строгой согласованностью и ACID транзакциями.", + "sharing": { + "image": "https://storage.yandexcloud.net/ydb-site-assets/share-ydb-eng.png" + } + }, + "blocks": [ + { + "type": "header-block", + "title": "YDB", + "description": "YDB — это распределённая отказоустойчивая Distributed SQL база данных с открытым исходным кодом, которая сочетает в себе высокую доступность и масштабируемость со строгой согласованностью и транзакциями ACID. Она поддерживает одновременное выполнение транзакционных (OLTP), аналитических (OLAP) и потоковых нагрузок.", + "width": "s", + "verticalOffset": "m", + "imageSize": "m", + "background": { + "image": { + "src": "https://storage.yandexcloud.net/ydb-site-assets/cover.png", + "disableCompress": true, + "alt": "Фон YDB" + }, + "color": "#e2eaff", + "fullWidth": false + }, + "buttons": [ + { + "text": "Быстрый старт", + "theme": "accent", + "url": "/../docs/ru/quickstart" + }, + { + "text": "Документация", + "theme": "outlined", + "url": "/../docs/ru/" + } + ] + }, + { + "type": "card-layout-block", + "title": "Что я могу делать с YDB?", + "animated": false, + "colSizes": { + "all": 12, + "sm": 6, + "md": 4 + }, + "anchor": { + "url": "whatcanido", + "text": "Что я могу делать с YDB?" + }, + "description": "", + "children": [ + { + "type": "background-card", + "backgroundColor": "#CCE7FF", + "title": "Транзакционные нагрузки (OLTP)", + "text": "Вы можете использовать YDB для хранения состояния вашего приложения, независимо от объема данных или частоты их изменения. Обрабатывать петабайты при миллионах транзакций в секунду — не проблема.", + "controlPosition": "footer", + "buttons": [ + { + "text": "Узнать больше", + "theme": "outlined", + "url": "/docs/ru/concepts/" + } + ] + }, + { + "type": "background-card", + "backgroundColor": "rgba(107,132,153,.12)", + "title": "Аналитические нагрузки (OLAP)", + "text": "Вы можете создавать аналитические отчёты на основе хранимых в YDB данных с производительностью, сопоставимой со специализированными аналитическими СУБД. При этом никаких компромиссов по согласованности и доступности не потребуется.", + "controlPosition": "footer", + "buttons": [ + { + "text": "Узнать больше", + "theme": "outlined", + "url": "/docs/ru/concepts/datamodel/table#column-oriented-tables" + } + ] + }, + { + "type": "background-card", + "backgroundColor": "rgba(107,132,153,.12)", + "title": "Потоковые нагрузки", + "text": "Вы можете использовать функциональность YDB-топиков для надёжной отправки данных между вашими приложениями или отслеживания изменений в таблицах YDB. Можно выбрать как семантику доставки сообщений ровно один раз (exactly once), так и не менее одного раза (at least once).", + "controlPosition": "footer", + "buttons": [ + { + "text": "Узнать больше", + "theme": "outlined", + "url": "/docs/ru/concepts/topic" + } + ] + } + ] + }, + { + "type": "extended-features-block", + "title": "Почему YDB?", + "animated": false, + "colSizes": { + "all": 12, + "sm": 6, + "md": 4 + }, + "items": [ + { + "title": "Эластичность и масштабируемость", + "text": "Добавляйте или удаляйте узлы на лету, чтобы легко масштабировать кластер по мере необходимости. YDB имеет отдельные слои вычисления и хранения, что позволяет независимо добавлять дисковую ёмкость или вычислительные ресурсы в зависимости от того, чего не хватает при текущей нагрузке.", + "icon": "https://storage.yandexcloud.net/ydb-site-assets/features-icons/icon_01.svg" + }, + { + "title": "Отказоустойчивость", + "text": "YDB спроектирована для работы в трёх зонах доступности и обеспечивает работоспособность даже в случае выхода из строя одной из них. Она автоматически восстанавливается после сбоя диска, сервера или датацентра с минимальной задержкой для приложений.", + "icon": "https://storage.yandexcloud.net/ydb-site-assets/features-icons/icon_03.svg" + }, + { + "title": "Простота в использовании", + "text": "Работа с кластером YDB ощущается как работа с одноузловой СУБД с безграничными ресурсами благодаря строгой согласованности, ACID-транзакциям, высокопроизводительным запросам, возможности загрузки больших объёмов данных, а также поддержке знакомого диалекта SQL и JSON API.", + "icon": "https://storage.yandexcloud.net/ydb-site-assets/features-icons/icon_02.svg" + }, + { + "title": "Универсальность", + "text": "Благодаря поддержке различных видов нагрузок в одной системе YDB может заменить несколько систем хранения и обработки данных или всю корпоративную экосистему данных в компании.", + "icon": "https://storage.yandexcloud.net/ydb-site-assets/features-icons/icon_06.svg" + }, + { + "title": "Открытый исходный код", + "text": "[Исходный код YDB](https://github.com/ydb-platform/ydb) опубликован под лицензией Apache 2.0, накладывающей минимум ограничений на использование. Таким образом, нет рисков, связанных с привязкой к конкретному поставщику или провайдеру облачных услуг.", + "icon": "https://storage.yandexcloud.net/ydb-site-assets/features-icons/icon_05.svg" + }, + { + "title": "Совместимость с любым окружением", + "text": "YDB можно развернуть в [Kubernetes](https://github.com/ydb-platform/ydb-kubernetes-operator), в любом облачном окружении или в корпоративных ЦОД. Либо можно использовать YDB как [управляемый сервис в Yandex Cloud](https://cloud.yandex.ru/services/ydb). Также возможны локальные эксперименты на любом компьютере.", + "icon": "https://storage.yandexcloud.net/ydb-site-assets/features-icons/icon_04.svg" + } + ] + }, + { + "type": "banner-block", + "animated": false, + "title": "Оценка производительности PostgreSQL vs Distributed DBMS", + "subtitle": "TPC-C является наиболее известным набором тестов производительности для OLTP. Мы подготовили исследование производительности, сравнивающее PostgreSQL, YDB и CockroachDB в отказоустойчивых конфигурациях.", + "image": "https://storage.yandexcloud.net/ydb-site-assets/banner-block-pg.png", + "color": "#CCE7FF", + "button": { + "text": "Читать дальше", + "theme": "raised", + "url": "https://habr.com/ru/companies/ydb/articles/801587/" + } + }, + { + "type": "card-layout-block", + "title": "Кто использует YDB?", + "animated": false, + "colSizes": { + "all": 12, + "sm": 6, + "md": 4 + }, + "anchor": { + "url": "whouses", + "text": "Кто использует YDB?" + }, + "description": "", + "children": [ + { + "type": "layout-item", + "media": { + "image": { + "src": "https://storage.yandexcloud.net/ydb-site-assets/clients/metrika.png", + "disableCompress": true + } + }, + "content": { + "title": "Метрика", + "text": "[Метрика](https://metrika.yandex.ru/) - одна из крупнейших в мире платформ мобильной и веб-аналитики. Она полагается на YDB для создания пользовательских сессий на лету.\n\nПереход на YDB позволил Метрике расширить объём хранимых данных и бесконечно наращивать обрабатываемую нагрузку. Теперь одна из баз данных Метрики в YDB содержит более 400 ТБ данных и выдерживает нагрузку более 1 000 000 RPS." + }, + "fullScreen": false, + "border": true + }, + { + "type": "layout-item", + "media": { + "image": { + "src": "https://storage.yandexcloud.net/ydb-site-assets/clients/yandex-cloud-new.png", + "disableCompress": true + } + }, + "content": { + "title": "Yandex Cloud", + "text": "YDB отвечает за слой хранения для сетевых дисков [Yandex Cloud](https://cloud.yandex.ru), используется в качестве СУБД для хранения данных и метаданных облачной инфраструктуры и сервисов платформы, а также в качестве базы данных для облачного Control Plane.\n\nИнфраструктурным и платформенным сервисам Yandex Cloud необходима высокая доступность и масштабируемость, поэтому платформа выбрала YDB в качестве ключевого компонента." + }, + "fullScreen": false, + "border": true + }, + { + "type": "layout-item", + "media": { + "image": { + "src": "https://storage.yandexcloud.net/ydb-site-assets/clients/praktikum.jpg", + "disableCompress": true + } + }, + "content": { + "title": "Практикум", + "text": "[Практикум](https://practicum.yandex.ru/) - это онлайн-ориентированная образовательная платформа. Она использует YDB в качестве гибкого хранилища состояний для своих микросервисов.\n\nНативная поддержка многоарендности в YDB позволяет им избежать создания и управления выделенными базами данных для каждого компонента сервиса." + }, + "fullScreen": false, + "border": true + }, + { + "type": "layout-item", + "media": { + "image": { + "src": "https://storage.yandexcloud.net/ydb-site-assets/clients/market.png", + "disableCompress": true + } + }, + "content": { + "title": "Яндекс Маркет", + "text": "[Яндекс Маркет](https://market.yandex.ru) - один из крупнейших сервисов электронной коммерции в СНГ. Многие ключевые функции сервиса, такие как корзина, скидки, и оформление заказа, используют YDB для хранения своего состояния.\n\nВыбор YDB в качестве базы данных позволил Яндекс Маркету выдержать стократное увеличение нагрузки на корзину при соблюдении строгих гарантий времени отклика." + }, + "fullScreen": false, + "border": true + }, + { + "type": "layout-item", + "media": { + "image": { + "src": "https://storage.yandexcloud.net/ydb-site-assets/clients/jaeger.png", + "disableCompress": true + } + }, + "content": { + "title": "Auto.ru", + "text": "[Auto.ru](https://auto.ru) снизила потребление CPU для трассировочной базы данных [Jaeger](https://www.jaegertracing.io/) в три раза после перехода на YDB, что позволило записывать 500 000 трассировок в секунду без семплирования.\n\nУспешная реализация YDB в качестве хранилища трассировок доказала применимость и ключевые свойства, такие как масштабируемость, отказоустойчивость и строгая согласованность. В результате Auto.ru выбрала YDB в качестве реляционной базы данных для некоторых своих микросервисов." + }, + "fullScreen": false, + "border": true + }, + { + "type": "layout-item", + "media": { + "image": { + "src": "https://storage.yandexcloud.net/ydb-site-assets/clients/alice-new-speakers.png", + "disableCompress": "true\"" + } + }, + "content": { + "title": "Алиса", + "text": "[Алиса](https://yandex.ru/alice) - это голосовой помощник и экосистема умного дома. После перехода на YDB команда Алисы решила проблемы синхронизации между дата‑центрами, простоев при переключении основных серверов, снизила нагрузку на команду DevOps, увеличила объём хранимых данных до сотен терабайт и нагрузку до сотен тысяч запросов в секунду.\n\nПереход на YDB позволил отказаться от ручного шардинга данных и добиться строгой согласованности в кросс-датацентровом кластере. Сейчас команда Алисы использует YDB как реляционную базу данных и как базу для хранения логов и трейсов." + }, + "fullScreen": false, + "border": true + } + ] + }, + { + "type": "slider-block", + "animated": false, + "anchor": { + "url": "scenarios", + "text": "scenarios" + }, + "title": { + "text": "В каких типовых сценариях стоит использовать YDB?", + "textSize": "m" + }, + "children": [ + { + "type": "basic-card", + "title": "Работа с внезапным ростом нагрузки", + "text": "Эластичность YDB позволяет быстро изменять количество ресурсов, выделенных базе данных, чтобы обеспечить необходимую пропускную способность в соответствии с нагрузкой. Вы можете легко увеличить или уменьшить количество вычислительных ресурсов в зависимости от предстоящих нагрузок, например, на «Чёрную пятницу» или для маркетинговых кампаний." + }, + { + "type": "basic-card", + "title": "Хранение Observability данных", + "text": "[Колоночные таблицы YDB](/docs/ru/concepts/column-table) отлично подходят для хранения логов и метрик с простым доступом к ним через SQL интерфейс. Также, низкое потребление вычислительных ресурсов и масштабируемость YDB делают запись [Jaeger трейсов](https://www.jaegertracing.io/) выгодной по себестоимости и лёгкой в использовании." + }, + { + "type": "basic-card", + "title": "Документоориентированная СУБД", + "text": "Не смотря на строгую систему типов данных YDB, поддержка [типа данных JSON и связанных с ним функций](/docs/ru/yql/reference/builtins/json), расширяет возможности YDB в роли системы хранения неструктурированных документов." + }, + { + "type": "basic-card", + "title": "Кеш с SQL-интерфейсом", + "text": "Быстрый отклик и масштабируемость пропускной способности позволяют использовать YDB одновременно в качестве онлайн-базы данных и предварительно посчитанного кеша. Возможности SQL значительно повышают удобство использования и позволяют проводить оперативную аналитику данных в кеше. Например, сайты туроператоров и агрегаторов путешествий могут использовать базу данных для кеширования результатов поиска авиабилетов или туров, а также для пересчёта цен и проверки сезонной доступности." + }, + { + "type": "basic-card", + "title": "Централизованная система учёта запасов", + "text": "YDB обеспечивает строгую целостность транзакций. Это позволяет предоставлять консистентные данные о запасах на всех складах и торговых объектах, и делает YDB подходящим решением для электронной коммерции, приложений складской или транспортной логистики." + }, + { + "type": "basic-card", + "title": "Система хранения данных для Internet of Things (IoT) экосистемы", + "text": "Поддержка автоматического шардирования YDB позволяет обрабатывать потоки данных от большого количества устройств — профиль нагрузки, который встречается в проектах интернета вещей." + } + ] + }, + { + "type": "media-block", + "animated": false, + "direction": "content-media", + "title": "Можете вкратце объяснить, что такое YDB, в видеоформате?", + "description": "Конечно, смотрите →", + "anchor": { + "url": "video", + "text": "Video" + }, + "largeMedia": true, + "media": { + "youtube": "https://youtu.be/QVmhR__wAJg" + } + }, + { + "type": "banner-block", + "animated": false, + "title": "Учебный курс по YDB от участника сообщества пользователей", + "subtitle": "_Сторонняя разработка - Contribution_\n\nНачальный [учебный курс по YDB](https://stepik.org/course/186264/promo), разработанный одним из наших пользователей - Владом Бурмистровым, позволяет ознакомиться с ключевыми свойствами, возможностями и архитектурой YDB, а также научиться основным приемам по работе с YDB.", + "color": "#CCE7FF", + "image": "https://storage.yandexcloud.net/ydb-site-assets/careers/img_4.png", + "button": { + "text": "Описание и программа курса", + "theme": "raised", + "url": "https://stepik.org/course/186264/promo" + } + }, + { + "type": "tabs-block", + "title": { + "text": "Как начать?" + }, + "anchor": { + "url": "howtostart", + "text": "Как начать?" + }, + "items": [ + { + "tabName": "Docker", + "title": "Docker", + "text": " Создайте рабочий каталог и запустите локальный контейнер YDB из этого каталога:\n```bash mkdir ydb-local && cd ydb-local \ndocker run -d --rm --name ydb-local -h localhost \\\n--platform linux/amd64 \\\n-p 2135:2135 -p 2136:2136 -p 8765:8765 \\\n-v $(pwd)/ydb_certs:/ydb_certs -v $(pwd)/ydb_data:/ydb_data \\\n-e GRPC_TLS_PORT=2135 -e GRPC_PORT=2136 -e MON_PORT=8765 \\\nydbplatform/local-ydb:latest\n```\nПерейдите в раздел [«Быстрый старт»](/docs/ru/quickstart) в документации YDB, чтобы получить дополнительную информацию. " + }, + { + "tabName": "Minikube", + "title": "Minikube", + "text": " Установите Kubernetes CLI [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) и менеджер пакетов [Helm 3](https://helm.sh/docs/intro/install/).\nУстановите и запустите [Minikube](https://kubernetes.io/ru/docs/tasks/tools/install-minikube/).\nСклонируйте репозиторий с [YDB Kubernetes Operator](https://github.com/ydb-platform/ydb-kubernetes-operator).\n```\ngit clone https://github.com/ydb-platform/ydb-kubernetes-operator && cd ydb-kubernetes-operator\n```\nУстановите контроллер YDB на кластер.\n```\nhelm upgrade --install ydb-operator deploy/ydb-operator --set metrics.enabled=false\n```\nПримените манифест для создания кластера YDB.\n```\nkubectl apply -f samples/minikube/storage.yaml\n```\nДождитесь, пока `kubectl get storages.ydb.tech` не станет `Ready`.\nПримените манифест для создания базы данных.\n```\nkubectl apply -f samples/minikube/database.yaml\n```\nДождитесь, пока `kubectl get databases.ydb.tech` не станет `Ready`.\n\nПосле обработки манифеста будет создан объект StatefulSet, описывающий набор динамических узлов. Созданная база данных будет доступна изнутри кластера Kubernetes по DNS имени `database-minikube-sample` на порту 2135.\n\nПерейдите в раздел [«Быстрый старт»](/docs/ru/quickstart) в документации YDB, чтобы получить дополнительную информацию. " + }, + { + "tabName": "Установка вручную", + "title": "Установка вручную (только для Linux x86_64)", + "text": " Создайте рабочий каталог, запустите скрипт установки из него, а затем запустите одноузловую базу данных YDB с включенным режимом работы в оперативной памяти:\n``` mkdir ydb-local && cd ydb-local\ncurl https://install.ydb.tech | bash\n./start.sh ram\n```\nПерейдите в раздел [«Быстрый старт»](/docs/ru/quickstart) в документации YDB, чтобы получить дополнительную информацию. " + } + ] + }, + { + "type": "icons-block", + "size": "s", + "title": "Как оставаться на связи?", + "items": [ + { + "url": "https://github.com/ydb-platform/ydb", + "text": "GitHub", + "src": "https://storage.yandexcloud.net/ydb-site-assets/community/git.svg" + }, + { + "url": "https://t.me/ydb_ru", + "text": "Telegram", + "src": "https://storage.yandexcloud.net/ydb-site-assets/community/telegram.svg" + }, + { + "url": "https://habr.com/ru/companies/ydb/articles/", + "text": "Habr", + "src": "https://storage.yandexcloud.net/ydb-site-assets/community/habr.svg" + }, + { + "url": "https://blog.ydb.tech", + "text": "Medium", + "src": "https://storage.yandexcloud.net/ydb-site-assets/community/medium.svg" + }, + { + "url": "https://twitter.com/YDBPlatform", + "text": "Twitter", + "src": "https://storage.yandexcloud.net/ydb-site-assets/community/twitter.svg" + }, + { + "url": "https://www.linkedin.com/company/ydb-platform/", + "text": "LinkedIn", + "src": "https://storage.yandexcloud.net/ydb-site-assets/community/linkedin.svg" + }, + { + "url": "https://www.youtube.com/c/YDBPlatform", + "text": "YouTube", + "src": "https://storage.yandexcloud.net/ydb-site-assets/community/youtube.svg" + } + ] + } + ] +} diff --git a/playground/src/pages/experemental/example-2/navigation.json b/playground/src/pages/experemental/example-2/navigation.json new file mode 100644 index 0000000000..8df0e9cccd --- /dev/null +++ b/playground/src/pages/experemental/example-2/navigation.json @@ -0,0 +1,48 @@ +{ + "logo": { + "text": "", + "icon": "https://storage.yandexcloud.net/ydb-site-assets/ydb_icon.svg" + }, + "header": { + "leftItems": [ + { + "type": "link", + "text": "Документация", + "url": "/../docs/ru", + "target": "_self" + }, + { + "type": "link", + "text": "Команда", + "url": "/careers/" + }, + { + "type": "link", + "text": "Студентам", + "url": "/students/" + }, + { + "type": "link", + "text": "Поддержка", + "url": "/support/" + }, + { + "type": "link", + "text": "Блог", + "url": "/blog/" + } + ], + "rightItems": [ + { + "type": "github-button", + "text": "Star", + "label": "Star ydb-platform/ydb on GitHub", + "url": "https://github.com/ydb-platform/ydb" + } + ] + }, + "meta": { + "title": "", + "description": "" + } +} diff --git a/playground/src/pages/experemental/example-2/styles.scss b/playground/src/pages/experemental/example-2/styles.scss new file mode 100644 index 0000000000..b44174a144 --- /dev/null +++ b/playground/src/pages/experemental/example-2/styles.scss @@ -0,0 +1,118 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap'); + +.g-root_theme_light { + --g-border-radius-xs: 0px; + --g-border-radius-s: 0px; + --g-border-radius-m: 0px; + --g-border-radius-l: 0px; + --g-border-radius-xl: 0px; + + --g-font-family-sans: 'Inter', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --g-font-family-monospace: + 'Roboto Mono', 'Menlo', 'Monaco', 'Consolas', 'Ubuntu Mono', 'Liberation Mono', + 'DejaVu Sans Mono', 'Courier New', 'Courier', monospace; + + --g-color-private-brand-50: rgba(203, 255, 92, 0.1); + --g-color-private-brand-100: rgba(203, 255, 92, 0.15); + --g-color-private-brand-150: rgba(203, 255, 92, 0.2); + --g-color-private-brand-200: rgba(203, 255, 92, 0.3); + --g-color-private-brand-250: rgba(203, 255, 92, 0.4); + --g-color-private-brand-300: rgba(203, 255, 92, 0.5); + --g-color-private-brand-350: rgba(203, 255, 92, 0.6); + --g-color-private-brand-400: rgba(203, 255, 92, 0.7); + --g-color-private-brand-450: rgba(203, 255, 92, 0.8); + --g-color-private-brand-500: rgba(203, 255, 92, 0.9); + --g-color-private-brand-550-solid: rgb(203, 255, 92); + --g-color-private-brand-1000-solid: rgb(59, 63, 43); + --g-color-private-brand-950-solid: rgb(68, 74, 46); + --g-color-private-brand-900-solid: rgb(85, 97, 51); + --g-color-private-brand-850-solid: rgb(102, 119, 57); + --g-color-private-brand-800-solid: rgb(119, 142, 63); + --g-color-private-brand-750-solid: rgb(135, 165, 69); + --g-color-private-brand-700-solid: rgb(152, 187, 75); + --g-color-private-brand-650-solid: rgb(169, 210, 80); + --g-color-private-brand-600-solid: rgb(186, 232, 86); + --g-color-private-brand-500-solid: rgb(208, 255, 108); + --g-color-private-brand-450-solid: rgb(213, 255, 125); + --g-color-private-brand-400-solid: rgb(219, 255, 141); + --g-color-private-brand-350-solid: rgb(224, 255, 157); + --g-color-private-brand-300-solid: rgb(229, 255, 174); + --g-color-private-brand-250-solid: rgb(234, 255, 190); + --g-color-private-brand-200-solid: rgb(239, 255, 206); + --g-color-private-brand-150-solid: rgb(245, 255, 222); + --g-color-private-brand-100-solid: rgb(247, 255, 231); + --g-color-private-brand-50-solid: rgb(250, 255, 239); + + --g-color-base-brand: rgb(203, 255, 92); + --g-color-base-background: rgb(255, 255, 255); + --g-color-base-brand-hover: var(--g-color-private-brand-600-solid); + --g-color-base-selection: var(--g-color-private-brand-200); + --g-color-base-selection-hover: var(--g-color-private-brand-300); + --g-color-line-brand: var(--g-color-private-brand-600-solid); + --g-color-text-brand: var(--g-color-private-brand-700-solid); + --g-color-text-brand-heavy: var(--g-color-private-brand-700-solid); + --g-color-text-brand-contrast: rgba(0, 0, 0, 0.85); + --g-color-text-link: var(--g-color-private-brand-600-solid); + --g-color-text-link-hover: var(--g-color-private-brand-800-solid); + --g-color-text-link-visited: var(--g-color-private-purple-550-solid); + --g-color-text-link-visited-hover: var(--g-color-private-purple-800-solid); +} + +.g-root_theme_dark { + --g-border-radius-xs: 0px; + --g-border-radius-s: 0px; + --g-border-radius-m: 0px; + --g-border-radius-l: 0px; + --g-border-radius-xl: 0px; + + --g-font-family-sans: 'Inter', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --g-font-family-monospace: + 'Roboto Mono', 'Menlo', 'Monaco', 'Consolas', 'Ubuntu Mono', 'Liberation Mono', + 'DejaVu Sans Mono', 'Courier New', 'Courier', monospace; + + --g-color-private-brand-50: rgba(203, 255, 92, 0.1); + --g-color-private-brand-100: rgba(203, 255, 92, 0.15); + --g-color-private-brand-150: rgba(203, 255, 92, 0.2); + --g-color-private-brand-200: rgba(203, 255, 92, 0.3); + --g-color-private-brand-250: rgba(203, 255, 92, 0.4); + --g-color-private-brand-300: rgba(203, 255, 92, 0.5); + --g-color-private-brand-350: rgba(203, 255, 92, 0.6); + --g-color-private-brand-400: rgba(203, 255, 92, 0.7); + --g-color-private-brand-450: rgba(203, 255, 92, 0.8); + --g-color-private-brand-500: rgba(203, 255, 92, 0.9); + --g-color-private-brand-550-solid: rgb(203, 255, 92); + --g-color-private-brand-1000-solid: rgb(247, 255, 231); + --g-color-private-brand-950-solid: rgb(245, 255, 222); + --g-color-private-brand-900-solid: rgb(239, 255, 206); + --g-color-private-brand-850-solid: rgb(234, 255, 190); + --g-color-private-brand-800-solid: rgb(229, 255, 174); + --g-color-private-brand-750-solid: rgb(224, 255, 157); + --g-color-private-brand-700-solid: rgb(219, 255, 141); + --g-color-private-brand-650-solid: rgb(213, 255, 125); + --g-color-private-brand-600-solid: rgb(208, 255, 108); + --g-color-private-brand-500-solid: rgb(186, 232, 86); + --g-color-private-brand-450-solid: rgb(169, 210, 80); + --g-color-private-brand-400-solid: rgb(152, 187, 75); + --g-color-private-brand-350-solid: rgb(135, 165, 69); + --g-color-private-brand-300-solid: rgb(119, 142, 63); + --g-color-private-brand-250-solid: rgb(102, 119, 57); + --g-color-private-brand-200-solid: rgb(85, 97, 51); + --g-color-private-brand-150-solid: rgb(68, 74, 46); + --g-color-private-brand-100-solid: rgb(59, 63, 43); + --g-color-private-brand-50-solid: rgb(51, 52, 40); + + --g-color-base-brand: rgb(203, 255, 92); + --g-color-base-background: rgb(34, 29, 34); + --g-color-base-brand-hover: var(--g-color-private-brand-650-solid); + --g-color-base-selection: var(--g-color-private-brand-150); + --g-color-base-selection-hover: var(--g-color-private-brand-200); + --g-color-line-brand: var(--g-color-private-brand-600-solid); + --g-color-text-brand: var(--g-color-private-brand-600-solid); + --g-color-text-brand-heavy: var(--g-color-private-brand-700-solid); + --g-color-text-brand-contrast: rgba(0, 0, 0, 0.9); + --g-color-text-link: var(--g-color-private-brand-550-solid); + --g-color-text-link-hover: var(--g-color-private-brand-700-solid); + --g-color-text-link-visited: var(--g-color-private-purple-700-solid); + --g-color-text-link-visited-hover: var(--g-color-private-purple-850-solid); +} diff --git a/playground/src/pages/experemental/experemental.tsx b/playground/src/pages/experemental/experemental.tsx new file mode 100644 index 0000000000..f09c440835 --- /dev/null +++ b/playground/src/pages/experemental/experemental.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; + +// @gravity-ui/page-constructor +import {NavigationData, PageConstructor, PageConstructorProvider} from '../../../../src'; +// Custom blocks — atoms +import AccordionBlockConfig from '../../blocks/AccordionBlock'; +import AlertBlockConfig from '../../blocks/AlertBlock'; +import AvatarBlockConfig from '../../blocks/AvatarBlock'; +import BreadcrumbsBlockConfig from '../../blocks/BreadcrumbsBlock'; +import ButtonBlockConfig from '../../blocks/ButtonBlock'; +import CardContainerConfig from '../../blocks/CardContainer'; +import ColumnsContainerConfig from '../../blocks/ColumnsContainer'; +import DefinitionListBlockConfig from '../../blocks/DefinitionListBlock'; +import LabelBlockConfig from '../../blocks/LabelBlock'; +import ProgressBlockConfig from '../../blocks/ProgressBlock'; +import SectionContainerConfig from '../../blocks/SectionContainer'; +import TabsAtomBlockConfig from '../../blocks/TabsAtomBlock'; +import UserBlockConfig from '../../blocks/UserBlock'; +// Custom blocks — containers +// Example 1 +import {experementalBlocksExtension} from '../../custom-plugin'; + +import contentExample1 from './example-1/content.json'; +// Example 2 +import contentExample2 from './example-2/content.json'; +import navigationExample2 from './example-2/navigation.json'; + +const customBlocks = [ + // Atoms + ButtonBlockConfig, + AlertBlockConfig, + LabelBlockConfig, + ProgressBlockConfig, + AccordionBlockConfig, + TabsAtomBlockConfig, + UserBlockConfig, + BreadcrumbsBlockConfig, + AvatarBlockConfig, + DefinitionListBlockConfig, + // Containers + ColumnsContainerConfig, + CardContainerConfig, + SectionContainerConfig, +]; + +interface PCPageProps { + id?: string | null; +} + +export default function PCPage({id}: PCPageProps) { + const pageId = id || '1'; + + const page = React.useMemo(() => { + switch (Number(pageId)) { + case 2: + import('./example-2/styles.scss'); + return { + content: contentExample2, + navigation: navigationExample2 as NavigationData, + }; + default: + case 1: + return { + content: contentExample1, + navigation: undefined, + }; + } + }, [pageId]); + + return ( + + + + ); +} diff --git a/playground/src/pages/pc/example-1/content.json b/playground/src/pages/pc/example-1/content.json new file mode 100644 index 0000000000..bafed010c0 --- /dev/null +++ b/playground/src/pages/pc/example-1/content.json @@ -0,0 +1,383 @@ +{ + "blocks": [ + { + "type": "header-block", + "title": "Yandex Open Source", + "description": "

Мы в Яндексе верим, что вклад в опенсорс — это вклад в технологическую эволюцию: без открытости, совместной работы и поддержки развитие IT‑индустрии сильно затруднено. Уже много лет мы используем в своих продуктах сторонние открытые технологии, а также делимся собственными и активно вовлекаем в их развитие разработчиков по всему миру.

", + "width": "s", + "verticalOffset": "l", + "offset": "default", + "resetPaddings": true, + "background": { + "image": { + "mobile": "https://storage.yandexcloud.net/yandex-opensource/pages/index/yos-index-cover-m.png", + "desktop": "https://storage.yandexcloud.net/yandex-opensource/pages/index/yos-index-cover.png" + }, + "color": "#EFF2F8", + "fullWidth": false, + "fullWidthMedia": true + } + }, + { + "type": "extended-features-block", + "title": { + "text": "Почему мы выкладываем наши технологии в открытый доступ?" + }, + "items": [ + { + "title": "Ответственность", + "icon": "https://storage.yandexcloud.net/cloud-www-assets/pages/open-source/os-index-icon-01.svg", + "text": "

Мы верим, что вкладываться в развитие опенсорс‑технологий — это ответственность каждого технологического лидера на рынке. Без опенсорс‑решений не появились бы многие продукты и сервисы не только Яндекса, но и других крупных компаний, и мы хотим отдавать обратно, делиться теми нашими решениями, которые, как мы считаем, принесут реальную пользу.

" + }, + { + "title": "Польза для сообщества", + "icon": "https://storage.yandexcloud.net/cloud-www-assets/pages/open-source/os-index-icon-02.svg", + "text": "

Технологии, которые мы разрабатываем, ежедневно помогают нам эффективно решать огромное количество самых разных задач в наших сервисах. Мы знаем, что разработчики вне Яндекса часто сталкиваются с теми же самыми задачами — и верим, что наши технологии могут быть полезны и им.

" + }, + { + "title": "Качество сервисов", + "icon": "https://storage.yandexcloud.net/cloud-www-assets/pages/open-source/os-index-icon-03.svg", + "text": "

Для нас важно разрабатывать и использовать только качественные технологические решения. В особенности это касается опенсорса: зная, что наши решения увидят и будут использовать другие, мы уделяем их качеству особое внимание. А уже в открытом доступе у технологии больше шансов развиваться и улучшаться — в том числе, при участии сообщества разработчиков.

" + }, + { + "title": "Бизнес‑потенциал", + "icon": "https://storage.yandexcloud.net/cloud-www-assets/pages/open-source/os-index-icon-04.svg", + "text": "

Мы верим, что при условии роста популярности наших решений и спроса на них со стороны сообщества, то, что мы выкладываем в опенсорс, может далее стать для нас бизнесом. То, что мы выкладываем в опенсорс, можно использовать и во внешних коммерческих проектах.

" + }, + { + "title": "Поиск талантов", + "icon": "https://storage.yandexcloud.net/cloud-www-assets/pages/open-source/os-index-icon-05.svg", + "text": "

Мы ценим каждого, кто вкладывается в сторонние опенсорс‑решения или делится с миром своими. Контрибьюторы в наши продукты нам особенно важны: среди них мы ищем и находим тех, кто сможет развивать технологии уже будучи частью команды Яндекса.

" + } + ] + }, + { + "type": "custom-parent-block", + "title": "Custom Grid Block", + "description": "A configurable grid with 3, 4, or 5 cards per row. Change perRow to try different layouts.", + "perRow": 4, + "children": [ + { + "type": "custom-children-block", + "title": "Design Systems", + "description": "Building consistent UI components across all products." + }, + { + "type": "custom-children-block", + "title": "Open Source", + "description": "Contributing to and maintaining public repositories." + }, + { + "type": "custom-children-block", + "title": "Performance", + "description": "Optimizing load times and runtime efficiency." + }, + { + "type": "custom-children-block", + "title": "Accessibility", + "description": "Making products usable for everyone, including assistive tech users." + }, + { + "type": "custom-children-block", + "title": "Developer Tools", + "description": "CLIs, plugins, and extensions that improve dev workflows." + }, + { + "type": "custom-children-block", + "title": "Documentation", + "description": "Clear and up-to-date guides for every project." + }, + { + "type": "custom-children-block", + "title": "Testing", + "description": "Unit, integration, and end-to-end test coverage." + }, + { + "type": "custom-children-block", + "title": "Security", + "description": "Audits, dependency scanning, and secure coding practices." + } + ] + }, + { + "type": "card-layout-block", + "animated": false, + "title": "Краткая история опенсорса в Яндексе", + "description": "

С начала истории развития опенсорса в Яндексе мы успели выложить в открытый доступ десятки собственных проектов, использовать в разработке наших продуктов внешние технологии, а также внесли существенный вклад в их развитие.

", + "colSizes": { + "all": 12, + "lg": 3, + "md": 4, + "sm": 6 + }, + "anchor": { + "url": "history", + "text": "history" + }, + "children": [ + { + "type": "layout-item", + "content": { + "title": "2010", + "text": "

Методология веб‑разработки БЭМ (Блок‑Элемент‑Модификатор) выходит в оперсорс

" + }, + "media": { + "image": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-timeline-01.png" + } + }, + { + "type": "layout-item", + "content": { + "title": "2012", + "text": "

Запуск Яндекс Браузера на базе Blink (Chromium)

" + }, + "media": { + "image": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-timeline-02.png" + } + }, + { + "type": "layout-item", + "content": { + "title": "2013", + "text": "

Яндекс начинает контрибьютить в ядро Linux

" + }, + "media": { + "image": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-timeline-03.png" + } + }, + { + "type": "layout-item", + "content": { + "title": "2016", + "text": "

Выход в опенсорс ClickHouse

\\n

Выход в опенсорс Hermione (с 2024 года — Testplane)

" + }, + "media": { + "image": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-timeline-2016.png" + } + }, + { + "type": "layout-item", + "content": { + "title": "2017", + "text": "

Выход в опенсорс CatBoost
\\nЯндекс начинает контрибьютить в PostgreSQL

" + }, + "media": { + "image": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-timeline-05.png" + } + }, + { + "type": "layout-item", + "content": { + "title": "2018", + "text": "

Выход в опенсорс Одиссея

\\n

Яндекс — топ‑контрибьютор в WAL‑G

" + }, + "media": { + "image": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-timeline-06.png" + } + }, + { + "type": "layout-item", + "content": { + "title": "2019", + "text": "

В Яндексе появляется команда разработки СУБД с открытым исходным кодом

\\n

Яндекс — спонсор разработки PostgreSQL

" + }, + "media": { + "image": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-timeline-07.png" + } + }, + { + "type": "layout-item", + "content": { + "title": "2020", + "text": "

Выход в опенсорс Testsuite

" + }, + "media": { + "image": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-timeline-2020.png" + } + }, + { + "type": "layout-item", + "content": { + "title": "2022", + "text": "

Яндекс — один из основных спонсоров разработки PostgreSQL

\\n

Выход в опенсорс YDB, userver, YaLM 100B, DivKit, Yatagan

\\n

Старт программы «Код для всех»

" + }, + "media": { + "image": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-timeline-09.png" + } + }, + { + "type": "layout-item", + "content": { + "title": "2023", + "text": "

Выход в опенсорс YTsaurus, Gravity UI, AppMetrica, Diplodoc, DataLens и счётчика Метрики

\\n

Старт Программы грантов Yandex Open Source

" + }, + "media": { + "image": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-timeline-2023.png" + } + }, + { + "type": "layout-item", + "content": { + "title": "2024", + "text": "

Первый Yandex Open Source Jam

\\n

Выход в опенсорс YaFSDP

" + }, + "media": { + "image": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-timeline-2024.png" + } + } + ] + }, + { + "type": "card-layout-block", + "title": "Наши проекты", + "description": "

В Яндексе мы разрабатываем и развиваем технологические решения самых разных сфер применения, размеров и сложности. Поэтому и в открытый доступ попадают самые разные проекты — главное, чтобы они приносили пользу не только нам, но и другим.

", + "colSizes": { + "all": 12, + "lg": 3, + "md": 4, + "sm": 6 + }, + "anchor": { + "url": "projects", + "text": "projects" + }, + "children": [ + { + "type": "background-card", + "title": "YDB", + "text": "

Отказоустойчивая распределённая SQL база данных

", + "backgroundColor": "#2399FF", + "background": { + "src": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-card-ydb.png", + "alt": "card-background" + }, + "paddingBottom": "m", + "theme": "dark" + }, + { + "type": "background-card", + "title": "YTsaurus", + "text": "

Платформа для хранения и обработки больших данных

", + "backgroundColor": "#FFB23E", + "background": { + "src": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-card-yt.png", + "alt": "card-background" + }, + "paddingBottom": "m", + "theme": "light" + }, + { + "type": "background-card", + "title": "GravityUI", + "text": "

Библиотеки для создания интерфейсов

", + "backgroundColor": "#262626", + "background": { + "src": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-card-gui.png", + "alt": "card-background" + }, + "paddingBottom": "m", + "theme": "dark" + }, + { + "type": "background-card", + "title": "DivKit", + "text": "

Фреймворк для server‑driven интерфейсов

", + "backgroundColor": "#F1F1F1", + "background": { + "src": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-card-dk.png", + "alt": "card-background" + }, + "paddingBottom": "m", + "theme": "light" + }, + { + "type": "background-card", + "title": "Diplodoc", + "text": "

Платформа для написания документации в концепции Docs as Code

", + "backgroundColor": "#79F985", + "background": { + "src": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-card-dd.png", + "alt": "card-background" + }, + "paddingBottom": "m", + "theme": "light" + }, + { + "type": "background-card", + "title": "userver", + "text": "

Фреймворк для создания высоконагруженных приложений

", + "backgroundColor": "#FF9D73", + "background": { + "src": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-card-u.png", + "alt": "card-background" + }, + "paddingBottom": "m", + "theme": "light" + }, + { + "type": "background-card", + "title": "DataLens", + "text": "

BI-платформа для анализа и визуализации данных

", + "backgroundColor": "#FF7132", + "background": { + "src": "https://storage.yandexcloud.net/yandex-opensource/pages/index/os-index-card-dl.png", + "alt": "card-background" + }, + "paddingBottom": "m", + "theme": "dark" + }, + { + "type": "basic-card", + "title": "И это ещё не всё", + "text": "

Узнать про наши опенсорс‑проекты больше вы можете на другой странице

", + "border": "none", + "buttons": [ + { + "text": "Все проекты", + "primary": true, + "theme": "monochrome", + "size": "promo", + "url": "/projects" + } + ] + } + ] + }, + { + "largeMedia": true, + "mediaOnly": false, + "size": "l", + "type": "media-block", + "direction": "content-media", + "anchor": { + "url": "1", + "text": "Фильм" + }, + "title": "

Смотрите фильм с YaC 22

", + "description": "

Руководители опенсорс‑проектов рассказывают про историю и культуру открытого кода в Яндексе.

", + "media": { + "youtube": "https://www.youtube.com/watch?v=G7G286S8ntc", + "previewImg": "https://storage.yandexcloud.net/cloud-www-assets/pages/open-source/video-cover.png" + }, + "disableShadow": true + }, + { + "type": "content-layout-block", + "size": "l", + "centered": true, + "textContent": { + "title": "", + "text": "", + "additionalInfo": "", + "buttons": [ + { + "text": "Сделано на GravityUI", + "theme": "monochrome", + "img": "https://storage.yandexcloud.net/cloud-www-assets/pages/open-source/open-source-gravity-ui-button.svg", + "url": "https://gravity-ui.com/" + } + ] + } + } + ] +} diff --git a/playground/src/pages/pc/example-2/content.json b/playground/src/pages/pc/example-2/content.json new file mode 100644 index 0000000000..568c1b8d2d --- /dev/null +++ b/playground/src/pages/pc/example-2/content.json @@ -0,0 +1,402 @@ +{ + "meta": { + "title": "YDB — распределённая SQL база данных с открытым исходным кодом", + "description": "YDB — это открытая распределённая SQL база данных, которая сочетает высокую доступность и масштабируемость со строгой согласованностью и ACID транзакциями.", + "sharing": { + "image": "https://storage.yandexcloud.net/ydb-site-assets/share-ydb-eng.png" + } + }, + "blocks": [ + { + "type": "header-block", + "title": "YDB", + "description": "YDB — это распределённая отказоустойчивая Distributed SQL база данных с открытым исходным кодом, которая сочетает в себе высокую доступность и масштабируемость со строгой согласованностью и транзакциями ACID. Она поддерживает одновременное выполнение транзакционных (OLTP), аналитических (OLAP) и потоковых нагрузок.", + "width": "s", + "verticalOffset": "m", + "imageSize": "m", + "background": { + "image": { + "src": "https://storage.yandexcloud.net/ydb-site-assets/cover.png", + "disableCompress": true, + "alt": "Фон YDB" + }, + "color": "#e2eaff", + "fullWidth": false + }, + "buttons": [ + { + "text": "Быстрый старт", + "theme": "accent", + "url": "/../docs/ru/quickstart" + }, + { + "text": "Документация", + "theme": "outlined", + "url": "/../docs/ru/" + } + ] + }, + { + "type": "card-layout-block", + "title": "Что я могу делать с YDB?", + "animated": false, + "colSizes": { + "all": 12, + "sm": 6, + "md": 4 + }, + "anchor": { + "url": "whatcanido", + "text": "Что я могу делать с YDB?" + }, + "description": "", + "children": [ + { + "type": "background-card", + "backgroundColor": "#CCE7FF", + "title": "Транзакционные нагрузки (OLTP)", + "text": "Вы можете использовать YDB для хранения состояния вашего приложения, независимо от объема данных или частоты их изменения. Обрабатывать петабайты при миллионах транзакций в секунду — не проблема.", + "controlPosition": "footer", + "buttons": [ + { + "text": "Узнать больше", + "theme": "outlined", + "url": "/docs/ru/concepts/" + } + ] + }, + { + "type": "background-card", + "backgroundColor": "rgba(107,132,153,.12)", + "title": "Аналитические нагрузки (OLAP)", + "text": "Вы можете создавать аналитические отчёты на основе хранимых в YDB данных с производительностью, сопоставимой со специализированными аналитическими СУБД. При этом никаких компромиссов по согласованности и доступности не потребуется.", + "controlPosition": "footer", + "buttons": [ + { + "text": "Узнать больше", + "theme": "outlined", + "url": "/docs/ru/concepts/datamodel/table#column-oriented-tables" + } + ] + }, + { + "type": "background-card", + "backgroundColor": "rgba(107,132,153,.12)", + "title": "Потоковые нагрузки", + "text": "Вы можете использовать функциональность YDB-топиков для надёжной отправки данных между вашими приложениями или отслеживания изменений в таблицах YDB. Можно выбрать как семантику доставки сообщений ровно один раз (exactly once), так и не менее одного раза (at least once).", + "controlPosition": "footer", + "buttons": [ + { + "text": "Узнать больше", + "theme": "outlined", + "url": "/docs/ru/concepts/topic" + } + ] + } + ] + }, + { + "type": "extended-features-block", + "title": "Почему YDB?", + "animated": false, + "colSizes": { + "all": 12, + "sm": 6, + "md": 4 + }, + "items": [ + { + "title": "Эластичность и масштабируемость", + "text": "Добавляйте или удаляйте узлы на лету, чтобы легко масштабировать кластер по мере необходимости. YDB имеет отдельные слои вычисления и хранения, что позволяет независимо добавлять дисковую ёмкость или вычислительные ресурсы в зависимости от того, чего не хватает при текущей нагрузке.", + "icon": "https://storage.yandexcloud.net/ydb-site-assets/features-icons/icon_01.svg" + }, + { + "title": "Отказоустойчивость", + "text": "YDB спроектирована для работы в трёх зонах доступности и обеспечивает работоспособность даже в случае выхода из строя одной из них. Она автоматически восстанавливается после сбоя диска, сервера или датацентра с минимальной задержкой для приложений.", + "icon": "https://storage.yandexcloud.net/ydb-site-assets/features-icons/icon_03.svg" + }, + { + "title": "Простота в использовании", + "text": "Работа с кластером YDB ощущается как работа с одноузловой СУБД с безграничными ресурсами благодаря строгой согласованности, ACID-транзакциям, высокопроизводительным запросам, возможности загрузки больших объёмов данных, а также поддержке знакомого диалекта SQL и JSON API.", + "icon": "https://storage.yandexcloud.net/ydb-site-assets/features-icons/icon_02.svg" + }, + { + "title": "Универсальность", + "text": "Благодаря поддержке различных видов нагрузок в одной системе YDB может заменить несколько систем хранения и обработки данных или всю корпоративную экосистему данных в компании.", + "icon": "https://storage.yandexcloud.net/ydb-site-assets/features-icons/icon_06.svg" + }, + { + "title": "Открытый исходный код", + "text": "[Исходный код YDB](https://github.com/ydb-platform/ydb) опубликован под лицензией Apache 2.0, накладывающей минимум ограничений на использование. Таким образом, нет рисков, связанных с привязкой к конкретному поставщику или провайдеру облачных услуг.", + "icon": "https://storage.yandexcloud.net/ydb-site-assets/features-icons/icon_05.svg" + }, + { + "title": "Совместимость с любым окружением", + "text": "YDB можно развернуть в [Kubernetes](https://github.com/ydb-platform/ydb-kubernetes-operator), в любом облачном окружении или в корпоративных ЦОД. Либо можно использовать YDB как [управляемый сервис в Yandex Cloud](https://cloud.yandex.ru/services/ydb). Также возможны локальные эксперименты на любом компьютере.", + "icon": "https://storage.yandexcloud.net/ydb-site-assets/features-icons/icon_04.svg" + } + ] + }, + { + "type": "banner-block", + "animated": false, + "title": "Оценка производительности PostgreSQL vs Distributed DBMS", + "subtitle": "TPC-C является наиболее известным набором тестов производительности для OLTP. Мы подготовили исследование производительности, сравнивающее PostgreSQL, YDB и CockroachDB в отказоустойчивых конфигурациях.", + "image": "https://storage.yandexcloud.net/ydb-site-assets/banner-block-pg.png", + "color": "#CCE7FF", + "button": { + "text": "Читать дальше", + "theme": "raised", + "url": "https://habr.com/ru/companies/ydb/articles/801587/" + } + }, + { + "type": "card-layout-block", + "title": "Кто использует YDB?", + "animated": false, + "colSizes": { + "all": 12, + "sm": 6, + "md": 4 + }, + "anchor": { + "url": "whouses", + "text": "Кто использует YDB?" + }, + "description": "", + "children": [ + { + "type": "layout-item", + "media": { + "image": { + "src": "https://storage.yandexcloud.net/ydb-site-assets/clients/metrika.png", + "disableCompress": true + } + }, + "content": { + "title": "Метрика", + "text": "[Метрика](https://metrika.yandex.ru/) - одна из крупнейших в мире платформ мобильной и веб-аналитики. Она полагается на YDB для создания пользовательских сессий на лету.\n\nПереход на YDB позволил Метрике расширить объём хранимых данных и бесконечно наращивать обрабатываемую нагрузку. Теперь одна из баз данных Метрики в YDB содержит более 400 ТБ данных и выдерживает нагрузку более 1 000 000 RPS." + }, + "fullScreen": false, + "border": true + }, + { + "type": "layout-item", + "media": { + "image": { + "src": "https://storage.yandexcloud.net/ydb-site-assets/clients/yandex-cloud-new.png", + "disableCompress": true + } + }, + "content": { + "title": "Yandex Cloud", + "text": "YDB отвечает за слой хранения для сетевых дисков [Yandex Cloud](https://cloud.yandex.ru), используется в качестве СУБД для хранения данных и метаданных облачной инфраструктуры и сервисов платформы, а также в качестве базы данных для облачного Control Plane.\n\nИнфраструктурным и платформенным сервисам Yandex Cloud необходима высокая доступность и масштабируемость, поэтому платформа выбрала YDB в качестве ключевого компонента." + }, + "fullScreen": false, + "border": true + }, + { + "type": "layout-item", + "media": { + "image": { + "src": "https://storage.yandexcloud.net/ydb-site-assets/clients/praktikum.jpg", + "disableCompress": true + } + }, + "content": { + "title": "Практикум", + "text": "[Практикум](https://practicum.yandex.ru/) - это онлайн-ориентированная образовательная платформа. Она использует YDB в качестве гибкого хранилища состояний для своих микросервисов.\n\nНативная поддержка многоарендности в YDB позволяет им избежать создания и управления выделенными базами данных для каждого компонента сервиса." + }, + "fullScreen": false, + "border": true + }, + { + "type": "layout-item", + "media": { + "image": { + "src": "https://storage.yandexcloud.net/ydb-site-assets/clients/market.png", + "disableCompress": true + } + }, + "content": { + "title": "Яндекс Маркет", + "text": "[Яндекс Маркет](https://market.yandex.ru) - один из крупнейших сервисов электронной коммерции в СНГ. Многие ключевые функции сервиса, такие как корзина, скидки, и оформление заказа, используют YDB для хранения своего состояния.\n\nВыбор YDB в качестве базы данных позволил Яндекс Маркету выдержать стократное увеличение нагрузки на корзину при соблюдении строгих гарантий времени отклика." + }, + "fullScreen": false, + "border": true + }, + { + "type": "layout-item", + "media": { + "image": { + "src": "https://storage.yandexcloud.net/ydb-site-assets/clients/jaeger.png", + "disableCompress": true + } + }, + "content": { + "title": "Auto.ru", + "text": "[Auto.ru](https://auto.ru) снизила потребление CPU для трассировочной базы данных [Jaeger](https://www.jaegertracing.io/) в три раза после перехода на YDB, что позволило записывать 500 000 трассировок в секунду без семплирования.\n\nУспешная реализация YDB в качестве хранилища трассировок доказала применимость и ключевые свойства, такие как масштабируемость, отказоустойчивость и строгая согласованность. В результате Auto.ru выбрала YDB в качестве реляционной базы данных для некоторых своих микросервисов." + }, + "fullScreen": false, + "border": true + }, + { + "type": "layout-item", + "media": { + "image": { + "src": "https://storage.yandexcloud.net/ydb-site-assets/clients/alice-new-speakers.png", + "disableCompress": "true\"" + } + }, + "content": { + "title": "Алиса", + "text": "[Алиса](https://yandex.ru/alice) - это голосовой помощник и экосистема умного дома. После перехода на YDB команда Алисы решила проблемы синхронизации между дата‑центрами, простоев при переключении основных серверов, снизила нагрузку на команду DevOps, увеличила объём хранимых данных до сотен терабайт и нагрузку до сотен тысяч запросов в секунду.\n\nПереход на YDB позволил отказаться от ручного шардинга данных и добиться строгой согласованности в кросс-датацентровом кластере. Сейчас команда Алисы использует YDB как реляционную базу данных и как базу для хранения логов и трейсов." + }, + "fullScreen": false, + "border": true + } + ] + }, + { + "type": "slider-block", + "animated": false, + "anchor": { + "url": "scenarios", + "text": "scenarios" + }, + "title": { + "text": "В каких типовых сценариях стоит использовать YDB?", + "textSize": "m" + }, + "children": [ + { + "type": "basic-card", + "title": "Работа с внезапным ростом нагрузки", + "text": "Эластичность YDB позволяет быстро изменять количество ресурсов, выделенных базе данных, чтобы обеспечить необходимую пропускную способность в соответствии с нагрузкой. Вы можете легко увеличить или уменьшить количество вычислительных ресурсов в зависимости от предстоящих нагрузок, например, на «Чёрную пятницу» или для маркетинговых кампаний." + }, + { + "type": "basic-card", + "title": "Хранение Observability данных", + "text": "[Колоночные таблицы YDB](/docs/ru/concepts/column-table) отлично подходят для хранения логов и метрик с простым доступом к ним через SQL интерфейс. Также, низкое потребление вычислительных ресурсов и масштабируемость YDB делают запись [Jaeger трейсов](https://www.jaegertracing.io/) выгодной по себестоимости и лёгкой в использовании." + }, + { + "type": "basic-card", + "title": "Документоориентированная СУБД", + "text": "Не смотря на строгую систему типов данных YDB, поддержка [типа данных JSON и связанных с ним функций](/docs/ru/yql/reference/builtins/json), расширяет возможности YDB в роли системы хранения неструктурированных документов." + }, + { + "type": "basic-card", + "title": "Кеш с SQL-интерфейсом", + "text": "Быстрый отклик и масштабируемость пропускной способности позволяют использовать YDB одновременно в качестве онлайн-базы данных и предварительно посчитанного кеша. Возможности SQL значительно повышают удобство использования и позволяют проводить оперативную аналитику данных в кеше. Например, сайты туроператоров и агрегаторов путешествий могут использовать базу данных для кеширования результатов поиска авиабилетов или туров, а также для пересчёта цен и проверки сезонной доступности." + }, + { + "type": "basic-card", + "title": "Централизованная система учёта запасов", + "text": "YDB обеспечивает строгую целостность транзакций. Это позволяет предоставлять консистентные данные о запасах на всех складах и торговых объектах, и делает YDB подходящим решением для электронной коммерции, приложений складской или транспортной логистики." + }, + { + "type": "basic-card", + "title": "Система хранения данных для Internet of Things (IoT) экосистемы", + "text": "Поддержка автоматического шардирования YDB позволяет обрабатывать потоки данных от большого количества устройств — профиль нагрузки, который встречается в проектах интернета вещей." + } + ] + }, + { + "type": "media-block", + "animated": false, + "direction": "content-media", + "title": "Можете вкратце объяснить, что такое YDB, в видеоформате?", + "description": "Конечно, смотрите →", + "anchor": { + "url": "video", + "text": "Video" + }, + "largeMedia": true, + "media": { + "youtube": "https://youtu.be/QVmhR__wAJg" + } + }, + { + "type": "banner-block", + "animated": false, + "title": "Учебный курс по YDB от участника сообщества пользователей", + "subtitle": "_Сторонняя разработка - Contribution_\n\nНачальный [учебный курс по YDB](https://stepik.org/course/186264/promo), разработанный одним из наших пользователей - Владом Бурмистровым, позволяет ознакомиться с ключевыми свойствами, возможностями и архитектурой YDB, а также научиться основным приемам по работе с YDB.", + "color": "#CCE7FF", + "image": "https://storage.yandexcloud.net/ydb-site-assets/careers/img_4.png", + "button": { + "text": "Описание и программа курса", + "theme": "raised", + "url": "https://stepik.org/course/186264/promo" + } + }, + { + "type": "tabs-block", + "title": { + "text": "Как начать?" + }, + "anchor": { + "url": "howtostart", + "text": "Как начать?" + }, + "items": [ + { + "tabName": "Docker", + "title": "Docker", + "text": " Создайте рабочий каталог и запустите локальный контейнер YDB из этого каталога:\n```bash mkdir ydb-local && cd ydb-local \ndocker run -d --rm --name ydb-local -h localhost \\\n--platform linux/amd64 \\\n-p 2135:2135 -p 2136:2136 -p 8765:8765 \\\n-v $(pwd)/ydb_certs:/ydb_certs -v $(pwd)/ydb_data:/ydb_data \\\n-e GRPC_TLS_PORT=2135 -e GRPC_PORT=2136 -e MON_PORT=8765 \\\nydbplatform/local-ydb:latest\n```\nПерейдите в раздел [«Быстрый старт»](/docs/ru/quickstart) в документации YDB, чтобы получить дополнительную информацию. " + }, + { + "tabName": "Minikube", + "title": "Minikube", + "text": " Установите Kubernetes CLI [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) и менеджер пакетов [Helm 3](https://helm.sh/docs/intro/install/).\nУстановите и запустите [Minikube](https://kubernetes.io/ru/docs/tasks/tools/install-minikube/).\nСклонируйте репозиторий с [YDB Kubernetes Operator](https://github.com/ydb-platform/ydb-kubernetes-operator).\n```\ngit clone https://github.com/ydb-platform/ydb-kubernetes-operator && cd ydb-kubernetes-operator\n```\nУстановите контроллер YDB на кластер.\n```\nhelm upgrade --install ydb-operator deploy/ydb-operator --set metrics.enabled=false\n```\nПримените манифест для создания кластера YDB.\n```\nkubectl apply -f samples/minikube/storage.yaml\n```\nДождитесь, пока `kubectl get storages.ydb.tech` не станет `Ready`.\nПримените манифест для создания базы данных.\n```\nkubectl apply -f samples/minikube/database.yaml\n```\nДождитесь, пока `kubectl get databases.ydb.tech` не станет `Ready`.\n\nПосле обработки манифеста будет создан объект StatefulSet, описывающий набор динамических узлов. Созданная база данных будет доступна изнутри кластера Kubernetes по DNS имени `database-minikube-sample` на порту 2135.\n\nПерейдите в раздел [«Быстрый старт»](/docs/ru/quickstart) в документации YDB, чтобы получить дополнительную информацию. " + }, + { + "tabName": "Установка вручную", + "title": "Установка вручную (только для Linux x86_64)", + "text": " Создайте рабочий каталог, запустите скрипт установки из него, а затем запустите одноузловую базу данных YDB с включенным режимом работы в оперативной памяти:\n``` mkdir ydb-local && cd ydb-local\ncurl https://install.ydb.tech | bash\n./start.sh ram\n```\nПерейдите в раздел [«Быстрый старт»](/docs/ru/quickstart) в документации YDB, чтобы получить дополнительную информацию. " + } + ] + }, + { + "type": "icons-block", + "size": "s", + "title": "Как оставаться на связи?", + "items": [ + { + "url": "https://github.com/ydb-platform/ydb", + "text": "GitHub", + "src": "https://storage.yandexcloud.net/ydb-site-assets/community/git.svg" + }, + { + "url": "https://t.me/ydb_ru", + "text": "Telegram", + "src": "https://storage.yandexcloud.net/ydb-site-assets/community/telegram.svg" + }, + { + "url": "https://habr.com/ru/companies/ydb/articles/", + "text": "Habr", + "src": "https://storage.yandexcloud.net/ydb-site-assets/community/habr.svg" + }, + { + "url": "https://blog.ydb.tech", + "text": "Medium", + "src": "https://storage.yandexcloud.net/ydb-site-assets/community/medium.svg" + }, + { + "url": "https://twitter.com/YDBPlatform", + "text": "Twitter", + "src": "https://storage.yandexcloud.net/ydb-site-assets/community/twitter.svg" + }, + { + "url": "https://www.linkedin.com/company/ydb-platform/", + "text": "LinkedIn", + "src": "https://storage.yandexcloud.net/ydb-site-assets/community/linkedin.svg" + }, + { + "url": "https://www.youtube.com/c/YDBPlatform", + "text": "YouTube", + "src": "https://storage.yandexcloud.net/ydb-site-assets/community/youtube.svg" + } + ] + } + ] +} diff --git a/playground/src/pages/pc/example-2/navigation.json b/playground/src/pages/pc/example-2/navigation.json new file mode 100644 index 0000000000..8df0e9cccd --- /dev/null +++ b/playground/src/pages/pc/example-2/navigation.json @@ -0,0 +1,48 @@ +{ + "logo": { + "text": "", + "icon": "https://storage.yandexcloud.net/ydb-site-assets/ydb_icon.svg" + }, + "header": { + "leftItems": [ + { + "type": "link", + "text": "Документация", + "url": "/../docs/ru", + "target": "_self" + }, + { + "type": "link", + "text": "Команда", + "url": "/careers/" + }, + { + "type": "link", + "text": "Студентам", + "url": "/students/" + }, + { + "type": "link", + "text": "Поддержка", + "url": "/support/" + }, + { + "type": "link", + "text": "Блог", + "url": "/blog/" + } + ], + "rightItems": [ + { + "type": "github-button", + "text": "Star", + "label": "Star ydb-platform/ydb on GitHub", + "url": "https://github.com/ydb-platform/ydb" + } + ] + }, + "meta": { + "title": "", + "description": "" + } +} diff --git a/playground/src/pages/pc/example-2/styles.scss b/playground/src/pages/pc/example-2/styles.scss new file mode 100644 index 0000000000..b44174a144 --- /dev/null +++ b/playground/src/pages/pc/example-2/styles.scss @@ -0,0 +1,118 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap'); + +.g-root_theme_light { + --g-border-radius-xs: 0px; + --g-border-radius-s: 0px; + --g-border-radius-m: 0px; + --g-border-radius-l: 0px; + --g-border-radius-xl: 0px; + + --g-font-family-sans: 'Inter', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --g-font-family-monospace: + 'Roboto Mono', 'Menlo', 'Monaco', 'Consolas', 'Ubuntu Mono', 'Liberation Mono', + 'DejaVu Sans Mono', 'Courier New', 'Courier', monospace; + + --g-color-private-brand-50: rgba(203, 255, 92, 0.1); + --g-color-private-brand-100: rgba(203, 255, 92, 0.15); + --g-color-private-brand-150: rgba(203, 255, 92, 0.2); + --g-color-private-brand-200: rgba(203, 255, 92, 0.3); + --g-color-private-brand-250: rgba(203, 255, 92, 0.4); + --g-color-private-brand-300: rgba(203, 255, 92, 0.5); + --g-color-private-brand-350: rgba(203, 255, 92, 0.6); + --g-color-private-brand-400: rgba(203, 255, 92, 0.7); + --g-color-private-brand-450: rgba(203, 255, 92, 0.8); + --g-color-private-brand-500: rgba(203, 255, 92, 0.9); + --g-color-private-brand-550-solid: rgb(203, 255, 92); + --g-color-private-brand-1000-solid: rgb(59, 63, 43); + --g-color-private-brand-950-solid: rgb(68, 74, 46); + --g-color-private-brand-900-solid: rgb(85, 97, 51); + --g-color-private-brand-850-solid: rgb(102, 119, 57); + --g-color-private-brand-800-solid: rgb(119, 142, 63); + --g-color-private-brand-750-solid: rgb(135, 165, 69); + --g-color-private-brand-700-solid: rgb(152, 187, 75); + --g-color-private-brand-650-solid: rgb(169, 210, 80); + --g-color-private-brand-600-solid: rgb(186, 232, 86); + --g-color-private-brand-500-solid: rgb(208, 255, 108); + --g-color-private-brand-450-solid: rgb(213, 255, 125); + --g-color-private-brand-400-solid: rgb(219, 255, 141); + --g-color-private-brand-350-solid: rgb(224, 255, 157); + --g-color-private-brand-300-solid: rgb(229, 255, 174); + --g-color-private-brand-250-solid: rgb(234, 255, 190); + --g-color-private-brand-200-solid: rgb(239, 255, 206); + --g-color-private-brand-150-solid: rgb(245, 255, 222); + --g-color-private-brand-100-solid: rgb(247, 255, 231); + --g-color-private-brand-50-solid: rgb(250, 255, 239); + + --g-color-base-brand: rgb(203, 255, 92); + --g-color-base-background: rgb(255, 255, 255); + --g-color-base-brand-hover: var(--g-color-private-brand-600-solid); + --g-color-base-selection: var(--g-color-private-brand-200); + --g-color-base-selection-hover: var(--g-color-private-brand-300); + --g-color-line-brand: var(--g-color-private-brand-600-solid); + --g-color-text-brand: var(--g-color-private-brand-700-solid); + --g-color-text-brand-heavy: var(--g-color-private-brand-700-solid); + --g-color-text-brand-contrast: rgba(0, 0, 0, 0.85); + --g-color-text-link: var(--g-color-private-brand-600-solid); + --g-color-text-link-hover: var(--g-color-private-brand-800-solid); + --g-color-text-link-visited: var(--g-color-private-purple-550-solid); + --g-color-text-link-visited-hover: var(--g-color-private-purple-800-solid); +} + +.g-root_theme_dark { + --g-border-radius-xs: 0px; + --g-border-radius-s: 0px; + --g-border-radius-m: 0px; + --g-border-radius-l: 0px; + --g-border-radius-xl: 0px; + + --g-font-family-sans: 'Inter', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --g-font-family-monospace: + 'Roboto Mono', 'Menlo', 'Monaco', 'Consolas', 'Ubuntu Mono', 'Liberation Mono', + 'DejaVu Sans Mono', 'Courier New', 'Courier', monospace; + + --g-color-private-brand-50: rgba(203, 255, 92, 0.1); + --g-color-private-brand-100: rgba(203, 255, 92, 0.15); + --g-color-private-brand-150: rgba(203, 255, 92, 0.2); + --g-color-private-brand-200: rgba(203, 255, 92, 0.3); + --g-color-private-brand-250: rgba(203, 255, 92, 0.4); + --g-color-private-brand-300: rgba(203, 255, 92, 0.5); + --g-color-private-brand-350: rgba(203, 255, 92, 0.6); + --g-color-private-brand-400: rgba(203, 255, 92, 0.7); + --g-color-private-brand-450: rgba(203, 255, 92, 0.8); + --g-color-private-brand-500: rgba(203, 255, 92, 0.9); + --g-color-private-brand-550-solid: rgb(203, 255, 92); + --g-color-private-brand-1000-solid: rgb(247, 255, 231); + --g-color-private-brand-950-solid: rgb(245, 255, 222); + --g-color-private-brand-900-solid: rgb(239, 255, 206); + --g-color-private-brand-850-solid: rgb(234, 255, 190); + --g-color-private-brand-800-solid: rgb(229, 255, 174); + --g-color-private-brand-750-solid: rgb(224, 255, 157); + --g-color-private-brand-700-solid: rgb(219, 255, 141); + --g-color-private-brand-650-solid: rgb(213, 255, 125); + --g-color-private-brand-600-solid: rgb(208, 255, 108); + --g-color-private-brand-500-solid: rgb(186, 232, 86); + --g-color-private-brand-450-solid: rgb(169, 210, 80); + --g-color-private-brand-400-solid: rgb(152, 187, 75); + --g-color-private-brand-350-solid: rgb(135, 165, 69); + --g-color-private-brand-300-solid: rgb(119, 142, 63); + --g-color-private-brand-250-solid: rgb(102, 119, 57); + --g-color-private-brand-200-solid: rgb(85, 97, 51); + --g-color-private-brand-150-solid: rgb(68, 74, 46); + --g-color-private-brand-100-solid: rgb(59, 63, 43); + --g-color-private-brand-50-solid: rgb(51, 52, 40); + + --g-color-base-brand: rgb(203, 255, 92); + --g-color-base-background: rgb(34, 29, 34); + --g-color-base-brand-hover: var(--g-color-private-brand-650-solid); + --g-color-base-selection: var(--g-color-private-brand-150); + --g-color-base-selection-hover: var(--g-color-private-brand-200); + --g-color-line-brand: var(--g-color-private-brand-600-solid); + --g-color-text-brand: var(--g-color-private-brand-600-solid); + --g-color-text-brand-heavy: var(--g-color-private-brand-700-solid); + --g-color-text-brand-contrast: rgba(0, 0, 0, 0.9); + --g-color-text-link: var(--g-color-private-brand-550-solid); + --g-color-text-link-hover: var(--g-color-private-brand-700-solid); + --g-color-text-link-visited: var(--g-color-private-purple-700-solid); + --g-color-text-link-visited-hover: var(--g-color-private-purple-850-solid); +} diff --git a/playground/src/pages/pc/pc.tsx b/playground/src/pages/pc/pc.tsx new file mode 100644 index 0000000000..eeae06384d --- /dev/null +++ b/playground/src/pages/pc/pc.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; + +import {NavigationData, PageConstructor, PageConstructorProvider} from '../../../../src'; +import {blocks} from '../../../../src/blocks'; +import {gravityBlocksExtension} from '../../../../src/gravity-blocks/extensions/GravityBlocksExtension'; + +import contentExample1 from './example-1/content.json'; +import contentExample2 from './example-2/content.json'; +import navigationExample2 from './example-2/navigation.json'; + +const customBlocks = [...blocks]; + +interface PCPageProps { + id?: string | null; +} + +export default function PCPage({id}: PCPageProps) { + const pageId = id || '1'; + + const page = React.useMemo(() => { + switch (Number(pageId)) { + case 2: + import('./example-2/styles.scss'); + return { + content: contentExample2, + navigation: navigationExample2 as NavigationData, + }; + default: + case 1: + return { + content: contentExample1, + navigation: undefined, + }; + } + }, [pageId]); + + return ( + + + + ); +} diff --git a/playground/src/router.tsx b/playground/src/router.tsx new file mode 100644 index 0000000000..79486755ea --- /dev/null +++ b/playground/src/router.tsx @@ -0,0 +1,20 @@ +import {useSearchParams} from 'react-router'; + +import EditorPage from './pages/editor/editor'; +import ExperimentalPage from './pages/experemental/experemental'; +import PCPage from './pages/pc/pc'; + +export default function Router() { + const [searchParams] = useSearchParams(); + const page = searchParams.get('page'); + const id = searchParams.get('id'); + + switch (page) { + case 'gravity-blocks': + return ; + case 'experemental': + return ; + default: + return ; + } +} diff --git a/playground/src/styles/globals.scss b/playground/src/styles/globals.scss new file mode 100644 index 0000000000..5c7535e5af --- /dev/null +++ b/playground/src/styles/globals.scss @@ -0,0 +1,6 @@ +html, +body, +#root { + min-width: 320px; + margin: 0; +} diff --git a/playground/src/vite-env.d.ts b/playground/src/vite-env.d.ts new file mode 100644 index 0000000000..b1f45c7866 --- /dev/null +++ b/playground/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/playground/tsconfig.json b/playground/tsconfig.json new file mode 100644 index 0000000000..8261a7284a --- /dev/null +++ b/playground/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "@gravity-ui/tsconfig/tsconfig", + "compilerOptions": { + "target": "es2022", + "lib": ["dom", "dom.iterable", "esnext"], + "types": ["vite-plugin-svgr/client"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "allowSyntheticDefaultImports": true, + "paths": { + "@/*": ["./src/*"], + "*": ["../src/internal-typings/*"] + } + }, + "include": ["**/*.ts", "**/*.tsx", "vite.config.mts", "../src/internal-typings/*"], + "exclude": ["node_modules"] +} diff --git a/playground/vite.config.mts b/playground/vite.config.mts new file mode 100644 index 0000000000..c68ce224d2 --- /dev/null +++ b/playground/vite.config.mts @@ -0,0 +1,27 @@ +import {defineConfig} from 'vite'; +import react from '@vitejs/plugin-react'; +import svgr from 'vite-plugin-svgr'; + +// https://vitejs.dev/config/ +export default defineConfig({ + base: './', + plugins: [svgr(), react()], + optimizeDeps: { + //workaround for the problem https://github.com/vitejs/vite/issues/7719 + extensions: ['.css'], + esbuildOptions: { + plugins: [ + (await import('esbuild-sass-plugin')).sassPlugin({ + type: 'style', + }), + ], + }, + }, + css: { + preprocessorOptions: { + scss: { + silenceDeprecations: ['import', 'mixed-decls', 'global-builtin'], + }, + }, + }, +}); diff --git a/schema.webpack.js b/schema.webpack.js index 394d01d68c..94b1273265 100644 --- a/schema.webpack.js +++ b/schema.webpack.js @@ -5,7 +5,7 @@ const path = require('path'); const TerserPlugin = require('terser-webpack-plugin'); const SRC_PATH = path.resolve('src'); -const SCHEMA_SRC_PATH = path.resolve(SRC_PATH, 'schema'); +const SCHEMA_SRC_PATH = path.resolve(SRC_PATH, 'gravity-blocks/schema'); const SCHEMA_RESULT_PATH = path.resolve(__dirname, 'schema'); const SCHEMA_BUNDLE_FILENAME = 'index.js'; diff --git a/scripts/analyze-imports-simple.js b/scripts/analyze-imports-simple.js new file mode 100644 index 0000000000..9742b9ce15 --- /dev/null +++ b/scripts/analyze-imports-simple.js @@ -0,0 +1,510 @@ +#!/usr/bin/env node +/** + * Анализатор import-зависимостей (упрощённая версия без glob) + */ + +const fs = require('fs'); +const path = require('path'); + +const PROJECT_ROOT = path.resolve(__dirname, '..'); +const SRC_DIR = path.join(PROJECT_ROOT, 'src'); + +const IMPORT_PATTERNS = [ + /import\s+.*?\s+from\s+['"]([^'"]+)['"];?/g, + /import\s+['"]([^'"]+)['"];?/g, + /export\s+.*?\s+from\s+['"]([^'"]+)['"];?/g, +]; + +function findFiles(dir, files = []) { + const items = fs.readdirSync(dir); + for (const item of items) { + const fullPath = path.join(dir, item); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + if ( + item === 'node_modules' || + item === 'build' || + item === 'dist' || + item === '__stories__' || + item === '__tests__' || + item === 'demo' + ) { + continue; + } + findFiles(fullPath, files); + } else if (item.endsWith('.ts') || item.endsWith('.tsx')) { + if (!item.includes('.test.') && !item.includes('.spec.') && !item.endsWith('.d.ts')) { + files.push(fullPath); + } + } + } + return files; +} + +function resolveImport(sourcePath, importPath) { + if ( + importPath.endsWith('.scss') || + importPath.endsWith('.css') || + importPath.endsWith('.sass') || + importPath.endsWith('.less') + ) { + return null; + } + + if (!importPath.startsWith('.') && !importPath.startsWith('src/')) { + return {type: 'external', path: importPath}; + } + + let resolved; + if (importPath.startsWith('src/')) { + resolved = path.join(PROJECT_ROOT, importPath); + } else { + resolved = path.resolve(path.dirname(sourcePath), importPath); + } + + const extensions = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js']; + + for (const ext of extensions) { + const fullPath = resolved + ext; + if (fs.existsSync(fullPath)) { + return {type: 'internal', path: path.relative(PROJECT_ROOT, fullPath)}; + } + } + + return {type: 'internal', path: path.relative(PROJECT_ROOT, resolved)}; +} + +function parseImports(filePath) { + const content = fs.readFileSync(filePath, 'utf-8'); + const imports = new Set(); + + for (const pattern of IMPORT_PATTERNS) { + let match; + while ((match = pattern.exec(content)) !== null) { + const importPath = match[1]; + const resolved = resolveImport(filePath, importPath); + if (resolved) { + imports.add(JSON.stringify(resolved)); + } + } + } + + return Array.from(imports).map((i) => JSON.parse(i)); +} + +function analyze() { + console.log('🔍 Scanning files...'); + const files = findFiles(SRC_DIR); + console.log(`📁 Found ${files.length} files`); + + const nodes = new Map(); + const edges = []; + const externalDeps = new Set(); + + for (const file of files) { + const relativePath = path.relative(PROJECT_ROOT, file); + const dir = path.dirname(relativePath); + const category = dir.split('/')[1] || 'other'; + + nodes.set(relativePath, { + id: relativePath, + path: relativePath, + category, + name: path.basename(relativePath, path.extname(relativePath)), + imports: [], + }); + + const imports = parseImports(file); + for (const imp of imports) { + if (imp.type === 'external') { + externalDeps.add(imp.path); + } else { + const targetPath = path.join(PROJECT_ROOT, imp.path); + const exists = + fs.existsSync(targetPath) || + fs.existsSync(targetPath + '.ts') || + fs.existsSync(targetPath + '.tsx'); + + if (exists) { + const targetRel = path.relative(PROJECT_ROOT, targetPath); + if ( + nodes.has(targetRel) || + files.some((f) => path.relative(PROJECT_ROOT, f) === targetRel) + ) { + edges.push({source: relativePath, target: imp.path}); + nodes.get(relativePath).imports.push(imp.path); + } + } + } + } + } + + console.log(`📊 ${nodes.size} nodes, ${edges.length} edges`); + return {nodes: Array.from(nodes.values()), edges, externalDeps: Array.from(externalDeps)}; +} + +function generateHTML(data) { + const categories = [...new Set(data.nodes.map((n) => n.category))]; + const categoryColors = { + blocks: '#FF6B6B', + subBlocks: '#4ECDC4', + components: '#45B7D1', + editor: '#96CEB4', + 'editor-v2': '#96CEB4', + containers: '#FFEAA7', + models: '#DDA0DD', + hooks: '#98D8C8', + common: '#F7DC6F', + navigation: '#BB8FCE', + form: '#85C1E9', + schema: '#F8C471', + other: '#AAB7B8', + }; + + const totalImports = data.edges.length; + const cycles = []; + + return ` + + + + + Import Dependencies - Page Constructor + + + + +
+ +
+ +
+ + +
+
+
+ + + +`; +} + +// Run +const data = analyze(); +const html = generateHTML(data); +fs.writeFileSync(path.join(PROJECT_ROOT, 'import-graph.html'), html); +console.log('✅ Done! Open import-graph.html in your browser.'); diff --git a/scripts/analyze-imports.js b/scripts/analyze-imports.js new file mode 100644 index 0000000000..a8c95c58a5 --- /dev/null +++ b/scripts/analyze-imports.js @@ -0,0 +1,935 @@ +#!/usr/bin/env node +/** + * Анализатор import-зависимостей для page-constructor + * Сканирует TypeScript файлы и строит граф зависимостей + */ + +const fs = require('fs'); +const path = require('path'); + +const glob = require('glob'); + +const PROJECT_ROOT = path.resolve(__dirname, '..'); + +// Паттерны для поиска импортов +const IMPORT_PATTERNS = [ + /import\s+.*?\s+from\s+['"]([^'"]+)['"];?/g, + /import\s+['"]([^'"]+)['"];?/g, + /export\s+.*?\s+from\s+['"]([^'"]+)['"];?/g, +]; + +// Исключаемые пути +const EXCLUDE_PATTERNS = [ + 'node_modules', + 'build', + 'dist', + '.storybook', + '**/*.d.ts', + '**/*.test.ts', + '**/*.test.tsx', + '**/*.spec.ts', + '**/*.spec.tsx', + '**/__stories__/**', + '**/__tests__/**', + '**/demo/**', +]; + +function resolveImportPath(sourcePath, importPath) { + // Игнорируем все asset-файлы (стили, картинки, шрифты) + const assetExts = [ + '.scss', + '.css', + '.sass', + '.less', + '.svg', + '.png', + '.jpg', + '.jpeg', + '.gif', + '.webp', + '.woff', + '.woff2', + '.ttf', + '.eot', + '.json', + ]; + if (assetExts.some((ext) => importPath.endsWith(ext))) { + return null; + } + + // Внешние зависимости (@gravity-ui, react, etc.) + if (!importPath.startsWith('.') && !importPath.startsWith('src/')) { + return {type: 'external', path: importPath}; + } + + // Относительные пути + const resolved = path.resolve(path.dirname(sourcePath), importPath); + + // Проверяем существование файла с разными расширениями + const extensions = ['.ts', '.tsx', '.js', '.jsx']; + const indexExtensions = ['/index.ts', '/index.tsx', '/index.js']; + + // Сначала проверяем как файл + for (const ext of extensions) { + const fullPath = resolved + ext; + if (fs.existsSync(fullPath)) { + return {type: 'internal', path: path.relative(PROJECT_ROOT, fullPath)}; + } + } + + // Потом проверяем как папку с index + for (const ext of indexExtensions) { + const fullPath = resolved + ext; + if (fs.existsSync(fullPath)) { + return {type: 'internal', path: path.relative(PROJECT_ROOT, fullPath)}; + } + } + + // Если ничего не найдено - возвращаем null, не создаем ноду + return null; +} + +function parseImports(filePath) { + const content = fs.readFileSync(filePath, 'utf-8'); + const imports = new Set(); + + for (const pattern of IMPORT_PATTERNS) { + let match; + while ((match = pattern.exec(content)) !== null) { + const importPath = match[1]; + const resolved = resolveImportPath(filePath, importPath); + if (resolved) { + imports.add(JSON.stringify(resolved)); + } + } + } + + return Array.from(imports).map((i) => JSON.parse(i)); +} + +function analyzeProject() { + const files = glob.sync('src/**/*.{ts,tsx}', { + cwd: PROJECT_ROOT, + absolute: true, + ignore: EXCLUDE_PATTERNS, + }); + + const nodes = new Map(); + const edges = []; + const externalDeps = new Set(); + + files.forEach((file) => { + const relativePath = path.relative(PROJECT_ROOT, file); + const imports = parseImports(file); + + // Создаем узел + const dir = path.dirname(relativePath); + const category = dir.split('/')[1] || 'other'; // src/{category}/... + + nodes.set(relativePath, { + id: relativePath, + path: relativePath, + category, + name: path.basename(relativePath, path.extname(relativePath)), + imports: [], + }); + + imports.forEach((imp) => { + if (imp.type === 'external') { + externalDeps.add(imp.path); + } else { + // Проверяем, что target файл существует (исключаем SCSS и несуществующие файлы) + const targetPath = path.join(PROJECT_ROOT, imp.path); + const targetExists = + fs.existsSync(targetPath) || + fs.existsSync(targetPath + '.ts') || + fs.existsSync(targetPath + '.tsx'); + + if (targetExists) { + edges.push({ + source: relativePath, + target: imp.path, + }); + nodes.get(relativePath).imports.push(imp.path); + } + } + }); + }); + + // Находим циклические зависимости + const cycles = findCycles(nodes, edges); + + return { + nodes: Array.from(nodes.values()), + edges, + externalDeps: Array.from(externalDeps), + cycles, + stats: { + totalFiles: nodes.size, + totalImports: edges.length, + externalDeps: externalDeps.size, + cycles: cycles.length, + }, + }; +} + +function findCycles(nodes, edges) { + const cycles = []; + const visited = new Set(); + const recursionStack = new Set(); + + const adj = new Map(); + edges.forEach((e) => { + if (!adj.has(e.source)) adj.set(e.source, []); + adj.get(e.source).push(e.target); + }); + + function dfs(node, path) { + if (recursionStack.has(node)) { + const cycleStart = path.indexOf(node); + const cycle = path.slice(cycleStart); + cycles.push(cycle); + return; + } + + if (visited.has(node)) return; + + visited.add(node); + recursionStack.add(node); + path.push(node); + + const neighbors = adj.get(node) || []; + for (const neighbor of neighbors) { + if (nodes.has(neighbor)) { + dfs(neighbor, [...path]); + } + } + + recursionStack.delete(node); + } + + for (const [nodeId] of nodes) { + if (!visited.has(nodeId)) { + dfs(nodeId, []); + } + } + + return cycles; +} + +function generateHTML(data) { + const categories = [...new Set(data.nodes.map((n) => n.category))]; + const categoryColors = { + blocks: '#FF6B6B', + subBlocks: '#4ECDC4', + components: '#45B7D1', + editor: '#96CEB4', + 'editor-v2': '#96CEB4', + containers: '#FFEAA7', + models: '#DDA0DD', + hooks: '#98D8C8', + common: '#F7DC6F', + navigation: '#BB8FCE', + form: '#85C1E9', + schema: '#F8C471', + other: '#AAB7B8', + }; + + const nodesJson = JSON.stringify(data.nodes); + const edgesJson = JSON.stringify(data.edges); + const cyclesJson = JSON.stringify(data.cycles); + const categoriesJson = JSON.stringify(categories); + const colorsJson = JSON.stringify(categoryColors); + const statsJson = JSON.stringify(data.stats); + + return ` + + + + + Import Dependencies Graph - Page Constructor + + + + +
+ +
+ +
+
+
Connection Types
+
+
+ Normal import +
+
+
+ Circular / Selected +
+
+
+ + +
+
+
+ + + +`; +} + +// Main execution +if (require.main === module) { + console.log('🔍 Analyzing project imports...'); + + const data = analyzeProject(); + + console.log(` +📊 Statistics: + Files: ${data.stats.totalFiles} + Internal imports: ${data.stats.totalImports} + External deps: ${data.stats.externalDeps} + Circular deps: ${data.stats.cycles} + Categories: ${new Set(data.nodes.map((n) => n.category)).size} +`); + + if (data.cycles.length > 0) { + console.log('⚠️ Found circular dependencies:'); + data.cycles.forEach((cycle, i) => { + console.log(` ${i + 1}. ${cycle.join(' → ')}`); + }); + } + + const outputPath = path.join(PROJECT_ROOT, 'import-graph.html'); + const html = generateHTML(data); + fs.writeFileSync(outputPath, html); + + console.log(` +✅ Graph saved to: ${outputPath} +🌐 Open this file in your browser to explore the dependencies +`); +} + +module.exports = {analyzeProject, generateHTML}; diff --git a/src/blocks/Banner/Banner.tsx b/src/blocks/Banner/Banner.tsx index 89a7906fc7..efda5ab797 100644 --- a/src/blocks/Banner/Banner.tsx +++ b/src/blocks/Banner/Banner.tsx @@ -1,4 +1,5 @@ import AnimateBlock from '../../components/AnimateBlock/AnimateBlock'; +import {Grid, Row} from '../../gravity-blocks/grid'; import {BannerBlockProps} from '../../models'; import {BannerCard} from '../../sub-blocks'; import {block} from '../../utils'; @@ -12,7 +13,11 @@ export const BannerBlock = (props: BannerBlockProps) => { return ( - + + + + + ); }; diff --git a/src/blocks/Banner/__stories__/Banner.stories.tsx b/src/blocks/Banner/__stories__/Banner.stories.tsx index 7a8edd2653..c5c18fbd40 100644 --- a/src/blocks/Banner/__stories__/Banner.stories.tsx +++ b/src/blocks/Banner/__stories__/Banner.stories.tsx @@ -3,12 +3,16 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {BannerBlockModel, BannerBlockProps} from '../../../models'; import Banner from '../Banner'; +import {form} from '../form'; import data from './data.json'; export default { title: 'Blocks/Banner', component: Banner, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn = (args) => ( diff --git a/src/blocks/Banner/form.ts b/src/blocks/Banner/form.ts new file mode 100644 index 0000000000..51e3452360 --- /dev/null +++ b/src/blocks/Banner/form.ts @@ -0,0 +1,19 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {BannerCardProps} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + BannerCardProps as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + color: 'rgba(54, 151, 241, 0.4)', + title: 'Banner Block', + subtitle: 'Some sort of description.', + button: { + text: 'Read more', + }, +}; diff --git a/src/blocks/Banner/icon.ts b/src/blocks/Banner/icon.ts new file mode 100644 index 0000000000..a2ec141547 --- /dev/null +++ b/src/blocks/Banner/icon.ts @@ -0,0 +1,13 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + ` + + + + + + + +`, +); diff --git a/src/blocks/Banner/index.ts b/src/blocks/Banner/index.ts new file mode 100644 index 0000000000..89b11c44e8 --- /dev/null +++ b/src/blocks/Banner/index.ts @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import BannerBlock from './Banner'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const BannerBlockConfig: BlockData = { + type: '@gravity-ui/page-constructor/banner-block', + component: BannerBlock, + schema: { + name: 'Banner Block', + group: '@gravity-ui/page-constructor/Blocks', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default BannerBlockConfig; diff --git a/src/blocks/Banner/index_deprecated.ts b/src/blocks/Banner/index_deprecated.ts new file mode 100644 index 0000000000..5cd1f5c9da --- /dev/null +++ b/src/blocks/Banner/index_deprecated.ts @@ -0,0 +1,20 @@ +import {BlockData} from '../../constructor-items'; + +import BannerBlock from './Banner'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const BannerBlockConfig: BlockData = { + type: 'banner-block', + component: BannerBlock, + schema: { + name: 'Banner Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default BannerBlockConfig; diff --git a/src/blocks/Banner/schema.ts b/src/blocks/Banner/schema.ts index 110bc6d7c8..74f423658b 100644 --- a/src/blocks/Banner/schema.ts +++ b/src/blocks/Banner/schema.ts @@ -5,7 +5,7 @@ import { ThemeProps, mediaView, withTheme, -} from '../../schema/validators/common'; +} from '../../gravity-blocks/schema/validators/common'; export const BannerCardProps = { additionalProperties: false, diff --git a/src/blocks/CardLayout/CardLayout.scss b/src/blocks/CardLayout/CardLayout.scss index 404d6d22bf..8b46ad619a 100644 --- a/src/blocks/CardLayout/CardLayout.scss +++ b/src/blocks/CardLayout/CardLayout.scss @@ -35,6 +35,7 @@ $largeBorderRadius: 32px; width: 100%; height: 100%; border-radius: $largeBorderRadius; + pointer-events: none; img { object-fit: cover; diff --git a/src/blocks/CardLayout/CardLayout.tsx b/src/blocks/CardLayout/CardLayout.tsx index ad6294dfc2..7f56546c10 100644 --- a/src/blocks/CardLayout/CardLayout.tsx +++ b/src/blocks/CardLayout/CardLayout.tsx @@ -3,8 +3,10 @@ import * as React from 'react'; import isEmpty from 'lodash/isEmpty'; import {AnimateBlock, BackgroundImage, Title} from '../../components'; -import {useTheme} from '../../context/theme'; -import {Col, GridColumnSizesType, GridJustifyContent, Row} from '../../grid'; +import ChildrenItemWrap from '../../components/editor/ChildrenItemWrap/ChildrenItemWrap'; +import ChildrensWrap from '../../components/editor/ChildrensWrap/ChildrensWrap'; +import {useTheme} from '../../gravity-blocks/context/theme'; +import {Col, Grid, GridColumnSizesType, GridJustifyContent, Row} from '../../gravity-blocks/grid'; import {CardLayoutBlockProps as CardLayoutBlockParams, ClassNameProps} from '../../models'; import {block, getThemedValue} from '../../utils'; @@ -38,30 +40,38 @@ const CardLayout = ({ const {border, ...backgroundImageProps} = getThemedValue(background || {}, theme); return ( - {(title || description) && ( - - )} - <div - className={b('content', { - 'with-background': !isEmpty(background), - })} - > - <BackgroundImage className={b('image', {border})} {...backgroundImageProps} /> - <Row> - {React.Children.map(children, (child, index) => ( - <Col key={index} sizes={colSizes} className={b('item')}> - {child} - </Col> - ))} - </Row> - </div> + <Grid> + {(title || description) && ( + <Row> + <Title + title={title} + subtitle={description} + className={b('title', {centered}, titleClassName)} + colJustifyContent={ + centered ? GridJustifyContent.Center : GridJustifyContent.Start + } + /> + </Row> + )} + <div + className={b('content', { + 'with-background': !isEmpty(background), + })} + > + <BackgroundImage className={b('image', {border})} {...backgroundImageProps} /> + <ChildrensWrap> + <Row> + {React.Children.map(children, (child, index) => ( + <Col key={index} sizes={colSizes} className={b('item')}> + <ChildrenItemWrap className={b('item-wrap')} index={index}> + {child} + </ChildrenItemWrap> + </Col> + ))} + </Row> + </ChildrensWrap> + </div> + </Grid> </AnimateBlock> ); }; diff --git a/src/blocks/CardLayout/__stories__/CardLayout.stories.tsx b/src/blocks/CardLayout/__stories__/CardLayout.stories.tsx index 3f6572b55f..d89f605745 100644 --- a/src/blocks/CardLayout/__stories__/CardLayout.stories.tsx +++ b/src/blocks/CardLayout/__stories__/CardLayout.stories.tsx @@ -14,12 +14,16 @@ import { } from '../../../models'; import {BackgroundCard, BasicCard, ImageCard, LayoutItem, PriceCard} from '../../../sub-blocks'; import CardLayout, {CardLayoutBlockProps} from '../CardLayout'; +import {form} from '../form'; import data from './data.json'; export default { title: 'Blocks/CardLayout', component: CardLayout, + parameters: { + inputs: form, + }, } as Meta; const renderChild = (childArgs: SubBlockModels, index?: number) => { diff --git a/src/blocks/CardLayout/form.ts b/src/blocks/CardLayout/form.ts new file mode 100644 index 0000000000..13b2d807d5 --- /dev/null +++ b/src/blocks/CardLayout/form.ts @@ -0,0 +1,34 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {CardLayoutProps} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + CardLayoutProps as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + type: 'card-layout-block', + children: [ + { + type: 'background-card', + title: 'Tell a story and build a narrative', + text: 'We are all storytellers. Stories are a powerful way to communicate ideas and share information. The right story can lead to a better understanding of a situation, make us laugh, or even inspire us to do something in the future.', + }, + { + type: 'background-card', + title: 'Tell a story and build a narrative', + text: 'We are all storytellers. Stories are a powerful way to communicate ideas and share information. The right story can lead to a better understanding of a situation, make us laugh, or even inspire us to do something in the future.', + }, + { + type: 'background-card', + title: 'Tell a story and build a narrative', + text: 'We are all storytellers. Stories are a powerful way to communicate ideas and share information. The right story can lead to a better understanding of a situation, make us laugh, or even inspire us to do something in the future.', + }, + ], + title: 'Card Layout Block', + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', +}; diff --git a/src/blocks/CardLayout/icon.ts b/src/blocks/CardLayout/icon.ts new file mode 100644 index 0000000000..9b311bb69c --- /dev/null +++ b/src/blocks/CardLayout/icon.ts @@ -0,0 +1,19 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<rect x="8.93024" y="10.2906" width="25.8915" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="8.93024" y="10.2906" width="25.8915" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="12.2791" y="28.593" width="19.1938" height="2" rx="0.55814" fill="#262626"/> +<rect x="12.2791" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +<rect x="37.0543" y="10.2906" width="25.8915" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="37.0543" y="10.2906" width="25.8915" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="40.4031" y="28.593" width="19.1938" height="2" rx="0.55814" fill="#262626"/> +<rect x="40.4031" y="31.7094" width="10" height="2" rx="0.55814" fill="#262626"/> +<rect x="65.1783" y="10.2906" width="25.8915" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="65.1783" y="10.2906" width="25.8915" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="68.5271" y="28.593" width="19.1938" height="2" rx="0.55814" fill="#262626"/> +<rect x="68.5271" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +</svg>`, +); diff --git a/src/blocks/CardLayout/index.ts b/src/blocks/CardLayout/index.ts new file mode 100644 index 0000000000..e15e07203c --- /dev/null +++ b/src/blocks/CardLayout/index.ts @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import CardLayout from './CardLayout'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const CardLayoutBlockConfig: BlockData = { + type: 'card-layout-block', + component: CardLayout, + schema: { + name: 'Card Layout Block', + group: '@gravity-ui/page-constructor/CardContainers', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default CardLayoutBlockConfig; diff --git a/src/blocks/CardLayout/index_deprecated.ts b/src/blocks/CardLayout/index_deprecated.ts new file mode 100644 index 0000000000..0deccef818 --- /dev/null +++ b/src/blocks/CardLayout/index_deprecated.ts @@ -0,0 +1,20 @@ +import {BlockData} from '../../constructor-items'; + +import CardLayout from './CardLayout'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const CardLayoutBlockConfig: BlockData = { + type: 'card-layout-block', + component: CardLayout, + schema: { + name: 'Card Layout Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default CardLayoutBlockConfig; diff --git a/src/blocks/CardLayout/schema.ts b/src/blocks/CardLayout/schema.ts index e8a292f0db..5c63a57e23 100644 --- a/src/blocks/CardLayout/schema.ts +++ b/src/blocks/CardLayout/schema.ts @@ -6,7 +6,7 @@ import { BorderProps, ChildrenCardsProps, containerSizesObject, -} from '../../schema/validators/common'; +} from '../../gravity-blocks/schema/validators/common'; export const CardLayoutProps = { additionalProperties: false, diff --git a/src/blocks/Companies/Companies.tsx b/src/blocks/Companies/Companies.tsx index d16d78a05e..5d10de43c5 100644 --- a/src/blocks/Companies/Companies.tsx +++ b/src/blocks/Companies/Companies.tsx @@ -1,6 +1,7 @@ import {Image, Title} from '../../components'; import AnimateBlock from '../../components/AnimateBlock/AnimateBlock'; -import {useTheme} from '../../context/theme'; +import {useTheme} from '../../gravity-blocks/context/theme'; +import {Grid} from '../../gravity-blocks/grid'; import {CompaniesBlockProps} from '../../models'; import {block, getThemedValue} from '../../utils'; @@ -14,12 +15,18 @@ export const CompaniesBlock = ({title, description, images, animated}: Companies return ( <AnimateBlock className={b()} offset={150} animate={animated}> - <div className={b('content')}> - <Title title={title} subtitle={description} colSizes={{all: 12, sm: 12}}> -
- + +
+ +
+ +
-
+
); }; diff --git a/src/blocks/Companies/__stories__/Companies.stories.tsx b/src/blocks/Companies/__stories__/Companies.stories.tsx index 098ff6670d..44bd7d4842 100644 --- a/src/blocks/Companies/__stories__/Companies.stories.tsx +++ b/src/blocks/Companies/__stories__/Companies.stories.tsx @@ -3,6 +3,7 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {CompaniesBlockModel, CompaniesBlockProps} from '../../../models'; import Companies from '../Companies'; +import {form} from '../form'; import data from './data.json'; @@ -10,6 +11,7 @@ export default { title: 'Blocks/Companies', component: Companies, parameters: { + inputs: form, controls: { exclude: ['type'], }, diff --git a/src/blocks/Companies/form.ts b/src/blocks/Companies/form.ts new file mode 100644 index 0000000000..50ae4f2287 --- /dev/null +++ b/src/blocks/Companies/form.ts @@ -0,0 +1,15 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {CompaniesBlock as CompaniesBlockSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + CompaniesBlockSchema['companies-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + title: 'Companies Block', + description: 'Here is the list', +}; diff --git a/src/blocks/Companies/index.ts b/src/blocks/Companies/index.ts new file mode 100644 index 0000000000..9bea0f8014 --- /dev/null +++ b/src/blocks/Companies/index.ts @@ -0,0 +1,17 @@ +import {BlockData} from '../../constructor-items'; + +import CompaniesBlock from './Companies'; +import {defaultValue, form} from './form'; + +const CompaniesBlockConfig: BlockData = { + type: '@gravity-ui/page-constructor/companies-block', + component: CompaniesBlock, + schema: { + name: 'Companies Block', + group: '@gravity-ui/page-constructor/UnfinishedBlocks', + inputs: form, + default: defaultValue, + }, +}; + +export default CompaniesBlockConfig; diff --git a/src/blocks/Companies/index_deprecated.ts b/src/blocks/Companies/index_deprecated.ts new file mode 100644 index 0000000000..7c7a895e21 --- /dev/null +++ b/src/blocks/Companies/index_deprecated.ts @@ -0,0 +1,18 @@ +import {BlockData} from '../../constructor-items'; + +import CompaniesBlock from './Companies'; +import {defaultValue, form} from './form'; + +const CompaniesBlockConfig: BlockData = { + type: 'companies-block', + component: CompaniesBlock, + schema: { + name: 'Companies Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + }, +}; + +export default CompaniesBlockConfig; diff --git a/src/blocks/Companies/schema.ts b/src/blocks/Companies/schema.ts index 09e021d6f3..332434b9c7 100644 --- a/src/blocks/Companies/schema.ts +++ b/src/blocks/Companies/schema.ts @@ -1,4 +1,4 @@ -import {AnimatableProps, BaseProps, withTheme} from '../../schema/validators/common'; +import {AnimatableProps, BaseProps, withTheme} from '../../gravity-blocks/schema/validators/common'; export const CompaniesBlock = { 'companies-block': { diff --git a/src/blocks/ContentLayout/ContentLayout.tsx b/src/blocks/ContentLayout/ContentLayout.tsx index a6cfd666c6..884773d4e7 100644 --- a/src/blocks/ContentLayout/ContentLayout.tsx +++ b/src/blocks/ContentLayout/ContentLayout.tsx @@ -2,9 +2,9 @@ import * as React from 'react'; import {BackgroundImage, FileLink} from '../../components'; import {BREAKPOINTS} from '../../constants'; -import {useTheme} from '../../context/theme'; -import {useWindowWidth} from '../../context/windowWidthContext'; -import {Col} from '../../grid'; +import {useTheme} from '../../gravity-blocks/context/theme'; +import {useWindowWidth} from '../../gravity-blocks/context/windowWidthContext'; +import {Col, Grid} from '../../gravity-blocks/grid'; import {ContentLayoutBlockProps, ContentSize, ContentTextSize} from '../../models'; import {Content} from '../../sub-blocks'; import {block, getThemedValue} from '../../utils'; @@ -56,39 +56,41 @@ export const ContentLayoutBlock = (props: ContentLayoutBlockProps) => { const themedBackground = getThemedValue(background, globalTheme); return ( -
- - {fileContent && ( - - {fileContent.map((file) => ( - +
+ + {fileContent && ( + + {fileContent.map((file) => ( + + ))} + + )} + {background && ( +
+ - ))} - - )} - {background && ( -
- -
- )} -
+
+ )} +
+ ); }; export default ContentLayoutBlock; diff --git a/src/blocks/ContentLayout/__stories__/ContentLayout.stories.tsx b/src/blocks/ContentLayout/__stories__/ContentLayout.stories.tsx index a17da46343..7eeea783e6 100644 --- a/src/blocks/ContentLayout/__stories__/ContentLayout.stories.tsx +++ b/src/blocks/ContentLayout/__stories__/ContentLayout.stories.tsx @@ -5,12 +5,16 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {ContentLayoutBlockModel, ContentLayoutBlockProps} from '../../../models'; import Content from '../ContentLayout'; +import {form} from '../form'; import data from './data.json'; export default { title: 'Blocks/ContentLayout', component: Content, + parameters: { + inputs: form, + }, } as Meta; const SIZES = ['l', 'm', 's'].map((size) => ({ diff --git a/src/blocks/ContentLayout/form.ts b/src/blocks/ContentLayout/form.ts new file mode 100644 index 0000000000..9dad92fb28 --- /dev/null +++ b/src/blocks/ContentLayout/form.ts @@ -0,0 +1,423 @@ +import {Fields} from '../../form-generator-v2'; + +export const form = [ + { + type: 'section', + title: 'Layout settings', + opened: true, + fields: [ + { + type: 'switch', + title: 'Centered', + name: 'centered', + }, + ], + }, + { + type: 'section', + title: 'Text', + opened: true, + fields: [ + { + type: 'textInput', + title: 'Title', + name: 'textContent.title', + placeholder: 'Text', + }, + { + type: 'textArea', + title: 'Description', + name: 'textContent.text', + placeholder: 'Text', + }, + { + type: 'textArea', + title: 'Additional info', + name: 'textContent.additionalInfo', + placeholder: 'Text', + }, + { + type: 'select', + title: 'Width', + name: 'textWidth', + options: [ + {value: 's', content: 'S'}, + {value: 'm', content: 'M'}, + {value: 'l', content: 'L'}, + ], + defaultValue: 'm', + }, + { + type: 'select', + title: 'Text size', + name: 'size', + options: [ + {value: 's', content: 'S'}, + {value: 'm', content: 'M'}, + {value: 'l', content: 'L'}, + ], + defaultValue: 'l', + }, + { + type: 'segmentedRadioGroup', + title: 'Theme', + options: [ + {content: 'Light', value: 'light'}, + {content: 'Dark', value: 'dark'}, + {content: 'Default', value: 'default'}, + ], + name: 'theme', + }, + ], + }, + { + type: 'section', + title: 'Content list', + withAddButton: true, + index: 'index1', + itemTitle: 'Item {{index1}}', + itemView: 'card', + fields: [ + { + type: 'textInput', + title: 'Title', + name: 'textContent.list[{{index1}}].title', + placeholder: 'Text', + defaultValue: 'Item title', + }, + { + type: 'textArea', + title: 'Description', + name: 'textContent.list[{{index1}}].text', + placeholder: 'Text', + defaultValue: 'Item text', + }, + { + type: 'textInput', + title: 'URL icon', + name: 'textContent.list[{{index1}}].icon', + placeholder: 'https://', + }, + ], + }, + { + type: 'section', + title: 'Buttons', + withAddButton: true, + index: 'index', + itemTitle: 'Button {{index}}', + itemView: 'card', + fields: [ + { + type: 'section', + title: 'Main settings', + opened: true, + fields: [ + { + title: 'Text', + type: 'textInput', + name: 'textContent.buttons[{{index}}].text', + placeholder: 'Text', + defaultValue: 'Learn more', + }, + { + title: 'URL', + type: 'textInput', + name: 'textContent.buttons[{{index}}].url', + placeholder: 'https://', + }, + { + title: 'URL title', + type: 'textInput', + name: 'textContent.buttons[{{index}}].urlTitle', + placeholder: 'https://', + }, + { + title: 'Style', + type: 'select', + name: 'textContent.buttons[{{index}}].theme', + options: [ + {value: 'action', content: 'Action'}, + {value: 'outlined', content: 'Outlined'}, + {value: 'normal', content: 'Normal'}, + {value: 'monochrome', content: 'Monochrome'}, + { + value: 'outlined-contrast', + content: 'Outlined-contrast', + }, + {value: 'normal-contrast', content: 'Normal-contrast'}, + ], + }, + { + title: 'Target', + type: 'select', + name: 'textContent.buttons[{{index}}].target', + options: [ + {value: '_blank'}, + {value: '_self'}, + {value: '_parent'}, + {value: '_top'}, + ], + hasClear: true, + }, + ], + }, + { + type: 'section', + title: 'Analytics tracking', + itemView: 'clear', + fields: [ + { + title: 'Name', + type: 'textInput', + name: 'textContent.buttons[{{index}}].analyticsEvents[0].name', + placeholder: 'Text', + }, + { + title: 'Target', + type: 'textInput', + name: 'textContent.buttons[{{index}}].analyticsEvents[0].target', + placeholder: 'Text', + }, + { + title: 'Counter', + type: 'textInput', + name: 'textContent.buttons[{{index}}].analyticsEvents[0].counters[0].includes', + placeholder: 'Text', + }, + { + type: 'text', + text: 'Only events for the counters listed in the input field will be sent.', + level: 'info', + }, + ], + }, + ], + }, + { + type: 'section', + title: 'Link', + index: 'index1', + withAddButton: true, + itemTitle: 'Link {{index1}}', + itemView: 'card', + fields: [ + { + type: 'textInput', + title: 'Text', + name: 'textContent.links[{{index1}}].text', + placeholder: 'Text', + defaultValue: 'Learn more', + }, + { + type: 'textInput', + title: 'URL', + name: 'textContent.links[{{index1}}].url', + placeholder: 'https://', + }, + { + type: 'textInput', + title: 'URL title', + name: 'textContent.links[{{index1}}].urlTitle', + placeholder: 'https://', + }, + { + type: 'select', + title: 'Style', + name: 'textContent.links[{{index1}}].theme', + options: [ + {content: 'File-link', value: 'file-link'}, + {content: 'Normal', value: 'normal'}, + {content: 'Back', value: 'back'}, + {content: 'Underline', value: 'underline'}, + ], + defaultValue: 'normal', + }, + { + title: 'Target', + type: 'select', + name: 'textContent.links[{{index1}}].target', + options: [{value: '_blank'}, {value: '_self'}, {value: '_parent'}, {value: '_top'}], + hasClear: true, + }, + { + type: 'section', + title: 'Analytics tracking', + itemView: 'clear', + fields: [ + { + title: 'Name', + type: 'textInput', + name: 'textContent.links[{{index1}}].analyticsEvents[0].name', + placeholder: 'Text', + }, + { + title: 'Target', + type: 'textInput', + name: 'textContent.links[{{index1}}].analyticsEvents[0].target', + placeholder: 'Text', + }, + { + title: 'Counter', + type: 'textInput', + name: 'textContent.links[{{index1}}].analyticsEvents[0].counters[0].includes', + placeholder: 'Text', + }, + ], + }, + ], + }, + { + type: 'section', + title: 'File', + withAddButton: true, + index: 'index1', + itemTitle: 'File {{index1}}', + itemView: 'card', + fields: [ + { + type: 'textInput', + title: 'Href', + name: 'fileContent[{{index1}}].href', + placeholder: 'https://', + }, + { + type: 'textInput', + title: 'Name', + name: 'fileContent[{{index1}}].text', + placeholder: 'Text', + }, + ], + }, + { + type: 'section', + title: 'Background', + opened: true, + fields: [ + { + title: 'Color HEX', + type: 'colorInput', + name: 'background.style.background', + placeholder: '#000000', + }, + { + type: 'text', + text: 'Light theme', + }, + { + title: 'Desktop', + type: 'textInput', + name: 'background.light.image.desktop', + placeholder: 'https://', + }, + { + title: 'Tablet', + type: 'textInput', + name: 'background.light.image.tablet', + placeholder: 'https://', + }, + { + title: 'Mobile', + type: 'textInput', + name: 'background.light.image.mobile', + placeholder: 'https://', + }, + { + type: 'text', + text: 'Dark theme', + }, + { + title: 'Desktop', + type: 'textInput', + name: 'background.dark.image.desktop', + placeholder: 'https://', + }, + { + title: 'Tablet', + type: 'textInput', + name: 'background.dark.image.tablet', + placeholder: 'https://', + }, + { + title: 'Mobile', + type: 'textInput', + name: 'background.dark.image.mobile', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + ], + }, +] as Fields; + +export const defaultValue = { + textContent: { + title: 'Lorem ipsum dolor sit amet', + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + additionalInfo: + 'Duis aute irure dolor in reprehenderit n voluptate velit esse cillum dolore eu fugiat nulla pariatur.', + buttons: [ + { + text: 'Button', + theme: 'action', + url: 'https://example.com', + }, + { + text: 'Button', + theme: 'outlined', + url: 'https://example.com', + }, + ], + links: [ + { + url: 'https://example.com', + text: 'Link', + theme: 'normal', + arrow: true, + }, + ], + fileContent: [ + { + href: 'https://example.xls', + text: 'File xls', + }, + { + href: 'https://example.fig', + text: 'File PNG, JPG, and SVG format', + }, + { + href: 'https://example.pdf', + text: 'Pdf file', + }, + { + href: 'https://example.zip', + text: 'Archive file', + }, + { + href: 'https://example.doc', + text: 'Microsoft Word document', + }, + { + href: 'https://example.ppt', + text: 'PPT file', + }, + ], + list: [ + { + title: 'Lorem ipsum', + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + { + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + { + title: 'Lorem ipsum ipsum', + }, + ], +}; diff --git a/src/blocks/ContentLayout/icon.ts b/src/blocks/ContentLayout/icon.ts new file mode 100644 index 0000000000..203cd672cf --- /dev/null +++ b/src/blocks/ContentLayout/icon.ts @@ -0,0 +1,15 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + ` + + + + + + + + + +`, +); diff --git a/src/blocks/ContentLayout/index.ts b/src/blocks/ContentLayout/index.ts new file mode 100644 index 0000000000..1ec505f020 --- /dev/null +++ b/src/blocks/ContentLayout/index.ts @@ -0,0 +1,17 @@ +import ContentLayoutBlock from './ContentLayout'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const ContentLayoutBlockConfig = { + type: '@gravity-ui/page-constructor/content-layout-block', + component: ContentLayoutBlock, + schema: { + name: 'Content Layout Block', + group: '@gravity-ui/page-constructor/Blocks', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default ContentLayoutBlockConfig; diff --git a/src/blocks/ContentLayout/index_deprecated.ts b/src/blocks/ContentLayout/index_deprecated.ts new file mode 100644 index 0000000000..7939ef002b --- /dev/null +++ b/src/blocks/ContentLayout/index_deprecated.ts @@ -0,0 +1,16 @@ +import ContentLayoutBlock from './ContentLayout'; +import {defaultValue, form} from './form'; + +const ContentLayoutBlockConfig = { + type: 'content-layout-block', + component: ContentLayoutBlock, + schema: { + name: 'Content Layout Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + }, +}; + +export default ContentLayoutBlockConfig; diff --git a/src/blocks/ContentLayout/schema.ts b/src/blocks/ContentLayout/schema.ts index 20279e55ad..6f01c276f3 100644 --- a/src/blocks/ContentLayout/schema.ts +++ b/src/blocks/ContentLayout/schema.ts @@ -5,8 +5,8 @@ import { contentSizes, contentTextWidth, contentThemes, -} from '../../schema/validators/common'; -import {filteredArray} from '../../schema/validators/utils'; +} from '../../gravity-blocks/schema/validators/common'; +import {filteredArray} from '../../gravity-blocks/schema/validators/utils'; import {ContentBlock} from '../../sub-blocks/Content/schema'; const ContentLayoutBlockProperties = { diff --git a/src/blocks/ExtendedFeatures/ExtendedFeatures.tsx b/src/blocks/ExtendedFeatures/ExtendedFeatures.tsx index bc5e0b8fd8..925cf8cf55 100644 --- a/src/blocks/ExtendedFeatures/ExtendedFeatures.tsx +++ b/src/blocks/ExtendedFeatures/ExtendedFeatures.tsx @@ -1,8 +1,8 @@ import {AnimateBlock, Title, YFMWrapper} from '../../components/'; import Image from '../../components/Image/Image'; import {getMediaImage} from '../../components/Media/Image/utils'; -import {useTheme} from '../../context/theme'; -import {Col, Row} from '../../grid'; +import {useTheme} from '../../gravity-blocks/context/theme'; +import {Col, Grid, Row} from '../../gravity-blocks/grid'; import {ExtendedFeaturesProps} from '../../models'; import {Content} from '../../sub-blocks'; import {block, getThemedValue} from '../../utils'; @@ -29,69 +29,78 @@ export const ExtendedFeaturesBlock = ({ return ( - - <div className={b('items')}> - <Row> - {items.map( - ({ - title: itemTitle, - text, - list, - link, - links, - label, - icon, - buttons, - additionalInfo, - }) => { - const itemLinks = links || []; + <Grid> + <Title title={title} subtitle={description} className={b('header')} /> + <div className={b('items')}> + <Row> + {items && + items.map( + ({ + title: itemTitle, + text, + list, + link, + links, + label, + icon, + buttons, + additionalInfo, + }) => { + const itemLinks = links || []; - const iconThemed = icon && getThemedValue(icon, theme); - const iconData = iconThemed && getMediaImage(iconThemed); + const iconThemed = icon && getThemedValue(icon, theme); + const iconData = iconThemed && getMediaImage(iconThemed); - if (link) { - itemLinks.push(link); - } + if (link) { + itemLinks.push(link); + } - return ( - <Col className={b('item')} key={text || itemTitle} sizes={colSizes}> - {iconData && ( - <div className={b('icon-wrap')} aria-hidden> - <Image {...iconData} className={b('icon')} /> - </div> - )} - <div className={b('container')}> - {itemTitle && ( - <YFMWrapper - tagName={itemTitleHeadingTag} - content={itemTitle} - className={b('item-title-container')} - contentClassName={b('item-title')} - modifiers={{ - constructor: true, - }} - > - {label && ( - <span className={b('item-label')}>{label}</span> + return ( + <Col + className={b('item')} + key={text || itemTitle} + sizes={colSizes} + > + {iconData && ( + <div className={b('icon-wrap')} aria-hidden> + <Image {...iconData} className={b('icon')} /> + </div> + )} + <div className={b('container')}> + {itemTitle && ( + <YFMWrapper + tagName={itemTitleHeadingTag} + content={itemTitle} + className={b('item-title-container')} + contentClassName={b('item-title')} + modifiers={{ + constructor: true, + }} + > + {label && ( + <span className={b('item-label')}> + {label} + </span> + )} + </YFMWrapper> )} - </YFMWrapper> - )} - <Content - text={text} - links={itemLinks} - size="s" - list={list} - colSizes={{all: 12, md: 12}} - buttons={buttons} - additionalInfo={additionalInfo} - /> - </div> - </Col> - ); - }, - )} - </Row> - </div> + <Content + text={text} + links={itemLinks} + size="s" + list={list} + colSizes={{all: 12, md: 12}} + buttons={buttons} + additionalInfo={additionalInfo} + /> + </div> + </Col> + ); + }, + )} + </Row> + </div> + </Grid> </AnimateBlock> ); }; diff --git a/src/blocks/ExtendedFeatures/__stories__/ExtendedFeatures.stories.tsx b/src/blocks/ExtendedFeatures/__stories__/ExtendedFeatures.stories.tsx index 8fe143e66d..64ce80b63f 100644 --- a/src/blocks/ExtendedFeatures/__stories__/ExtendedFeatures.stories.tsx +++ b/src/blocks/ExtendedFeatures/__stories__/ExtendedFeatures.stories.tsx @@ -3,12 +3,16 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {ExtendedFeaturesBlockModel, ExtendedFeaturesProps} from '../../../models'; import ExtendedFeatures, {ExtendedFeaturesBlock} from '../ExtendedFeatures'; +import {form} from '../form'; import data from './data.json'; export default { component: ExtendedFeatures, title: 'Blocks/ExtendedFeatures', + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<ExtendedFeaturesBlockModel> = (args) => { diff --git a/src/blocks/ExtendedFeatures/form.ts b/src/blocks/ExtendedFeatures/form.ts new file mode 100644 index 0000000000..da0d7ecd13 --- /dev/null +++ b/src/blocks/ExtendedFeatures/form.ts @@ -0,0 +1,73 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {ExtendedFeaturesBlock as ExtendedFeaturesBlockSchema} from './schema'; + +export const form = generateFormFieldsFromAjvSchema( + ExtendedFeaturesBlockSchema['extended-features-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + type: 'extended-features-block', + title: { + text: 'Lorem ipsum dolor sit amet', + textSize: 'm', + }, + description: + 'Three cards in a row on the desktop, two cards in a row on a tablet, one card in a row on a mobile phone.', + items: [ + { + title: 'Sed do eiusmod tempor incididunt', + text: 'Ut enim ad minim veniam quis nostrud ullamco laboris nisi ut aliquip ex ea commodo consequat.', + additionalInfo: + 'Duis aute irure dolor in reprehenderit n voluptate velit esse cillum dolore eu fugiat nulla pariatur.', + }, + { + title: 'Sed do eiusmod tempor', + text: 'Ut enim ad minim veniam quis nostrud ullamco laboris nisi ut aliquip ex ea commodo consequat.', + buttons: [ + { + text: 'Button', + theme: 'action', + url: 'https://example.com', + }, + { + text: 'Button', + theme: 'outlined', + url: 'https://example.com', + }, + ], + }, + { + title: 'Sed do eiusmod tempor incididunt', + text: 'Ut enim ad minim veniam quis nostrud ullamco laboris nisi ut aliquip ex ea commodo consequat.', + links: [ + { + text: 'Go', + url: '#', + arrow: true, + theme: 'normal', + }, + ], + }, + { + title: 'Sed do eiusmod tempor incididunt', + text: 'Ut enim ad minim veniam quis nostrud ullamco laboris nisi ut aliquip ex ea commodo consequat.', + list: [ + { + title: 'Lorem ipsum', + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + { + title: 'Lorem ipsum ipsum', + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + { + title: 'Lorem ipsum', + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + ], + }, + ], +}; diff --git a/src/blocks/ExtendedFeatures/icon.ts b/src/blocks/ExtendedFeatures/icon.ts new file mode 100644 index 0000000000..3a401fff33 --- /dev/null +++ b/src/blocks/ExtendedFeatures/icon.ts @@ -0,0 +1,20 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.97674" fill="white"/> +<rect x="9.11627" y="14.5" width="51" height="5" rx="0.569767" fill="#222222"/> +<rect x="9.11627" y="21.5" width="4" height="4" rx="2" fill="#C0C8DB"/> +<rect x="9.11627" y="26.5" width="21" height="3" rx="0.569767" fill="#222222"/> +<rect x="9.11627" y="30.5" width="25.9225" height="2" rx="0.569767" fill="#222222"/> +<rect x="9.11627" y="33.5" width="16" height="2" rx="0.569767" fill="#222222"/> +<rect x="37.0388" y="21.5" width="4" height="4" rx="2" fill="#C0C8DB"/> +<rect x="37.0388" y="26.5" width="14" height="3" rx="0.569767" fill="#222222"/> +<rect x="37.0388" y="30.5" width="25.9225" height="2" rx="0.569767" fill="#222222"/> +<rect x="37.0388" y="33.5" width="20" height="2" rx="0.569767" fill="#222222"/> +<rect x="64.9612" y="21.5" width="4" height="4" rx="2" fill="#C0C8DB"/> +<rect x="64.9612" y="26.5" width="25" height="3" rx="0.569767" fill="#222222"/> +<rect x="64.9612" y="30.5" width="25.9225" height="2" rx="0.569767" fill="#222222"/> +<rect x="64.9612" y="33.5" width="8" height="2" rx="0.569767" fill="#222222"/> +</svg>`, +); diff --git a/src/blocks/ExtendedFeatures/index.ts b/src/blocks/ExtendedFeatures/index.ts new file mode 100644 index 0000000000..e11e5b1294 --- /dev/null +++ b/src/blocks/ExtendedFeatures/index.ts @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import ExtendedFeaturesBlock from './ExtendedFeatures'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const ExtendedFeaturesBlockConfig: BlockData = { + type: '@gravity-ui/page-constructor/extended-features-block', + component: ExtendedFeaturesBlock, + schema: { + name: 'Extended Features Block', + group: '@gravity-ui/page-constructor/Blocks', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default ExtendedFeaturesBlockConfig; diff --git a/src/blocks/ExtendedFeatures/index_deprecated.ts b/src/blocks/ExtendedFeatures/index_deprecated.ts new file mode 100644 index 0000000000..b6df6f4ef6 --- /dev/null +++ b/src/blocks/ExtendedFeatures/index_deprecated.ts @@ -0,0 +1,18 @@ +import ExtendedFeaturesBlock from './ExtendedFeatures'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const ExtendedFeaturesBlockConfig = { + type: 'extended-features-block', + component: ExtendedFeaturesBlock, + schema: { + name: 'Extended Features Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default ExtendedFeaturesBlockConfig; diff --git a/src/blocks/ExtendedFeatures/schema.ts b/src/blocks/ExtendedFeatures/schema.ts index a7705dfc17..3ca42cee58 100644 --- a/src/blocks/ExtendedFeatures/schema.ts +++ b/src/blocks/ExtendedFeatures/schema.ts @@ -5,9 +5,9 @@ import { LinkProps, containerSizesObject, withTheme, -} from '../../schema/validators/common'; -import {ImageProps} from '../../schema/validators/components'; -import {filteredArray} from '../../schema/validators/utils'; +} from '../../gravity-blocks/schema/validators/common'; +import {ImageProps} from '../../gravity-blocks/schema/validators/components'; +import {filteredArray} from '../../gravity-blocks/schema/validators/utils'; export const ExtendedFeaturesItem = { additionalProperties: false, @@ -20,6 +20,7 @@ export const ExtendedFeaturesItem = { text: { type: 'string', contentType: 'yfm', + inputType: 'textarea', }, label: { type: 'string', diff --git a/src/blocks/FilterBlock/FilterBlock.tsx b/src/blocks/FilterBlock/FilterBlock.tsx index 8eb4a81ed4..8d2f1c587d 100644 --- a/src/blocks/FilterBlock/FilterBlock.tsx +++ b/src/blocks/FilterBlock/FilterBlock.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; -import {CardLayoutBlock} from '..'; import {AnimateBlock, Title} from '../../components'; import ButtonTabs, {ButtonTabsItemProps} from '../../components/ButtonTabs/ButtonTabs'; import {ConstructorItem} from '../../containers/PageConstructor/components/ConstructorItem'; -import {Col, Row} from '../../grid'; -import {useAnalytics} from '../../hooks'; +import {Col, Grid, Row} from '../../gravity-blocks/grid'; +import {useAnalytics} from '../../gravity-blocks/hooks'; import {FilterBlockProps, FilterItem} from '../../models'; import {block, getBlockKey} from '../../utils'; +import {default as CardLayoutBlock} from '../CardLayout/CardLayout'; import {i18n} from './i18n'; @@ -21,7 +21,7 @@ const FilterBlock = ({ tags, tagButtonSize, allTag, - items, + items = [], colSizes, centered, animated, @@ -90,35 +90,36 @@ const FilterBlock = ({ return ( <AnimateBlock className={b()} animate={animated}> - {title && ( - <Title - className={b('title', {centered: centered})} - title={title} - subtitle={description} - /> - )} - {tabButtons.length && ( - <Row> - <Col> - <ButtonTabs - className={b('tabs', {centered: centered})} - items={tabButtons} - activeTab={selectedTag} - onSelectTab={handleSelectTab} - tabSize={tagButtonSize} - /> - </Col> + <Grid> + {title && ( + <Title + className={b('title', {centered: centered})} + title={title} + subtitle={description} + /> + )} + {tabButtons.length && ( + <Row> + <Col> + <ButtonTabs + className={b('tabs', {centered: centered})} + items={tabButtons} + activeTab={selectedTag} + onSelectTab={handleSelectTab} + tabSize={tagButtonSize} + /> + </Col> + </Row> + )} + <Row className={b('block-container')}> + <CardLayoutBlock title="" colSizes={colSizes} className={b('cards-container')}> + {cards.map((card, index) => { + const key = getBlockKey(card, index); + return <ConstructorItem data={card} blockKey={index} key={key} />; + })} + </CardLayoutBlock> </Row> - )} - <Row className={b('block-container')}> - <CardLayoutBlock title="" colSizes={colSizes} className={b('cards-container')}> - {cards.map((card, index) => { - const key = getBlockKey(card, index); - - return <ConstructorItem data={card} blockKey={key} key={key} />; - })} - </CardLayoutBlock> - </Row> + </Grid> </AnimateBlock> ); }; diff --git a/src/blocks/FilterBlock/__stories__/FilterBlock.stories.tsx b/src/blocks/FilterBlock/__stories__/FilterBlock.stories.tsx index 262bc07ed9..716fa513be 100644 --- a/src/blocks/FilterBlock/__stories__/FilterBlock.stories.tsx +++ b/src/blocks/FilterBlock/__stories__/FilterBlock.stories.tsx @@ -4,6 +4,7 @@ import {blockTransform} from '../../../../.storybook/utils'; import {PageConstructor} from '../../../containers/PageConstructor'; import {FilterBlockModel} from '../../../models'; import FilterBlock from '../FilterBlock'; +import {form} from '../form'; import data from './data.json'; @@ -47,6 +48,9 @@ export default { }, }, }, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<FilterBlockModel> = (args) => ( diff --git a/src/blocks/FilterBlock/form.ts b/src/blocks/FilterBlock/form.ts new file mode 100644 index 0000000000..8a6ad213e6 --- /dev/null +++ b/src/blocks/FilterBlock/form.ts @@ -0,0 +1,92 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {FilterProps} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema(FilterProps as unknown as JSONSchemaType<{}>); + +export const defaultValue = { + allTag: true, + description: + 'Three cards in a row on the desktop, two cards in a row on a tablet, one card in a row on a mobile phone.', + items: [ + { + card: { + content: { + title: 'Layout Item 1', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, + type: 'layout-item', + }, + tags: ['one'], + }, + { + card: { + content: { + title: 'Layout Item 2', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, + type: 'layout-item', + }, + tags: ['two'], + }, + { + card: { + content: { + title: 'Layout Item 3', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, + type: 'layout-item', + }, + tags: ['three'], + }, + { + card: { + content: { + title: 'Layout Item 4', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, + type: 'layout-item', + }, + tags: ['one'], + }, + { + card: { + content: { + title: 'Layout Item 5', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, + type: 'layout-item', + }, + tags: ['two'], + }, + { + card: { + content: { + title: 'Layout Item 6', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, + type: 'layout-item', + }, + tags: ['three'], + }, + ], + tags: [ + { + id: 'one', + label: 'First very long label', + }, + { + id: 'two', + label: 'Second very long label', + }, + { + id: 'three', + label: 'Third very long label', + }, + ], + title: 'Card Layout', + type: 'filter-block', +}; diff --git a/src/blocks/FilterBlock/index.ts b/src/blocks/FilterBlock/index.ts new file mode 100644 index 0000000000..3add3dbe5e --- /dev/null +++ b/src/blocks/FilterBlock/index.ts @@ -0,0 +1,15 @@ +import FilterBlock from './FilterBlock'; +import {defaultValue, form} from './form'; + +const FilterBlockConfig = { + type: '@gravity-ui/page-constructor/filter-block', + component: FilterBlock, + schema: { + name: 'Filter Block', + group: '@gravity-ui/page-constructor/UnfinishedBlocks', + inputs: form, + default: defaultValue, + }, +}; + +export default FilterBlockConfig; diff --git a/src/blocks/FilterBlock/index_deprecated.ts b/src/blocks/FilterBlock/index_deprecated.ts new file mode 100644 index 0000000000..6e0e6a0e64 --- /dev/null +++ b/src/blocks/FilterBlock/index_deprecated.ts @@ -0,0 +1,16 @@ +import FilterBlock from './FilterBlock'; +import {defaultValue, form} from './form'; + +const FilterBlockConfig = { + type: 'filter-block', + component: FilterBlock, + schema: { + name: 'Filter Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + }, +}; + +export default FilterBlockConfig; diff --git a/src/blocks/FilterBlock/schema.ts b/src/blocks/FilterBlock/schema.ts index eafe81483c..66db8f50c8 100644 --- a/src/blocks/FilterBlock/schema.ts +++ b/src/blocks/FilterBlock/schema.ts @@ -3,9 +3,9 @@ import { BlockBaseProps, BlockHeaderProps, containerSizesObject, -} from '../../schema/validators/common'; -import {AnalyticsEventSchema} from '../../schema/validators/event'; -import {filteredArray} from '../../schema/validators/utils'; +} from '../../gravity-blocks/schema/validators/common'; +import {AnalyticsEventSchema} from '../../gravity-blocks/schema/validators/event'; +import {filteredArray} from '../../gravity-blocks/schema/validators/utils'; export const FilterTagProps = { type: 'object', diff --git a/src/blocks/FoldableList/FoldableList.tsx b/src/blocks/FoldableList/FoldableList.tsx index 86dfafd465..2ad23ea8db 100644 --- a/src/blocks/FoldableList/FoldableList.tsx +++ b/src/blocks/FoldableList/FoldableList.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {Col, Row} from '../../grid'; +import {Col, Row} from '../../gravity-blocks/grid'; import {FoldableListProps} from '../../models'; import {Content} from '../../sub-blocks'; import {block} from '../../utils'; diff --git a/src/blocks/FoldableList/__stories__/FoldableList.stories.tsx b/src/blocks/FoldableList/__stories__/FoldableList.stories.tsx index 490f2461df..6af6146807 100644 --- a/src/blocks/FoldableList/__stories__/FoldableList.stories.tsx +++ b/src/blocks/FoldableList/__stories__/FoldableList.stories.tsx @@ -3,12 +3,16 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {FoldableListBlockModel, FoldableListProps} from '../../../models'; import FoldableListBlock from '../FoldableList'; +import {form} from '../form'; import data from './data.json'; export default { title: 'Blocks/FoldableList', component: FoldableListBlock, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<FoldableListBlockModel> = (args) => { diff --git a/src/blocks/FoldableList/form.ts b/src/blocks/FoldableList/form.ts new file mode 100644 index 0000000000..f6cfadc8ca --- /dev/null +++ b/src/blocks/FoldableList/form.ts @@ -0,0 +1,23 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {FoldableListBlock} from './schema'; + +export const form = generateFormFieldsFromAjvSchema( + FoldableListBlock['foldable-list-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + title: 'Lorem ipsum dolor sit amet', + items: [ + { + title: 'Item 1', + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris.', + }, + { + title: 'Item 2', + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris.', + }, + ], +}; diff --git a/src/blocks/FoldableList/index.ts b/src/blocks/FoldableList/index.ts new file mode 100644 index 0000000000..3593d2cebb --- /dev/null +++ b/src/blocks/FoldableList/index.ts @@ -0,0 +1,17 @@ +import {BlockData} from '../../constructor-items'; + +import FoldableListBlock from './FoldableList'; +import {defaultValue, form} from './form'; + +const FoldableListBlockConfig: BlockData = { + type: '@gravity-ui/page-constructor/foldable-list-block', + component: FoldableListBlock, + schema: { + name: 'Foldable List Block', + group: '@gravity-ui/page-constructor/UnfinishedBlocks', + inputs: form, + default: defaultValue, + }, +}; + +export default FoldableListBlockConfig; diff --git a/src/blocks/FoldableList/index_deprecated.ts b/src/blocks/FoldableList/index_deprecated.ts new file mode 100644 index 0000000000..8f5ddc845c --- /dev/null +++ b/src/blocks/FoldableList/index_deprecated.ts @@ -0,0 +1,18 @@ +import {BlockData} from '../../constructor-items'; + +import FoldableListBlock from './FoldableList'; +import {defaultValue, form} from './form'; + +const FoldableListBlockConfig: BlockData = { + type: 'foldable-list-block', + component: FoldableListBlock, + schema: { + name: 'Foldable List Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + }, +}; + +export default FoldableListBlockConfig; diff --git a/src/blocks/FoldableList/schema.ts b/src/blocks/FoldableList/schema.ts index 57437e0516..3f6f47aae6 100644 --- a/src/blocks/FoldableList/schema.ts +++ b/src/blocks/FoldableList/schema.ts @@ -1,7 +1,7 @@ import omit from 'lodash/omit'; -import {BlockBaseProps, LinkProps} from '../../schema/validators/common'; -import {filteredArray} from '../../schema/validators/utils'; +import {BlockBaseProps, LinkProps} from '../../gravity-blocks/schema/validators/common'; +import {filteredArray} from '../../gravity-blocks/schema/validators/utils'; import {ContentBase} from '../../sub-blocks/Content/schema'; const FoldableListBlockContentProps = omit(ContentBase, ['size', 'theme']); diff --git a/src/blocks/Form/Form.tsx b/src/blocks/Form/Form.tsx index c5c20dae49..8bb3ac560a 100644 --- a/src/blocks/Form/Form.tsx +++ b/src/blocks/Form/Form.tsx @@ -2,10 +2,10 @@ import * as React from 'react'; import {BackgroundImage, Title} from '../../components'; import InnerForm from '../../components/InnerForm/InnerForm'; -import {MobileContext} from '../../context/mobileContext'; -import {useTheme} from '../../context/theme'; -import {Col, Grid, GridAlignItems, GridColumnSize, Row} from '../../grid'; -import {useDeviceValue} from '../../hooks/useDeviceValue'; +import {MobileContext} from '../../gravity-blocks/context/mobileContext'; +import {useTheme} from '../../gravity-blocks/context/theme'; +import {Col, Grid, GridAlignItems, GridColumnSize, Row} from '../../gravity-blocks/grid'; +import {useDeviceValue} from '../../gravity-blocks/hooks/useDeviceValue'; import type {FormBlockProps} from '../../models'; import { FormBlockDataTypes, diff --git a/src/blocks/Form/formConfig.ts b/src/blocks/Form/formConfig.ts new file mode 100644 index 0000000000..7d9baf35df --- /dev/null +++ b/src/blocks/Form/formConfig.ts @@ -0,0 +1,15 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {FormBlock as FormBlockSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + FormBlockSchema['form-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + title: 'Form Block', + formData: {}, +}; diff --git a/src/blocks/Form/index.ts b/src/blocks/Form/index.ts new file mode 100644 index 0000000000..5a8bda0bd8 --- /dev/null +++ b/src/blocks/Form/index.ts @@ -0,0 +1,15 @@ +import FormBlock from './Form'; +import {defaultValue, form} from './formConfig'; + +const FormBlockConfig = { + type: '@gravity-ui/page-constructor/form-block', + component: FormBlock, + schema: { + name: 'Form Block', + group: '@gravity-ui/page-constructor/UnfinishedBlocks', + inputs: form, + default: defaultValue, + }, +}; + +export default FormBlockConfig; diff --git a/src/blocks/Form/index_deprecated.ts b/src/blocks/Form/index_deprecated.ts new file mode 100644 index 0000000000..ca84172b8f --- /dev/null +++ b/src/blocks/Form/index_deprecated.ts @@ -0,0 +1,16 @@ +import FormBlock from './Form'; +import {defaultValue, form} from './formConfig'; + +const FormBlockConfig = { + type: 'form-block', + component: FormBlock, + schema: { + name: 'Form Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + }, +}; + +export default FormBlockConfig; diff --git a/src/blocks/Form/schema.ts b/src/blocks/Form/schema.ts index c971e17d27..916a8d1e75 100644 --- a/src/blocks/Form/schema.ts +++ b/src/blocks/Form/schema.ts @@ -2,7 +2,7 @@ import omit from 'lodash/omit'; import {ImageProps} from '../../components/Image/schema'; import {YandexFormProps} from '../../components/YandexForm/schema'; -import {BlockBaseProps, withTheme} from '../../schema/validators/common'; +import {BlockBaseProps, withTheme} from '../../gravity-blocks/schema/validators/common'; import {ContentBase} from '../../sub-blocks/Content/schema'; import {HubspotFormProps} from '../../sub-blocks/HubspotForm/schema'; diff --git a/src/blocks/Header/Header.tsx b/src/blocks/Header/Header.tsx index ccb9369c83..04aca193c0 100644 --- a/src/blocks/Header/Header.tsx +++ b/src/blocks/Header/Header.tsx @@ -8,9 +8,9 @@ import HeaderBreadcrumbs from '../../components/HeaderBreadcrumbs/HeaderBreadcru import {getMediaImage} from '../../components/Media/Image/utils'; import YFMWrapper from '../../components/YFMWrapper/YFMWrapper'; import {BREAKPOINTS} from '../../constants'; -import {useTheme} from '../../context/theme'; -import {useWindowWidth} from '../../context/windowWidthContext'; -import {Col, Grid, Row} from '../../grid'; +import {useTheme} from '../../gravity-blocks/context/theme'; +import {useWindowWidth} from '../../gravity-blocks/context/windowWidthContext'; +import {Col, Grid, Row} from '../../gravity-blocks/grid'; import {ClassNameProps, HeaderBlockBackground, HeaderBlockProps} from '../../models'; import {block, getThemedValue} from '../../utils'; import {mergeVideoMicrodata} from '../../utils/microdata'; diff --git a/src/blocks/Header/__stories__/Header.stories.tsx b/src/blocks/Header/__stories__/Header.stories.tsx index 90bbb5e22a..eaf03e0dd0 100644 --- a/src/blocks/Header/__stories__/Header.stories.tsx +++ b/src/blocks/Header/__stories__/Header.stories.tsx @@ -5,6 +5,7 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {HeaderBlockModel, HeaderBlockProps} from '../../../models'; import Header from '../Header'; +import {form} from '../form'; import data from './data.json'; @@ -17,6 +18,9 @@ const SIZES = ['l', 'm', 's'].map((width) => ({ export default { title: 'Blocks/Header', component: Header, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<HeaderBlockModel> = (args) => ( diff --git a/src/blocks/Header/dynamic-form.ts b/src/blocks/Header/dynamic-form.ts new file mode 100644 index 0000000000..607838e6bb --- /dev/null +++ b/src/blocks/Header/dynamic-form.ts @@ -0,0 +1,732 @@ +import {imageInputs} from '../../components/Image/dynamic-form'; +import {BlockConfig, ConfigInput} from '../../form-generator'; +import {Theme} from '../../models'; + +const mediaInputs: ConfigInput[] = [ + { + type: 'oneOf', + name: '', + key: 'mediaType', + title: 'Media Type', + options: [ + { + title: 'Empty', + value: 'emptyMedia', + properties: [ + { + type: 'text', + name: 'color', + title: 'Color', + }, + ], + }, + { + title: 'Image', + value: 'image', + properties: [ + { + type: 'oneOf', + name: 'image', + key: 'imagesCount', + title: 'Images Count', + options: [ + {title: 'Single Image', value: 'singleImage', properties: imageInputs}, + { + title: 'Multiple Images', + value: 'multipleImages', + properties: [ + { + type: 'array', + name: '', + arrayType: 'object', + title: 'Images', + properties: imageInputs, + buttonText: 'Add Image', + }, + ], + }, + ], + }, + ], + }, + { + title: 'Youtube', + value: 'youtube', + properties: [ + { + type: 'text', + name: 'youtube', + title: 'Youtube', + }, + { + type: 'text', + name: 'previewImg', + title: 'Preview Image', + }, + ], + }, + { + title: 'Datalens', + value: 'datalens', + properties: [], + }, + { + title: 'Iframe', + value: 'iframe', + properties: [], + }, + { + title: 'Video', + value: 'video', + properties: [], + }, + ], + }, + { + type: 'boolean', + name: 'disableImageSliderForArrayInput', + title: 'Disable Image Slider For Array Input', + }, + { + type: 'boolean', + name: 'parallax', + title: 'Parallax', + }, + { + type: 'number', + name: 'height', + title: 'Height', + }, + { + type: 'boolean', + name: 'fullscreen', + title: 'Fullscreen', + }, + { + type: 'number', + name: 'ratio', + title: 'Ratio', + }, + { + type: 'boolean', + name: 'margins', + title: 'Margins', + }, +]; + +const headerBackgroundInputs: ConfigInput[] = [ + { + type: 'boolean', + name: 'fullWidthMedia', + title: 'Full width Media', + }, + { + type: 'boolean', + name: 'fullWidth', + title: 'Full width', + }, +]; + +const backgroundProperty = (properties: ConfigInput[]): ConfigInput => ({ + type: 'oneOf', + name: 'background', + title: 'Background', + options: [ + { + value: 'noThemes', + title: 'No Themes', + properties: properties, + }, + { + value: 'withThemes', + title: 'With Themes', + properties: Object.values(Theme).map((theme) => ({ + type: 'object', + name: theme, + title: theme, + properties: properties, + })), + }, + ], +}); + +export const blockConfig: BlockConfig = { + name: 'Header Block', + inputs: [ + { + type: 'text', + name: 'title', + title: 'Title', + }, + { + type: 'text', + name: 'overtitle', + title: 'Overtitle', + }, + { + type: 'textarea', + name: 'description', + title: 'Description', + }, + backgroundProperty([...mediaInputs, ...headerBackgroundInputs]), + /* { + type: 'text', + name: 'when', + title: 'when', + }, + { + type: 'object', + name: 'anchor', + title: 'anchor', + properties: [ + { + type: 'text', + name: 'text', + title: 'text', + }, + { + type: 'text', + name: 'url', + title: 'url', + }, + { + type: 'text', + name: 'urlTitle', + title: 'urlTitle', + }, + ], + }, + { + type: 'select', + name: 'visible', + title: 'visible', + enum: [ + { + content: 'sm', + value: 'sm', + }, + { + content: 'md', + value: 'md', + }, + { + content: 'lg', + value: 'lg', + }, + { + content: 'xl', + value: 'xl', + }, + { + content: 'all', + value: 'all', + }, + ], + }, + { + type: 'text', + name: 'context', + title: 'context', + }, + { + type: 'object', + name: 'indent', + title: 'indent', + properties: [], + }, + { + type: 'select', + name: 'width', + title: 'width', + enum: [ + { + content: 's', + value: 's', + }, + { + content: 'm', + value: 'm', + }, + { + content: 'l', + value: 'l', + }, + ], + }, + { + type: 'array', + name: 'buttons', + title: 'buttons', + properties: [ + { + type: 'text', + name: 'when', + title: 'when', + }, + { + type: 'text', + name: 'text', + title: 'text', + }, + { + type: 'text', + name: 'url', + title: 'url', + }, + { + type: 'text', + name: 'urlTitle', + title: 'urlTitle', + }, + { + type: 'select', + name: 'size', + title: 'size', + enum: [ + { + content: 'xs', + value: 'xs', + }, + { + content: 'ns', + value: 'ns', + }, + { + content: 's', + value: 's', + }, + { + content: 'n', + value: 'n', + }, + { + content: 'm', + value: 'm', + }, + { + content: 'l', + value: 'l', + }, + { + content: 'xl', + value: 'xl', + }, + { + content: 'head', + value: 'head', + }, + { + content: 'promo', + value: 'promo', + }, + ], + }, + { + type: 'select', + name: 'theme', + title: 'theme', + enum: [ + { + content: 'normal', + value: 'normal', + }, + { + content: 'action', + value: 'action', + }, + { + content: 'outlined', + value: 'outlined', + }, + { + content: 'outlined-info', + value: 'outlined-info', + }, + { + content: 'outlined-danger', + value: 'outlined-danger', + }, + { + content: 'raised', + value: 'raised', + }, + { + content: 'flat', + value: 'flat', + }, + { + content: 'flat-info', + value: 'flat-info', + }, + { + content: 'flat-danger', + value: 'flat-danger', + }, + { + content: 'flat-secondary', + value: 'flat-secondary', + }, + { + content: 'clear', + value: 'clear', + }, + { + content: 'normal-contrast', + value: 'normal-contrast', + }, + { + content: 'outlined-contrast', + value: 'outlined-contrast', + }, + { + content: 'flat-contrast', + value: 'flat-contrast', + }, + { + content: 'link', + value: 'link', + }, + { + content: 'pseudo', + value: 'pseudo', + }, + { + content: 'pseudo-special', + value: 'pseudo-special', + }, + { + content: 'websearch', + value: 'websearch', + }, + { + content: 'normal-dark', + value: 'normal-dark', + }, + { + content: 'normal-special', + value: 'normal-special', + }, + { + content: 'accent', + value: 'accent', + }, + { + content: 'dark-grey', + value: 'dark-grey', + }, + { + content: 'app-store', + value: 'app-store', + }, + { + content: 'google-play', + value: 'google-play', + }, + { + content: 'scale', + value: 'scale', + }, + { + content: 'github', + value: 'github', + }, + { + content: 'monochrome', + value: 'monochrome', + }, + ], + }, + { + type: 'oneOf', + name: 'img', + title: 'img', + options: [ + { + value: 'url', + title: 'url', + properties: [], + }, + { + value: 'options', + title: 'options', + properties: [], + }, + ], + }, + { + type: 'oneOf', + name: 'analyticsEvents', + title: 'analyticsEvents', + options: [ + { + value: 'single', + title: 'single', + properties: [ + { + type: 'text', + name: 'additionalProperties', + title: 'additionalProperties', + }, + ], + }, + { + value: 'list', + title: 'list', + properties: [ + { + type: 'object', + name: 'items', + title: 'items', + properties: [ + { + type: 'text', + name: 'name', + title: 'name', + }, + { + type: 'text', + name: 'type', + title: 'type', + }, + { + type: 'object', + name: 'counters', + title: 'counters', + properties: [], + }, + { + type: 'text', + name: 'context', + title: 'context', + }, + ], + }, + ], + }, + ], + }, + { + type: 'select', + name: 'target', + title: 'target', + enum: [ + { + content: '_self', + value: '_self', + }, + { + content: '_blank', + value: '_blank', + }, + { + content: '_parent', + value: '_parent', + }, + { + content: '_top', + value: '_top', + }, + ], + }, + { + type: 'select', + name: 'width', + title: 'width', + enum: [ + { + content: 'auto', + value: 'auto', + }, + { + content: 'max', + value: 'max', + }, + ], + }, + ], + }, + { + type: 'select', + name: 'offset', + title: 'offset', + enum: [ + { + content: 'default', + value: 'default', + }, + { + content: 'large', + value: 'large', + }, + ], + }, + { + type: 'oneOf', + name: 'image', + title: 'image', + options: [ + { + value: 'no theme', + title: 'no theme', + properties: [], + }, + { + value: 'themes', + title: 'themes', + properties: [], + }, + ], + }, + { + type: 'oneOf', + name: 'video', + title: 'video', + options: [ + { + value: 'no theme', + title: 'no theme', + properties: [], + }, + { + value: 'themes', + title: 'themes', + properties: [], + }, + ], + }, + { + type: 'select', + name: 'mediaView', + title: 'mediaView', + enum: [ + { + content: 'fit', + value: 'fit', + }, + { + content: 'full', + value: 'full', + }, + ], + }, + { + type: 'object', + name: 'backLink', + title: 'backLink', + properties: [ + { + type: 'text', + name: 'url', + title: 'url', + }, + { + type: 'text', + name: 'title', + title: 'title', + }, + ], + }, + { + type: 'select', + name: 'imageSize', + title: 'imageSize', + enum: [ + { + content: 's', + value: 's', + }, + { + content: 'm', + value: 'm', + }, + ], + }, + { + type: 'select', + name: 'verticalOffset', + title: 'verticalOffset', + enum: [ + { + content: '0', + value: '0', + }, + { + content: 's', + value: 's', + }, + { + content: 'm', + value: 'm', + }, + { + content: 'l', + value: 'l', + }, + { + content: 'xl', + value: 'xl', + }, + ], + }, + + { + type: 'select', + name: 'theme', + title: 'theme', + enum: [ + { + content: 'default', + value: 'default', + }, + { + content: 'dark', + value: 'dark', + }, + ], + }, + { + type: 'object', + name: 'breadcrumbs', + title: 'breadcrumbs', + properties: [ + { + type: 'array', + name: 'items', + title: 'items', + properties: [ + { + type: 'text', + name: 'url', + title: 'url', + }, + { + type: 'text', + name: 'text', + title: 'text', + }, + ], + }, + { + type: 'select', + name: 'theme', + title: 'theme', + enum: [ + { + content: 'light', + value: 'light', + }, + { + content: 'dark', + value: 'dark', + }, + ], + }, + ], + }, + { + type: 'text', + name: 'status', + title: 'status', + },*/ + ], +}; diff --git a/src/blocks/Header/form.ts b/src/blocks/Header/form.ts new file mode 100644 index 0000000000..6cd5224558 --- /dev/null +++ b/src/blocks/Header/form.ts @@ -0,0 +1,386 @@ +import {Fields} from '../../form-generator-v2/types'; + +export const form = [ + { + type: 'section', + title: 'Layout settings', + opened: true, + fields: [ + { + title: 'Vertical offset', + name: 'verticalOffset', + type: 'select', + options: [ + {content: '0', value: '0'}, + {content: 'S', value: 's'}, + {content: 'M', value: 'm'}, + {content: 'L', value: 'l'}, + {content: 'XL', value: 'xl'}, + ], + defaultValue: 'm', + }, + ], + }, + { + type: 'section', + title: 'Breadcrumbs', + withAddButton: true, + index: 'index', + itemTitle: 'Item {{index}}', + itemView: 'card', + fields: [ + { + title: 'Text', + name: 'breadcrumbs.items[{{index}}].text', + type: 'textInput', + placeholder: 'Text', + }, + { + title: 'URL', + name: 'breadcrumbs.items[{{index}}].url', + type: 'textInput', + placeholder: 'https://', + }, + ], + }, + { + type: 'section', + title: 'Text', + opened: true, + fields: [ + { + title: 'Overtitle', + name: 'overtitle', + type: 'textInput', + placeholder: 'Text', + }, + { + title: 'Title', + name: 'title', + type: 'textInput', + placeholder: 'Text', + }, + { + title: 'Description', + name: 'description', + type: 'textArea', + placeholder: 'Text', + }, + { + title: 'Width', + name: 'width', + options: [ + {content: 'S', value: 's'}, + {content: 'M', value: 'm'}, + ], + type: 'select', + }, + { + title: 'Theme', + name: 'theme', + options: [ + {content: 'Light', value: 'light'}, + {content: 'Dark', value: 'dark'}, + ], + type: 'segmentedRadioGroup', + defaultValue: 'light', + }, + ], + }, + { + type: 'section', + title: 'Buttons', + withAddButton: true, + index: 'index', + itemTitle: 'Button {{index}}', + itemView: 'card', + fields: [ + { + type: 'section', + title: 'Main settings', + opened: true, + fields: [ + { + title: 'Text', + type: 'textInput', + name: 'buttons[{{index}}].text', + placeholder: 'Text', + }, + { + title: 'URL', + type: 'textInput', + name: 'buttons[{{index}}].url', + placeholder: 'https://', + }, + { + title: 'URL title', + type: 'textInput', + name: 'buttons[{{index}}].urlTitle', + placeholder: 'https://', + }, + { + title: 'Style', + type: 'select', + name: 'buttons[{{index}}].theme', + options: [ + {value: 'action', content: 'Action'}, + {value: 'outlined', content: 'Outlined'}, + {value: 'normal', content: 'Normal'}, + {value: 'monochrome', content: 'Monochrome'}, + { + value: 'outlined-contrast', + content: 'Outlined-contrast', + }, + {value: 'normal-contrast', content: 'Normal-contrast'}, + ], + }, + { + title: 'Target', + type: 'select', + name: 'buttons[{{index}}].target', + options: [ + {value: '_blank'}, + {value: '_self'}, + {value: '_parent'}, + {value: '_top'}, + ], + hasClear: true, + }, + ], + }, + { + type: 'section', + title: 'Analytics tracking', + itemView: 'clear', + fields: [ + { + title: 'Name', + type: 'textInput', + name: 'buttons[{{index}}].analyticsEvents[0].name', + placeholder: 'Text', + }, + { + title: 'Target', + type: 'textInput', + name: 'buttons[{{index}}].analyticsEvents[0].target', + placeholder: 'Text', + }, + { + title: 'Counter', + type: 'textInput', + name: 'buttons[{{index}}].analyticsEvents[0].counters[0].includes', + placeholder: 'Text', + }, + { + type: 'text', + text: 'Only events for the counters listed in the input field will be sent.', + level: 'info', + }, + ], + }, + ], + }, + { + type: 'section', + title: 'Background', + opened: true, + fields: [ + { + title: 'Color HEX', + type: 'colorInput', + name: 'background.color', + placeholder: '#000000', + }, + { + title: 'Type', + name: '_mediaType', + options: [ + {content: 'Image', value: 'image'}, + {content: 'Video', value: 'video'}, + ], + type: 'segmentedRadioGroup', + }, + { + type: 'text', + text: 'Light theme', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + title: 'Desktop', + type: 'textInput', + name: 'background.light.image.desktop', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + title: 'Tablet', + type: 'textInput', + name: 'background.light.image.tablet', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + title: 'Mobile', + type: 'textInput', + name: 'background.light.image.mobile', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + type: 'text', + text: 'Dark theme', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + title: 'Desktop', + type: 'textInput', + name: 'background.dark.image.desktop', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + title: 'Tablet', + type: 'textInput', + name: 'background.dark.image.tablet', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + title: 'Mobile', + type: 'textInput', + name: 'background.dark.image.mobile', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + title: 'Type', + type: 'select', + options: [{value: 'default'}, {value: 'player'}], + name: 'background.video.type', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'video', + }, + ], + }, + { + title: 'Video URL', + type: 'textInput', + name: 'background.video.src[0]', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'video', + }, + ], + }, + { + title: 'Muted', + type: 'switch', + name: 'background.video.muted', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'video', + }, + ], + }, + { + title: 'Autoplay', + type: 'switch', + name: 'background.video.autoplay', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'video', + }, + ], + }, + { + title: 'Loop', + type: 'switch', + name: 'background.video.loop', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'video', + }, + ], + }, + ], + }, +] as Fields; + +export const defaultValue = { + type: 'header-block', + title: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', + description: + 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.', + buttons: [ + { + text: 'Button\r', + theme: 'action', + url: 'https://example.com', + }, + { + text: 'Button', + theme: 'outlined', + url: 'https://example.com', + }, + ], +}; diff --git a/src/blocks/Header/icon.ts b/src/blocks/Header/icon.ts new file mode 100644 index 0000000000..e9e056bee3 --- /dev/null +++ b/src/blocks/Header/icon.ts @@ -0,0 +1,13 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.97674" fill="white"/> +<rect x="9.11627" y="10.7559" width="81.7674" height="28.4884" rx="3" fill="#262626"/> +<rect x="13.6744" y="19" width="13" height="2" rx="0.569767" fill="#C0C8DB"/> +<rect x="13.6744" y="22" width="31.7674" height="2" rx="0.569767" fill="#C0C8DB"/> +<rect x="13.6744" y="25" width="31.7674" height="2" rx="0.569767" fill="#C0C8DB"/> +<rect x="13.6744" y="29" width="7" height="2" rx="0.569767" fill="#C0C8DB"/> +<rect x="50.6" y="11.3559" width="39.6837" height="27.2884" rx="2.8186" fill="#C0C8DB" stroke="#262626" stroke-width="1.2"/> +</svg>`, +); diff --git a/src/blocks/Header/index.ts b/src/blocks/Header/index.ts new file mode 100644 index 0000000000..e96b1ea6a4 --- /dev/null +++ b/src/blocks/Header/index.ts @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import HeaderBlock from './Header'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const HeaderBlockConfig: BlockData = { + type: '@gravity-ui/page-constructor/header-block', + component: HeaderBlock, + schema: { + name: 'Header Block', + group: '@gravity-ui/page-constructor/Blocks', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default HeaderBlockConfig; diff --git a/src/blocks/Header/index_deprecated.ts b/src/blocks/Header/index_deprecated.ts new file mode 100644 index 0000000000..3f17bfbe44 --- /dev/null +++ b/src/blocks/Header/index_deprecated.ts @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import HeaderBlock from './Header'; +import {defaultValue, form} from './form'; + +const HeaderBlockConfig: BlockData = { + type: 'header-block', + component: HeaderBlock, + schema: { + name: 'Header Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: 'https://storage.cloud-preprod.yandex.net/qradle-test/header-block.svg', + }, +}; + +export default HeaderBlockConfig; diff --git a/src/blocks/Header/schema.ts b/src/blocks/Header/schema.ts index 1c1d21baf4..3dacffb25e 100644 --- a/src/blocks/Header/schema.ts +++ b/src/blocks/Header/schema.ts @@ -7,8 +7,8 @@ import { VideoProps, mediaView, withTheme, -} from '../../schema/validators/common'; -import {filteredArray} from '../../schema/validators/utils'; +} from '../../gravity-blocks/schema/validators/common'; +import {filteredArray} from '../../gravity-blocks/schema/validators/utils'; export const HeaderVideoIframeProps = { type: 'object', diff --git a/src/blocks/HeaderSlider/HeaderSlider.tsx b/src/blocks/HeaderSlider/HeaderSlider.tsx index 443f76dc97..d3dfc58a9e 100644 --- a/src/blocks/HeaderSlider/HeaderSlider.tsx +++ b/src/blocks/HeaderSlider/HeaderSlider.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; -import {SliderBlock} from '../../blocks'; -import {MobileContext} from '../../context/mobileContext'; +import {MobileContext} from '../../gravity-blocks/context/mobileContext'; import {HeaderSliderBlockProps, SliderType} from '../../models'; import {block} from '../../utils'; import Header from '../Header/Header'; +import {default as SliderBlock} from '../Slider/Slider'; import './HeaderSlider.scss'; @@ -25,15 +25,16 @@ export const HeaderSliderBlock = ({items, arrows, ...props}: HeaderSliderBlockPr blockClassName={b()} arrowSize={20} > - {items.map((item, index) => ( - <div - key={index} - className={b('item')} - data-qa={`header-slider-item-${index + 1}`} - > - <Header {...item} className={b('item-content')} /> - </div> - ))} + {items && + items.map((item, index) => ( + <div + key={index} + className={b('item')} + data-qa={`header-slider-item-${index + 1}`} + > + <Header {...item} className={b('item-content')} /> + </div> + ))} </SliderBlock> </div> ); diff --git a/src/blocks/HeaderSlider/__stories__/HeaderSlider.stories.tsx b/src/blocks/HeaderSlider/__stories__/HeaderSlider.stories.tsx index f5f5c54756..8f221c5451 100644 --- a/src/blocks/HeaderSlider/__stories__/HeaderSlider.stories.tsx +++ b/src/blocks/HeaderSlider/__stories__/HeaderSlider.stories.tsx @@ -2,12 +2,16 @@ import {Meta, StoryFn} from '@storybook/react'; import {HeaderSliderBlockProps} from '../../../models'; import HeaderSlider from '../HeaderSlider'; +import {form} from '../form'; import data from './data.json'; export default { title: 'Blocks/HeaderSlider', component: HeaderSlider, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<HeaderSliderBlockProps> = (args) => { diff --git a/src/blocks/HeaderSlider/form.ts b/src/blocks/HeaderSlider/form.ts new file mode 100644 index 0000000000..04082ede45 --- /dev/null +++ b/src/blocks/HeaderSlider/form.ts @@ -0,0 +1,37 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {HeaderSliderBlock as HeaderSliderBlockSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + HeaderSliderBlockSchema['header-slider-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + type: 'header-slider-block', + items: [ + { + title: 'Header Slide 1', + overtitle: 'Header Slider Block presents', + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, + { + image: 'https://storage.yandexcloud.net/cloud-www-assets/constructor/storybook/images/header-bg-video_light.png', + mediaView: 'fit', + title: 'Header Slide 2', + overtitle: 'Header Slider Block presents', + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + width: 'm', + buttons: [ + { + text: 'Button', + theme: 'action', + }, + ], + }, + ], +}; diff --git a/src/blocks/HeaderSlider/index.ts b/src/blocks/HeaderSlider/index.ts new file mode 100644 index 0000000000..39650c1dfd --- /dev/null +++ b/src/blocks/HeaderSlider/index.ts @@ -0,0 +1,15 @@ +import HeaderSliderBlock from './HeaderSlider'; +import {defaultValue, form} from './form'; + +const HeaderSliderBlockConfig = { + type: '@gravity-ui/page-constructor/header-slider-block', + component: HeaderSliderBlock, + schema: { + name: 'Header Slider Block', + group: '@gravity-ui/page-constructor/UnfinishedBlocks', + inputs: form, + default: defaultValue, + }, +}; + +export default HeaderSliderBlockConfig; diff --git a/src/blocks/HeaderSlider/index_deprecated.ts b/src/blocks/HeaderSlider/index_deprecated.ts new file mode 100644 index 0000000000..6577d991f9 --- /dev/null +++ b/src/blocks/HeaderSlider/index_deprecated.ts @@ -0,0 +1,16 @@ +import HeaderSliderBlock from './HeaderSlider'; +import {defaultValue, form} from './form'; + +const HeaderSliderBlockConfig = { + type: 'header-slider-block', + component: HeaderSliderBlock, + schema: { + name: 'Header Slider Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + }, +}; + +export default HeaderSliderBlockConfig; diff --git a/src/blocks/HeaderSlider/schema.ts b/src/blocks/HeaderSlider/schema.ts index 2d842c6e0c..063fede772 100644 --- a/src/blocks/HeaderSlider/schema.ts +++ b/src/blocks/HeaderSlider/schema.ts @@ -1,7 +1,7 @@ import omit from 'lodash/omit'; -import {HeaderProperties, SliderProps} from '../../schema/validators/blocks'; -import {BlockBaseProps} from '../../schema/validators/common'; +import {HeaderProperties, SliderProps} from '../../gravity-blocks/schema/validators/blocks'; +import {BlockBaseProps} from '../../gravity-blocks/schema/validators/common'; export const HeaderSliderBlock = { 'header-slider-block': { diff --git a/src/blocks/Hero/Hero.tsx b/src/blocks/Hero/Hero.tsx index 7b0da2c601..bfb1924221 100644 --- a/src/blocks/Hero/Hero.tsx +++ b/src/blocks/Hero/Hero.tsx @@ -2,9 +2,9 @@ import * as React from 'react'; import {HeaderBreadcrumbs, Media, YFMWrapper} from '../../components'; import {BREAKPOINTS} from '../../constants'; -import {useTheme} from '../../context/theme'; -import {useWindowWidth} from '../../context/windowWidthContext'; -import {Grid} from '../../grid'; +import {useTheme} from '../../gravity-blocks/context/theme'; +import {useWindowWidth} from '../../gravity-blocks/context/windowWidthContext'; +import {Grid} from '../../gravity-blocks/grid'; import {ButtonProps, HeroBlockProps, Theme} from '../../models'; import {Content} from '../../sub-blocks'; import {block, getQaAttrubutes, getThemedValue} from '../../utils'; diff --git a/src/blocks/Hero/__stories__/Hero.stories.tsx b/src/blocks/Hero/__stories__/Hero.stories.tsx index bb18f1b426..a4c1a5475c 100644 --- a/src/blocks/Hero/__stories__/Hero.stories.tsx +++ b/src/blocks/Hero/__stories__/Hero.stories.tsx @@ -5,6 +5,7 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {HeroBlockModel, HeroBlockProps, Theme as ThemeColor} from '../../../models'; import Hero from '../Hero'; +import {form} from '../form'; import data from './data.json'; @@ -12,6 +13,7 @@ export default { title: 'Blocks/Hero', component: Hero, parameters: { + inputs: form, controls: { exclude: ['type'], }, diff --git a/src/blocks/Hero/form.ts b/src/blocks/Hero/form.ts new file mode 100644 index 0000000000..304cc3c033 --- /dev/null +++ b/src/blocks/Hero/form.ts @@ -0,0 +1,20 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {HeroBlock} from './schema'; + +export const form = generateFormFieldsFromAjvSchema( + HeroBlock['hero-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + title: 'Lorem ipsum dolor sit amet', + buttons: [ + { + text: 'Button', + theme: 'action', + url: 'https://example.com', + }, + ], +}; diff --git a/src/blocks/Hero/index.ts b/src/blocks/Hero/index.ts new file mode 100644 index 0000000000..a0e85c9b01 --- /dev/null +++ b/src/blocks/Hero/index.ts @@ -0,0 +1,17 @@ +import {BlockData} from '../../constructor-items'; + +import HeroBlock from './Hero'; +import {defaultValue, form} from './form'; + +const HeroBlockConfig: BlockData = { + type: '@gravity-ui/page-constructor/hero-block', + component: HeroBlock, + schema: { + name: 'Hero Block', + group: '@gravity-ui/page-constructor/UnfinishedBlocks', + inputs: form, + default: defaultValue, + }, +}; + +export default HeroBlockConfig; diff --git a/src/blocks/Hero/index_deprecated.ts b/src/blocks/Hero/index_deprecated.ts new file mode 100644 index 0000000000..9c2c13dccc --- /dev/null +++ b/src/blocks/Hero/index_deprecated.ts @@ -0,0 +1,18 @@ +import {BlockData} from '../../constructor-items'; + +import HeroBlock from './Hero'; +import {defaultValue, form} from './form'; + +const HeroBlockConfig: BlockData = { + type: 'hero-block', + component: HeroBlock, + schema: { + name: 'Hero Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + }, +}; + +export default HeroBlockConfig; diff --git a/src/blocks/Hero/schema.ts b/src/blocks/Hero/schema.ts index d8150b2c02..6142c81b93 100644 --- a/src/blocks/Hero/schema.ts +++ b/src/blocks/Hero/schema.ts @@ -6,7 +6,7 @@ import { HeaderBreadcrumbsProps, MediaProps, withTheme, -} from '../../schema/validators/common'; +} from '../../gravity-blocks/schema/validators/common'; import {ContentBase} from '../../sub-blocks/Content/schema'; export const HeroBlockButton = { diff --git a/src/blocks/Icons/Icons.scss b/src/blocks/Icons/Icons.scss index d098159d66..bd0cc7d3e4 100644 --- a/src/blocks/Icons/Icons.scss +++ b/src/blocks/Icons/Icons.scss @@ -48,6 +48,7 @@ $block: '.#{$ns}icons-block'; @include reset-link-style(); margin: 0 $indentXXXS $indentSM; } + a#{$block}__item { @include focusable(); border-radius: var(--g-focus-border-radius); diff --git a/src/blocks/Icons/Icons.tsx b/src/blocks/Icons/Icons.tsx index e6ce8c667d..83c94e5392 100644 --- a/src/blocks/Icons/Icons.tsx +++ b/src/blocks/Icons/Icons.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import {Image, Title} from '../../components'; -import {LocationContext} from '../../context/locationContext'; -import {useTheme} from '../../context/theme'; -import {useAnalytics} from '../../hooks'; +import {LocationContext} from '../../gravity-blocks/context/locationContext'; +import {useTheme} from '../../gravity-blocks/context/theme'; +import {Grid} from '../../gravity-blocks/grid'; +import {useAnalytics} from '../../gravity-blocks/hooks'; import {IconsBlockItemProps, IconsBlockProps} from '../../models'; import {block, getLinkProps, getThemedValue} from '../../utils'; @@ -31,38 +32,41 @@ const Icons = ({title, description, size = 's', colSizes = {all: 12}, items}: Ic ); return ( - <div className={b({size})}> - {(title || description) && ( - <Title - className={b('header')} - title={title} - subtitle={description} - colSizes={colSizes} - /> - )} - {items.map((item) => { - const themedSrc = getThemedValue(item.src, theme); - const itemContent = getItemContent({...item, src: themedSrc}); - const {url, text} = item; - return url ? ( - <a - className={b('item')} - key={url} - href={url} - aria-label={text} - title={text} - {...getLinkProps(url, hostname)} - onClick={() => onClick(item)} - > - {itemContent} - </a> - ) : ( - <div className={b('item')} key={text}> - {itemContent} - </div> - ); - })} - </div> + <Grid> + <div className={b({size})}> + {(title || description) && ( + <Title + className={b('header')} + title={title} + subtitle={description} + colSizes={colSizes} + /> + )} + {items && + items.map((item) => { + const themedSrc = getThemedValue(item.src, theme); + const itemContent = getItemContent({...item, src: themedSrc}); + const {url, text} = item; + return url ? ( + <a + className={b('item')} + key={url} + href={url} + aria-label={text} + title={text} + {...getLinkProps(url, hostname)} + onClick={() => onClick(item)} + > + {itemContent} + </a> + ) : ( + <div className={b('item')} key={text}> + {itemContent} + </div> + ); + })} + </div> + </Grid> ); }; diff --git a/src/blocks/Icons/__stories__/Icons.stories.tsx b/src/blocks/Icons/__stories__/Icons.stories.tsx index 12c379db51..97c13600a0 100644 --- a/src/blocks/Icons/__stories__/Icons.stories.tsx +++ b/src/blocks/Icons/__stories__/Icons.stories.tsx @@ -2,16 +2,19 @@ import * as React from 'react'; import {Meta, StoryFn} from '@storybook/react'; -import {IconsBlock} from '../..'; import {blockTransform} from '../../../../.storybook/utils'; import {IconsBlockModel, IconsBlockProps} from '../../../models'; -import Icons from '../Icons'; +import Icons, {default as IconsBlock} from '../../Icons/Icons'; +import {form} from '../form'; import data from './data.json'; export default { title: 'Blocks/Icons', component: Icons, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<IconsBlockModel> = (args) => ( diff --git a/src/blocks/Icons/form.ts b/src/blocks/Icons/form.ts new file mode 100644 index 0000000000..7b56fdbd60 --- /dev/null +++ b/src/blocks/Icons/form.ts @@ -0,0 +1,58 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {IconsProps} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema(IconsProps as unknown as JSONSchemaType<{}>); + +export const defaultValue = { + type: 'icons-block', + title: 'Icons Block', + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + size: 'm', + items: [ + { + url: '/security/standards/software-registry', + text: 'Государственные реестры РФ', + src: 'https://storage.yandexcloud.net/cloud-www-assets/security-new/main-shield/security-software-registry.svg', + }, + { + url: '/security/standards/gdpr', + text: 'Общий регламент защиты данных (GDPR)', + src: 'https://storage.yandexcloud.net/cloud-www-assets/security-new/main-shield/security-gdpr.svg', + }, + { + url: '/security/standards/cloud-security-alliance', + text: 'Cloud Security Alliance', + src: 'https://storage.yandexcloud.net/cloud-www-assets/security-new/main-shield/security-csa.svg', + }, + { + url: '/security/standards/iso-standards', + text: 'Международная организация по стандартизации (ISO)', + src: 'https://storage.yandexcloud.net/cloud-www-assets/security-new/main-shield/security-iso.svg', + }, + { + url: '/security/standards/152-fz', + text: '№152-ФЗ «О персональных данных»', + src: 'https://storage.yandexcloud.net/cloud-www-assets/security-new/main-shield/security-152-fz.svg', + }, + { + url: '/security/standards/gost-p-57580', + text: 'ГОСТ Р 57580', + src: 'https://storage.yandexcloud.net/cloud-www-assets/security-new/main-shield/security-gost.svg', + }, + { + url: '/security/standards/pci', + text: 'Payment Card Industry Data Security Standard', + src: 'https://storage.yandexcloud.net/cloud-www-assets/security-new/main-shield/security-pci.svg', + }, + { + url: '/docs/security/standard/all', + text: 'Стандарт по защите облачной инфраструктуры', + src: 'https://storage.yandexcloud.net/cloud-www-assets/security-new/icons/security_yc.svg', + }, + ], +}; diff --git a/src/blocks/Icons/index.ts b/src/blocks/Icons/index.ts new file mode 100644 index 0000000000..88ff1b5fd9 --- /dev/null +++ b/src/blocks/Icons/index.ts @@ -0,0 +1,15 @@ +import IconsBlock from './Icons'; +import {defaultValue, form} from './form'; + +const IconsBlockConfig = { + type: '@gravity-ui/page-constructor/icons-block', + component: IconsBlock, + schema: { + name: 'Icons Block', + group: '@gravity-ui/page-constructor/UnfinishedBlocks', + inputs: form, + default: defaultValue, + }, +}; + +export default IconsBlockConfig; diff --git a/src/blocks/Icons/index_deprecated.ts b/src/blocks/Icons/index_deprecated.ts new file mode 100644 index 0000000000..113e9570ec --- /dev/null +++ b/src/blocks/Icons/index_deprecated.ts @@ -0,0 +1,16 @@ +import IconsBlock from './Icons'; +import {defaultValue, form} from './form'; + +const IconsBlockConfig = { + type: 'icons-block', + component: IconsBlock, + schema: { + name: 'Icons Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + }, +}; + +export default IconsBlockConfig; diff --git a/src/blocks/Icons/schema.ts b/src/blocks/Icons/schema.ts index a1a0ddfbed..ac328e87ae 100644 --- a/src/blocks/Icons/schema.ts +++ b/src/blocks/Icons/schema.ts @@ -2,8 +2,8 @@ import { AnimatableProps, BlockBaseProps, containerSizesObject, -} from '../../schema/validators/common'; -import {AnalyticsEventSchema} from '../../schema/validators/event'; +} from '../../gravity-blocks/schema/validators/common'; +import {AnalyticsEventSchema} from '../../gravity-blocks/schema/validators/event'; export const IconsProps = { additionalProperties: false, diff --git a/src/blocks/Info/Info.tsx b/src/blocks/Info/Info.tsx index fdca648b60..594a8af83f 100644 --- a/src/blocks/Info/Info.tsx +++ b/src/blocks/Info/Info.tsx @@ -1,5 +1,5 @@ -import {useTheme} from '../../context/theme'; -import {Col, Grid, Row} from '../../grid'; +import {useTheme} from '../../gravity-blocks/context/theme'; +import {Col, Grid, Row} from '../../gravity-blocks/grid'; import {ContentTheme, InfoBlockProps, LinkTheme} from '../../models'; import Content from '../../sub-blocks/Content/Content'; import {block, getThemedValue} from '../../utils'; @@ -39,12 +39,12 @@ export const InfoBlock = (props: InfoBlockProps) => { }; return ( - <div className={b()}> - <div - className={b('container')} - style={{backgroundColor: getThemedValue(backgroundColor, theme)}} - > - <Grid> + <Grid> + <div className={b()}> + <div + className={b('container')} + style={{backgroundColor: getThemedValue(backgroundColor, theme)}} + > <Row> <Col sizes={sizes} className={b('left')}> <Content @@ -69,9 +69,9 @@ export const InfoBlock = (props: InfoBlockProps) => { /> </Col> </Row> - </Grid> + </div> </div> - </div> + </Grid> ); }; diff --git a/src/blocks/Info/__stories__/Info.stories.tsx b/src/blocks/Info/__stories__/Info.stories.tsx index 08ad7ea7cc..47853a44d7 100644 --- a/src/blocks/Info/__stories__/Info.stories.tsx +++ b/src/blocks/Info/__stories__/Info.stories.tsx @@ -3,12 +3,16 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {CustomBlock, InfoBlockModel, InfoBlockProps} from '../../../models'; import Info from '../Info'; +import {form} from '../form'; import data from './data.json'; export default { title: 'Blocks/Info', component: Info, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<InfoBlockProps> = (args) => ( diff --git a/src/blocks/Info/form.ts b/src/blocks/Info/form.ts new file mode 100644 index 0000000000..7ec3398202 --- /dev/null +++ b/src/blocks/Info/form.ts @@ -0,0 +1,38 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {InfoBlock as InfoBlockSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + InfoBlockSchema['info-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + type: 'info-block', + title: 'Info Block', + backgroundColor: '#2c2c2c', + sectionsTitle: 'Other links', + links: [ + { + text: 'Link 1', + }, + { + text: 'Link 2', + }, + { + text: 'Link 3', + }, + ], + buttons: [ + { + text: 'Read more', + theme: 'outlined-contrast', + }, + { + text: 'Go back', + theme: 'action', + }, + ], +}; diff --git a/src/blocks/Info/icon.ts b/src/blocks/Info/icon.ts new file mode 100644 index 0000000000..bed9b8ee2c --- /dev/null +++ b/src/blocks/Info/icon.ts @@ -0,0 +1,18 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<rect x="8.93024" y="12" width="82.1395" height="26" rx="3.34884" fill="#C0C8DB"/> +<rect x="9.20931" y="12.2791" width="81.5814" height="25.4419" rx="3.06977" fill="#222222"/> +<rect x="9.20931" y="12.2791" width="81.5814" height="25.4419" rx="3.06977" stroke="#C0C8DB" stroke-width="0.55814"/> +<rect x="12" y="18" width="23" height="2" rx="0.55814" fill="#C0C8DB"/> +<rect x="12" y="22" width="7" height="2" rx="0.55814" fill="#C0C8DB"/> +<rect x="20" y="22" width="7" height="2" rx="0.55814" fill="#C0C8DB"/> +<rect x="48" y="18" width="27" height="2" rx="0.55814" fill="#C0C8DB"/> +<rect x="48" y="22" width="12" height="1" rx="0.5" fill="#C0C8DB"/> +<rect x="48" y="24" width="14" height="1" rx="0.5" fill="#C0C8DB"/> +<rect x="48" y="26" width="15" height="1" rx="0.5" fill="#C0C8DB"/> +<rect x="48" y="28" width="8" height="1" rx="0.5" fill="#C0C8DB"/> +</svg>`, +); diff --git a/src/blocks/Info/index.ts b/src/blocks/Info/index.ts new file mode 100644 index 0000000000..0c89f93a71 --- /dev/null +++ b/src/blocks/Info/index.ts @@ -0,0 +1,17 @@ +import InfoBlock from './Info'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const InfoBlockConfig = { + type: '@gravity-ui/page-constructor/info-block', + component: InfoBlock, + schema: { + name: 'Info Block', + group: '@gravity-ui/page-constructor/Blocks', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default InfoBlockConfig; diff --git a/src/blocks/Info/index_deprecated.ts b/src/blocks/Info/index_deprecated.ts new file mode 100644 index 0000000000..0b261ea38b --- /dev/null +++ b/src/blocks/Info/index_deprecated.ts @@ -0,0 +1,18 @@ +import InfoBlock from './Info'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const InfoBlockConfig = { + type: 'info-block', + component: InfoBlock, + schema: { + name: 'Info Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default InfoBlockConfig; diff --git a/src/blocks/Info/schema.ts b/src/blocks/Info/schema.ts index b5a60508ea..397f11e8dc 100644 --- a/src/blocks/Info/schema.ts +++ b/src/blocks/Info/schema.ts @@ -6,8 +6,8 @@ import { LinkProps, ThemeProps, withTheme, -} from '../../schema/validators/common'; -import {filteredArray} from '../../schema/validators/utils'; +} from '../../gravity-blocks/schema/validators/common'; +import {filteredArray} from '../../gravity-blocks/schema/validators/utils'; import {ContentBase} from '../../sub-blocks/Content/schema'; const ContentProps = { diff --git a/src/blocks/Map/__stories__/Map.stories.tsx b/src/blocks/Map/__stories__/Map.stories.tsx index 15bb798584..e5ee808c70 100644 --- a/src/blocks/Map/__stories__/Map.stories.tsx +++ b/src/blocks/Map/__stories__/Map.stories.tsx @@ -5,16 +5,23 @@ import {Meta, StoryFn} from '@storybook/react'; import {scriptsSrc, ymapApiKeyForStorybook} from '../../../../.storybook/maps'; import {blockTransform} from '../../../../.storybook/utils'; import {ApiKeyInput} from '../../../components/Map/__stories__/ApiKeyInput'; -import {MapType} from '../../../context/mapsContext/mapsContext'; -import {MapProvider, gmapApiKeyIdInLS} from '../../../context/mapsContext/mapsProvider'; +import {MapType} from '../../../gravity-blocks/context/mapsContext/mapsContext'; +import { + MapProvider, + gmapApiKeyIdInLS, +} from '../../../gravity-blocks/context/mapsContext/mapsProvider'; import {MapBlockModel, MapBlockProps} from '../../../models'; import MapBlock from '../Map'; +import {form} from '../form'; import data from './data.json'; export default { title: 'Blocks/Map', component: MapBlock, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<MapBlockModel> = (args) => ( diff --git a/src/blocks/Map/form.ts b/src/blocks/Map/form.ts new file mode 100644 index 0000000000..ebac4e63ea --- /dev/null +++ b/src/blocks/Map/form.ts @@ -0,0 +1,14 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {MapBlock as MapBlockSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + MapBlockSchema['map-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + title: 'Map Block', +}; diff --git a/src/blocks/Map/index.ts b/src/blocks/Map/index.ts new file mode 100644 index 0000000000..ab70a8eb92 --- /dev/null +++ b/src/blocks/Map/index.ts @@ -0,0 +1,15 @@ +import MapBlock from './Map'; +import {defaultValue, form} from './form'; + +const MapBlockConfig = { + type: '@gravity-ui/page-constructor/map-block', + component: MapBlock, + schema: { + name: 'Map Block', + group: '@gravity-ui/page-constructor/UnfinishedBlocks', + inputs: form, + default: defaultValue, + }, +}; + +export default MapBlockConfig; diff --git a/src/blocks/Map/index_deprecated.ts b/src/blocks/Map/index_deprecated.ts new file mode 100644 index 0000000000..8e9fd33d47 --- /dev/null +++ b/src/blocks/Map/index_deprecated.ts @@ -0,0 +1,16 @@ +import MapBlock from './Map'; +import {defaultValue, form} from './form'; + +const MapBlockConfig = { + type: 'map-block', + component: MapBlock, + schema: { + name: 'Map Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + }, +}; + +export default MapBlockConfig; diff --git a/src/blocks/Map/schema.ts b/src/blocks/Map/schema.ts index dc2e3bb906..9b4f72357d 100644 --- a/src/blocks/Map/schema.ts +++ b/src/blocks/Map/schema.ts @@ -1,4 +1,4 @@ -import {MapProps} from '../../schema/validators/common'; +import {MapProps} from '../../gravity-blocks/schema/validators/common'; import {MediaBlockBaseProps} from '../Media/schema'; export const Map = { diff --git a/src/blocks/Media/Media.tsx b/src/blocks/Media/Media.tsx index e8e5720dbf..364c18d69b 100644 --- a/src/blocks/Media/Media.tsx +++ b/src/blocks/Media/Media.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Media from '../../components/Media/Media'; import MediaBase from '../../components/MediaBase/MediaBase'; -import {useTheme} from '../../context/theme'; +import {useTheme} from '../../gravity-blocks/context/theme'; import {MediaBlockProps} from '../../models'; import {block, getThemedValue} from '../../utils'; import {getMediaBorder} from '../../utils/borderSelector'; diff --git a/src/blocks/Media/__stories__/Media.stories.tsx b/src/blocks/Media/__stories__/Media.stories.tsx index eaafa5e5bd..1ccd082956 100644 --- a/src/blocks/Media/__stories__/Media.stories.tsx +++ b/src/blocks/Media/__stories__/Media.stories.tsx @@ -5,12 +5,16 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {MediaBlockModel, MediaBlockProps} from '../../../models'; import Media from '../Media'; +import {form} from '../form'; import data from './data.json'; export default { title: 'Blocks/Media', component: Media, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<MediaBlockModel> = (args) => ( diff --git a/src/blocks/Media/form.ts b/src/blocks/Media/form.ts new file mode 100644 index 0000000000..2b5d6f196c --- /dev/null +++ b/src/blocks/Media/form.ts @@ -0,0 +1,539 @@ +import {Fields} from '../../form-generator-v2/types'; + +export const form = [ + { + type: 'section', + title: 'Layout settings', + opened: true, + fields: [ + { + type: 'select', + title: 'Direction', + name: 'direction', + options: [ + {content: 'Media-content', value: 'media-content'}, + {content: 'Content-media', value: 'content-media'}, + ], + defaultValue: 'content-media', + }, + { + type: 'select', + title: 'Mobile direction', + name: 'mobileDirection', + options: [ + {content: 'Media-content', value: 'media-content'}, + {content: 'Content-media', value: 'content-media'}, + ], + defaultValue: 'content-media', + }, + { + type: 'switch', + title: 'Large media', + name: 'largeMedia', + }, + { + type: 'switch', + title: 'Media only', + name: 'mediaOnly', + }, + ], + }, + { + type: 'section', + opened: true, + title: 'Text', + fields: [ + { + type: 'textInput', + title: 'Title', + name: 'title', + placeholder: 'Text', + }, + { + type: 'textArea', + title: 'Description', + name: 'description', + placeholder: 'Text', + }, + { + type: 'textArea', + title: 'Additional info', + name: 'additionalInfo', + placeholder: 'Text', + }, + { + type: 'select', + title: 'Text size', + name: 'size', + options: [ + {value: 's', content: 'S'}, + {value: 'm', content: 'M'}, + {value: 'l', content: 'L'}, + ], + defaultValue: 'l', + }, + { + type: 'segmentedRadioGroup', + title: 'Theme', + options: [ + {content: 'Light', value: 'light'}, + {content: 'Dark', value: 'dark'}, + ], + name: 'theme', + }, + ], + }, + { + type: 'section', + title: 'Content list', + withAddButton: true, + index: 'index1', + itemTitle: 'Item {{index1}}', + itemView: 'card', + fields: [ + { + type: 'textInput', + title: 'Title', + name: 'list[{{index1}}].title', + placeholder: 'Text', + }, + { + type: 'textArea', + title: 'Description', + name: 'list[{{index1}}].text', + placeholder: 'Text', + }, + { + type: 'textInput', + title: 'URL icon', + name: 'list[{{index1}}].icon', + placeholder: 'https://', + }, + ], + }, + { + type: 'section', + title: 'Buttons', + withAddButton: true, + index: 'index', + itemTitle: 'Button {{index}}', + itemView: 'card', + fields: [ + { + type: 'section', + title: 'Main settings', + opened: true, + fields: [ + { + title: 'Text', + type: 'textInput', + name: 'buttons[{{index}}].text', + placeholder: 'Text', + }, + { + title: 'URL', + type: 'textInput', + name: 'buttons[{{index}}].url', + placeholder: 'https://', + }, + { + title: 'URL title', + type: 'textInput', + name: 'buttons[{{index}}].urlTitle', + placeholder: 'https://', + }, + { + title: 'Style', + type: 'select', + name: 'buttons[{{index}}].theme', + options: [ + {value: 'action', content: 'Action'}, + {value: 'outlined', content: 'Outlined'}, + {value: 'normal', content: 'Normal'}, + {value: 'monochrome', content: 'Monochrome'}, + { + value: 'outlined-contrast', + content: 'Outlined-contrast', + }, + {value: 'normal-contrast', content: 'Normal-contrast'}, + ], + }, + { + title: 'Target', + type: 'select', + name: 'buttons[{{index}}].target', + options: [ + {value: '_blank'}, + {value: '_self'}, + {value: '_parent'}, + {value: '_top'}, + ], + hasClear: true, + }, + ], + }, + { + type: 'section', + title: 'Analytics tracking', + itemView: 'clear', + fields: [ + { + title: 'Name', + type: 'textInput', + name: 'buttons[{{index}}].analyticsEvents[0].name', + placeholder: 'Text', + }, + { + title: 'Target', + type: 'textInput', + name: 'buttons[{{index}}].analyticsEvents[0].target', + placeholder: 'Text', + }, + { + title: 'Counter', + type: 'textInput', + name: 'buttons[{{index}}].analyticsEvents[0].counters[0].includes', + placeholder: 'Text', + }, + { + type: 'text', + text: 'Only events for the counters listed in the input field will be sent.', + level: 'info', + }, + ], + }, + ], + }, + { + type: 'section', + title: 'Link', + index: 'index1', + withAddButton: true, + itemTitle: 'Link {{index1}}', + itemView: 'card', + fields: [ + { + type: 'textInput', + title: 'Text', + name: 'links[{{index1}}].text', + placeholder: 'Text', + }, + { + type: 'textInput', + title: 'URL', + name: 'links[{{index1}}].url', + placeholder: 'https://', + }, + { + type: 'textInput', + title: 'URL title', + name: 'links[{{index1}}].urlTitle', + placeholder: 'https://', + }, + { + type: 'select', + title: 'Style', + name: 'links[{{index1}}].theme', + options: [ + {content: 'File-link', value: 'file-link'}, + {content: 'Normal', value: 'normal'}, + {content: 'Back', value: 'back'}, + {content: 'Underline', value: 'underline'}, + ], + }, + { + title: 'Target', + type: 'select', + name: 'links[{{index1}}].target', + options: [{value: '_blank'}, {value: '_self'}, {value: '_parent'}, {value: '_top'}], + hasClear: true, + }, + { + type: 'section', + title: 'Analytics tracking', + itemView: 'clear', + fields: [ + { + title: 'Name', + type: 'textInput', + name: 'links[{{index1}}].analyticsEvents[0].name', + placeholder: 'Text', + }, + { + title: 'Target', + type: 'textInput', + name: 'links[{{index1}}].analyticsEvents[0].target', + placeholder: 'Text', + }, + { + title: 'Counter', + type: 'textInput', + name: 'links[{{index1}}].analyticsEvents[0].counters[0].includes', + placeholder: 'Text', + }, + ], + }, + ], + }, + { + type: 'section', + title: 'Media', + opened: true, + fields: [ + { + title: 'Type', + name: '_mediaType', + options: [ + {content: 'Image', value: 'image'}, + {content: 'Video', value: 'video'}, + ], + type: 'segmentedRadioGroup', + }, + { + type: 'text', + text: 'Light theme', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + title: 'Desktop', + type: 'textInput', + name: 'media.light.image.desktop', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + title: 'Tablet', + type: 'textInput', + name: 'media.light.image.tablet', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + title: 'Mobile', + type: 'textInput', + name: 'media.light.image.mobile', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + type: 'text', + text: 'Dark theme', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + title: 'Desktop', + type: 'textInput', + name: 'media.dark.image.desktop', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + title: 'Tablet', + type: 'textInput', + name: 'media.dark.image.tablet', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + title: 'Mobile', + type: 'textInput', + name: 'media.dark.image.mobile', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'image', + }, + ], + }, + { + title: 'Type', + type: 'select', + options: [{value: 'default'}, {value: 'player'}], + name: 'media.video.type', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'video', + }, + ], + }, + { + title: 'Video URL', + type: 'textInput', + name: 'media.video.src[0]', + placeholder: 'https://', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'video', + }, + ], + }, + { + title: 'Muted', + type: 'switch', + name: 'media.video.muted', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'video', + }, + ], + }, + { + title: 'Autoplay', + type: 'switch', + name: 'media.video.autoplay', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'video', + }, + ], + }, + { + title: 'Loop', + type: 'switch', + name: 'media.video.loop', + when: [ + { + field: '_mediaType', + operator: '===', + value: 'video', + }, + ], + }, + { + type: 'divider', + }, + { + title: 'Ratio', + type: 'select', + name: 'media.ratio', + options: [ + {value: 'auto', content: 'Auto'}, + {value: String(16 / 9), content: '16:9'}, + ], + defaultValue: String(16 / 9), + when: [ + { + field: '_mediaType', + operator: '===', + value: 'video', + }, + ], + }, + { + title: 'Border', + type: 'select', + name: 'border', + defaultValue: 'none', + options: [{value: 'none'}, {value: 'shadow'}, {value: 'line'}], + when: [ + { + field: '_mediaType', + operator: '!==', + value: undefined, + }, + ], + }, + ], + }, +] as Fields; + +export const defaultValue = { + title: 'Lorem ipsum dolor sit', + description: + 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.', + additionalInfo: + 'Duis aute irure dolor in reprehenderit n voluptate velit esse cillum dolore eu fugiat nulla pariatur.', + links: [ + { + url: '#', + text: 'Learn more', + theme: 'normal', + arrow: true, + }, + ], + buttons: [ + { + text: 'Button', + theme: 'action', + url: 'https://example.com', + }, + { + text: 'Button', + theme: 'outlined', + url: '#', + }, + ], + list: [ + { + title: 'Lorem ipsum', + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + { + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + { + title: 'Lorem ipsum ipsum', + }, + ], + media: { + light: { + image: { + desktop: + 'https://storage.yandexcloud.net/cloud-www-assets/constructor/main/new/media-01-01.jpg', + }, + }, + }, + _mediaType: 'image', +}; diff --git a/src/blocks/Media/icon.ts b/src/blocks/Media/icon.ts new file mode 100644 index 0000000000..5e9b841673 --- /dev/null +++ b/src/blocks/Media/icon.ts @@ -0,0 +1,13 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.97674" fill="white"/> +<rect x="9.11628" y="16.9999" width="13" height="2" rx="0.569767" fill="#262626"/> +<rect x="9.11628" y="20.9999" width="37.3256" height="2" rx="0.569767" fill="#262626"/> +<rect x="9.11628" y="23.9999" width="37.3256" height="2" rx="0.569767" fill="#262626"/> +<rect x="9.11628" y="26.9999" width="37.3256" height="2" rx="0.569767" fill="#262626"/> +<rect x="9.11628" y="30.9999" width="7" height="2" rx="0.569767" fill="#262626"/> +<rect x="51" y="5.05811" width="39.8837" height="39.8837" rx="3" fill="#222222"/> +</svg>`, +); diff --git a/src/blocks/Media/index.ts b/src/blocks/Media/index.ts new file mode 100644 index 0000000000..194c433a1e --- /dev/null +++ b/src/blocks/Media/index.ts @@ -0,0 +1,17 @@ +import MediaBlock from './Media'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const MediaBlockConfig = { + type: '@gravity-ui/page-constructor/media-block', + component: MediaBlock, + schema: { + name: 'Media Block', + group: '@gravity-ui/page-constructor/Blocks', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default MediaBlockConfig; diff --git a/src/blocks/Media/index_deprecated.ts b/src/blocks/Media/index_deprecated.ts new file mode 100644 index 0000000000..fc07ad1312 --- /dev/null +++ b/src/blocks/Media/index_deprecated.ts @@ -0,0 +1,18 @@ +import MediaBlock from './Media'; +import {defaultValue, form} from './form'; +import svgIcon from './icon'; + +const MediaBlockConfig = { + type: 'media-block', + component: MediaBlock, + schema: { + name: 'Media Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: svgIcon, + }, +}; + +export default MediaBlockConfig; diff --git a/src/blocks/Media/schema.ts b/src/blocks/Media/schema.ts index adf596ab2b..1fad7f8e3e 100644 --- a/src/blocks/Media/schema.ts +++ b/src/blocks/Media/schema.ts @@ -8,7 +8,7 @@ import { containerSizesObject, mediaDirection, withTheme, -} from '../../schema/validators/common'; +} from '../../gravity-blocks/schema/validators/common'; import {ContentBase} from '../../sub-blocks/Content/schema'; export const Media = { diff --git a/src/blocks/PromoFeaturesBlock/PromoFeaturesBlock.tsx b/src/blocks/PromoFeaturesBlock/PromoFeaturesBlock.tsx index 4c34e6a958..16adb7b3ed 100644 --- a/src/blocks/PromoFeaturesBlock/PromoFeaturesBlock.tsx +++ b/src/blocks/PromoFeaturesBlock/PromoFeaturesBlock.tsx @@ -5,7 +5,8 @@ import Media from '../../components/Media/Media'; import Title from '../../components/Title/Title'; import YFMWrapper from '../../components/YFMWrapper/YFMWrapper'; import {BREAKPOINTS} from '../../constants'; -import {useTheme} from '../../context/theme'; +import {useTheme} from '../../gravity-blocks/context/theme'; +import {Grid} from '../../gravity-blocks/grid'; import {PromoFeaturesProps} from '../../models'; import {block, getThemedValue} from '../../utils'; import {mergeVideoMicrodata} from '../../utils/microdata'; @@ -26,43 +27,50 @@ const PromoFeaturesBlock = (props: PromoFeaturesProps) => { const globalTheme = useTheme(); return ( - <AnimateBlock className={b({[backgroundTheme]: true})} animate={animated}> - <FullWidthBackground className={b('background', {[backgroundTheme]: true})} /> - <Title title={title} subtitle={description} className={b('header')} /> - <BalancedMasonry - breakpointCols={breakpointColumns} - className={b('card-container')} - columnClassName={b('card-container-column')} - > - {items.map(({title: cardTitle, text, media, theme: cardTheme}, index) => { - const blockModifier = backgroundTheme === 'default' ? 'default' : 'light'; - const themeMod = cardTheme || blockModifier || ''; - const themedMedia = getThemedValue(media, globalTheme); - const allProps = mergeVideoMicrodata(themedMedia, { - name: cardTitle, - description: text, - }); + <Grid> + <AnimateBlock className={b({[backgroundTheme]: true})} animate={animated}> + <FullWidthBackground className={b('background', {[backgroundTheme]: true})} /> + <Title title={title} subtitle={description} className={b('header')} /> + <BalancedMasonry + breakpointCols={breakpointColumns} + className={b('card-container')} + columnClassName={b('card-container-column')} + > + {items && + items.map(({title: cardTitle, text, media, theme: cardTheme}, index) => { + const blockModifier = + backgroundTheme === 'default' ? 'default' : 'light'; + const themeMod = cardTheme || blockModifier || ''; + const themedMedia = getThemedValue(media, globalTheme); + const allProps = mergeVideoMicrodata(themedMedia, { + name: cardTitle, + description: text, + }); - return ( - <div - key={index} - className={b('card', { - 'no-media': !media, - [themeMod]: Boolean(themeMod), - })} - > - <div className={b('card-info')}> - <h3 className={b('card-title')}>{cardTitle}</h3> - <div className={b('card-text')}> - <YFMWrapper content={text} modifiers={{constructor: true}} /> + return ( + <div + key={index} + className={b('card', { + 'no-media': !media, + [themeMod]: Boolean(themeMod), + })} + > + <div className={b('card-info')}> + <h3 className={b('card-title')}>{cardTitle}</h3> + <div className={b('card-text')}> + <YFMWrapper + content={text} + modifiers={{constructor: true}} + /> + </div> + </div> + {media && <Media className={b('card-media')} {...allProps} />} </div> - </div> - {media && <Media className={b('card-media')} {...allProps} />} - </div> - ); - })} - </BalancedMasonry> - </AnimateBlock> + ); + })} + </BalancedMasonry> + </AnimateBlock> + </Grid> ); }; diff --git a/src/blocks/PromoFeaturesBlock/__stories__/PromoFeaturesBlock.stories.tsx b/src/blocks/PromoFeaturesBlock/__stories__/PromoFeaturesBlock.stories.tsx index 964a41ff78..ff6c5ddcf6 100644 --- a/src/blocks/PromoFeaturesBlock/__stories__/PromoFeaturesBlock.stories.tsx +++ b/src/blocks/PromoFeaturesBlock/__stories__/PromoFeaturesBlock.stories.tsx @@ -3,6 +3,7 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {PromoFeaturesBlockModel, PromoFeaturesProps} from '../../../models'; import PromoFeaturesBlock from '../PromoFeaturesBlock'; +import {form} from '../form'; import data from './data.json'; @@ -12,6 +13,9 @@ export default { args: { theme: 'default', }, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<PromoFeaturesBlockModel> = (args) => { diff --git a/src/blocks/PromoFeaturesBlock/form.ts b/src/blocks/PromoFeaturesBlock/form.ts new file mode 100644 index 0000000000..1c117a7ab7 --- /dev/null +++ b/src/blocks/PromoFeaturesBlock/form.ts @@ -0,0 +1,44 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {PromoFeaturesBlock as PromoFeaturesBlockSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + PromoFeaturesBlockSchema['promo-features-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + title: 'Promo Features Block', + theme: 'default', + items: [ + { + title: 'Lorem ipsum dolor sit amet', + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + { + title: 'Lorem ipsum dolor sit amet', + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + { + title: 'Lorem ipsum dolor sit amet', + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + { + title: 'Lorem ipsum dolor sit amet', + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + theme: 'accent', + }, + { + title: 'Lorem ipsum dolor sit amet', + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + theme: 'accent-light', + }, + { + title: 'Lorem ipsum dolor sit amet', + text: 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + theme: 'primary', + }, + ], +}; diff --git a/src/blocks/PromoFeaturesBlock/index.ts b/src/blocks/PromoFeaturesBlock/index.ts new file mode 100644 index 0000000000..90f68f5dce --- /dev/null +++ b/src/blocks/PromoFeaturesBlock/index.ts @@ -0,0 +1,17 @@ +import {BlockData} from '../../constructor-items'; + +import PromoFeaturesBlock from './PromoFeaturesBlock'; +import {defaultValue, form} from './form'; + +const PromoFeaturesBlockConfig: BlockData = { + type: '@gravity-ui/page-constructor/promo-features-block', + component: PromoFeaturesBlock, + schema: { + name: 'Promo Features Block', + group: '@gravity-ui/page-constructor/UnfinishedBlocks', + inputs: form, + default: defaultValue, + }, +}; + +export default PromoFeaturesBlockConfig; diff --git a/src/blocks/PromoFeaturesBlock/index_deprecated.ts b/src/blocks/PromoFeaturesBlock/index_deprecated.ts new file mode 100644 index 0000000000..013d043d39 --- /dev/null +++ b/src/blocks/PromoFeaturesBlock/index_deprecated.ts @@ -0,0 +1,16 @@ +import PromoFeaturesBlock from './PromoFeaturesBlock'; +import {defaultValue, form} from './form'; + +const PromoFeaturesBlockConfig = { + type: 'promo-features-block', + component: PromoFeaturesBlock, + schema: { + name: 'Promo Features Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + }, +}; + +export default PromoFeaturesBlockConfig; diff --git a/src/blocks/PromoFeaturesBlock/schema.ts b/src/blocks/PromoFeaturesBlock/schema.ts index 0f2407df9d..68e49ded52 100644 --- a/src/blocks/PromoFeaturesBlock/schema.ts +++ b/src/blocks/PromoFeaturesBlock/schema.ts @@ -1,5 +1,9 @@ -import {AnimatableProps, BlockBaseProps, BlockHeaderProps} from '../../schema/validators/common'; -import {filteredArray} from '../../schema/validators/utils'; +import { + AnimatableProps, + BlockBaseProps, + BlockHeaderProps, +} from '../../gravity-blocks/schema/validators/common'; +import {filteredArray} from '../../gravity-blocks/schema/validators/utils'; import {Media} from '../Media/schema'; export const PromoFeaturesItem = { diff --git a/src/blocks/Questions/Questions.tsx b/src/blocks/Questions/Questions.tsx index a0160a5df0..39dc367549 100644 --- a/src/blocks/Questions/Questions.tsx +++ b/src/blocks/Questions/Questions.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {Col, Row} from '../../grid'; +import {Col, Grid, Row} from '../../gravity-blocks/grid'; import {QuestionsProps} from '../../models'; import {Content} from '../../sub-blocks'; import {block} from '../../utils'; @@ -60,50 +60,52 @@ const QuestionsBlock = (props: QuestionsProps) => { return ( <div className={b()}> {faqMicrodataScript} - <Row> - <Col sizes={{all: 12, md: 4}}> - <div className={b('title')}> - <Content - title={title} - text={text} - additionalInfo={additionalInfo} - links={links} - list={list} - buttons={buttons} - colSizes={{all: 12, md: 12}} - /> - </div> - </Col> - <Col sizes={{all: 12, md: 8}} role={'list'}> - {items.map( - ( - { - title: itemTitle, - text: itemText, - link, - listStyle = 'dash', - onClick: itemOnClick, - }, - index, - ) => { - const isOpened = opened.includes(index); - const onClick = () => toggleItem(index, itemOnClick); + <Grid> + <Row> + <Col sizes={{all: 12, md: 4}}> + <div className={b('title')}> + <Content + title={title} + text={text} + additionalInfo={additionalInfo} + links={links} + list={list} + buttons={buttons} + colSizes={{all: 12, md: 12}} + /> + </div> + </Col> + <Col sizes={{all: 12, md: 8}} role={'list'}> + {items.map( + ( + { + title: itemTitle, + text: itemText, + link, + listStyle = 'dash', + onClick: itemOnClick, + }, + index, + ) => { + const isOpened = opened.includes(index); + const onClick = () => toggleItem(index, itemOnClick); - return ( - <QuestionBlockItem - key={itemTitle} - title={itemTitle} - text={itemText} - link={link} - listStyle={listStyle} - isOpened={isOpened} - onClick={onClick} - /> - ); - }, - )} - </Col> - </Row> + return ( + <QuestionBlockItem + key={itemTitle} + title={itemTitle} + text={itemText} + link={link} + listStyle={listStyle} + isOpened={isOpened} + onClick={onClick} + /> + ); + }, + )} + </Col> + </Row> + </Grid> </div> ); }; diff --git a/src/blocks/Questions/__stories__/Questions.stories.tsx b/src/blocks/Questions/__stories__/Questions.stories.tsx index d5b40a2e2c..db5d8c04a4 100644 --- a/src/blocks/Questions/__stories__/Questions.stories.tsx +++ b/src/blocks/Questions/__stories__/Questions.stories.tsx @@ -3,12 +3,16 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {QuestionsBlockModel, QuestionsProps} from '../../../models'; import QuestionsBlock from '../Questions'; +import {form} from '../form'; import data from './data.json'; export default { title: 'Blocks/Questions', component: QuestionsBlock, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<QuestionsBlockModel> = (args) => { diff --git a/src/blocks/Questions/form.ts b/src/blocks/Questions/form.ts new file mode 100644 index 0000000000..5396609732 --- /dev/null +++ b/src/blocks/Questions/form.ts @@ -0,0 +1,44 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {QuestionsBlock as QuestionsBlockSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + QuestionsBlockSchema['questions-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + type: 'questions-block', + title: 'Questions Block', + text: 'Here you can find answers.', + links: [ + { + text: 'Report for 2024', + url: 'file.doc', + }, + ], + buttons: [ + { + text: 'Get more', + theme: 'outlined-info', + }, + ], + list: [ + { + title: 'Report for 2024', + text: 'Some advice here', + }, + ], + items: [ + { + title: 'Question 1', + text: 'Answer for question 1', + }, + { + title: 'Question 2', + text: 'Answer for question 2', + }, + ], +}; diff --git a/src/blocks/Questions/icon.ts b/src/blocks/Questions/icon.ts new file mode 100644 index 0000000000..0e49676093 --- /dev/null +++ b/src/blocks/Questions/icon.ts @@ -0,0 +1,19 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.97674" fill="white"/> +<rect x="9.11628" y="13" width="19" height="2" rx="0.569767" fill="#262626"/> +<rect x="9.11628" y="17" width="13" height="2" rx="0.569767" fill="#262626"/> +<rect x="36.1163" y="13" width="50" height="2" rx="0.569767" fill="#262626"/> +<rect x="88.8837" y="13" width="2" height="2" rx="0.569767" fill="#262626"/> +<rect x="36.1163" y="17" width="39" height="2" rx="0.569767" fill="#262626"/> +<rect x="88.8837" y="17" width="2" height="2" rx="0.569767" fill="#262626"/> +<rect x="36.1163" y="21" width="44" height="2" rx="0.569767" fill="#262626"/> +<rect x="88.8837" y="21" width="2" height="2" rx="0.569767" fill="#262626"/> +<rect x="36.1163" y="25" width="32" height="2" rx="0.569767" fill="#262626"/> +<rect x="88.8837" y="25" width="2" height="2" rx="0.569767" fill="#262626"/> +<rect x="36.1163" y="29" width="36" height="2" rx="0.569767" fill="#262626"/> +<rect x="88.8837" y="29" width="2" height="2" rx="0.569767" fill="#262626"/> +</svg>`, +); diff --git a/src/blocks/Questions/index.ts b/src/blocks/Questions/index.ts new file mode 100644 index 0000000000..6ab9c3daf9 --- /dev/null +++ b/src/blocks/Questions/index.ts @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import QuestionsBlock from './Questions'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const QuestionsBlockConfig: BlockData = { + type: '@gravity-ui/page-constructor/questions-block', + component: QuestionsBlock, + schema: { + name: 'Questions Block', + group: '@gravity-ui/page-constructor/Blocks', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default QuestionsBlockConfig; diff --git a/src/blocks/Questions/index_deprecated.ts b/src/blocks/Questions/index_deprecated.ts new file mode 100644 index 0000000000..4785ffd4e8 --- /dev/null +++ b/src/blocks/Questions/index_deprecated.ts @@ -0,0 +1,18 @@ +import QuestionsBlock from './Questions'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const QuestionsBlockConfig = { + type: 'questions-block', + component: QuestionsBlock, + schema: { + name: 'Questions Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default QuestionsBlockConfig; diff --git a/src/blocks/Questions/schema.ts b/src/blocks/Questions/schema.ts index 6b910bbe56..10407e2e16 100644 --- a/src/blocks/Questions/schema.ts +++ b/src/blocks/Questions/schema.ts @@ -1,7 +1,7 @@ import omit from 'lodash/omit'; -import {BlockBaseProps, LinkProps} from '../../schema/validators/common'; -import {filteredArray} from '../../schema/validators/utils'; +import {BlockBaseProps, LinkProps} from '../../gravity-blocks/schema/validators/common'; +import {filteredArray} from '../../gravity-blocks/schema/validators/utils'; import {ContentBase} from '../../sub-blocks/Content/schema'; const QuestionsBlockContentProps = omit(ContentBase, ['size', 'theme']); diff --git a/src/blocks/Share/Share.tsx b/src/blocks/Share/Share.tsx index 0c17d7dd88..abbc61ab62 100644 --- a/src/blocks/Share/Share.tsx +++ b/src/blocks/Share/Share.tsx @@ -3,13 +3,13 @@ import * as React from 'react'; import {Button, Icon} from '@gravity-ui/uikit'; import {YFMWrapper} from '../../components'; -import {LocationContext} from '../../context/locationContext'; -import {useAnalytics} from '../../hooks'; -import {Facebook} from '../../icons/Facebook'; -import {Linkedin} from '../../icons/Linkedin'; -import {Telegram} from '../../icons/Telegram'; -import {Twitter} from '../../icons/Twitter'; -import {Vk} from '../../icons/Vk'; +import {LocationContext} from '../../gravity-blocks/context/locationContext'; +import {useAnalytics} from '../../gravity-blocks/hooks'; +import {Facebook} from '../../gravity-blocks/icons/Facebook'; +import {Linkedin} from '../../gravity-blocks/icons/Linkedin'; +import {Telegram} from '../../gravity-blocks/icons/Telegram'; +import {Twitter} from '../../gravity-blocks/icons/Twitter'; +import {Vk} from '../../gravity-blocks/icons/Vk'; import {DefaultEventNames, ShareBlockProps} from '../../models'; import {block, getAbsolutePath, getShareLink} from '../../utils'; @@ -52,29 +52,32 @@ const Share = ({items, title}: ShareBlockProps) => { <h5 className={b('title')}>{i18n('constructor-share')}</h5> )} <div className={b('items')}> - {items.map((type) => { - const url = getAbsolutePath(hostname, pathname); - const socialUrl = getShareLink(url, type); - const icon = icons[type]; - const urlTitle = i18n(`${type}-title`); - const buttonLabel = i18n(`${type}-label`); + {items && + items.map((type) => { + const url = getAbsolutePath(hostname, pathname); + const socialUrl = getShareLink(url, type); + const icon = icons[type]; + const urlTitle = i18n(`${type}-title`); + const buttonLabel = i18n(`${type}-label`); - return ( - <Button - key={type} - view="flat" - size="l" - target="_blank" - href={socialUrl} - className={b('item', {type: type.toLowerCase()})} - onClick={handleButtonClick} - title={urlTitle} - aria-label={buttonLabel} - > - {icon && <Icon data={icon} size={24} className={b('icon', {type})} />} - </Button> - ); - })} + return ( + <Button + key={type} + view="flat" + size="l" + target="_blank" + href={socialUrl} + className={b('item', {type: type.toLowerCase()})} + onClick={handleButtonClick} + title={urlTitle} + aria-label={buttonLabel} + > + {icon && ( + <Icon data={icon} size={24} className={b('icon', {type})} /> + )} + </Button> + ); + })} </div> </div> ); diff --git a/src/blocks/Share/__stories__/Share.stories.tsx b/src/blocks/Share/__stories__/Share.stories.tsx index e09861e916..d0a098b12e 100644 --- a/src/blocks/Share/__stories__/Share.stories.tsx +++ b/src/blocks/Share/__stories__/Share.stories.tsx @@ -3,6 +3,7 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {PCShareSocialNetwork, ShareBLockModel, ShareBlockProps} from '../../../models'; import Share from '../Share'; +import {form} from '../form'; import data from './data.json'; @@ -15,6 +16,9 @@ export default { options: Object.values(PCShareSocialNetwork), }, }, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<ShareBLockModel> = (args) => ( diff --git a/src/blocks/Share/form.ts b/src/blocks/Share/form.ts new file mode 100644 index 0000000000..04accfc1c0 --- /dev/null +++ b/src/blocks/Share/form.ts @@ -0,0 +1,15 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {ShareBlock as ShareBlockSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + ShareBlockSchema['share-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + items: ['vk', 'telegram', 'facebook'], + title: 'Share Block', +}; diff --git a/src/blocks/Share/index.ts b/src/blocks/Share/index.ts new file mode 100644 index 0000000000..e280b94884 --- /dev/null +++ b/src/blocks/Share/index.ts @@ -0,0 +1,15 @@ +import ShareBlock from './Share'; +import {defaultValue, form} from './form'; + +const ShareBlockConfig = { + type: '@gravity-ui/page-constructor/share-block', + component: ShareBlock, + schema: { + name: 'Share Block', + group: '@gravity-ui/page-constructor/UnfinishedBlocks', + inputs: form, + default: defaultValue, + }, +}; + +export default ShareBlockConfig; diff --git a/src/blocks/Share/index_deprecated.ts b/src/blocks/Share/index_deprecated.ts new file mode 100644 index 0000000000..e6dbc45446 --- /dev/null +++ b/src/blocks/Share/index_deprecated.ts @@ -0,0 +1,16 @@ +import ShareBlock from './Share'; +import {defaultValue, form} from './form'; + +const ShareBlockConfig = { + type: 'share-block', + component: ShareBlock, + schema: { + name: 'Share Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + }, +}; + +export default ShareBlockConfig; diff --git a/src/blocks/Share/schema.ts b/src/blocks/Share/schema.ts index 2974e426c2..e151de7a87 100644 --- a/src/blocks/Share/schema.ts +++ b/src/blocks/Share/schema.ts @@ -1,4 +1,4 @@ -import {BaseProps} from '../../schema/validators/common'; +import {BaseProps} from '../../gravity-blocks/schema/validators/common'; export const ShareBlock = { 'share-block': { diff --git a/src/blocks/Slider/Slider.tsx b/src/blocks/Slider/Slider.tsx index 55618298c8..97c5ca7765 100644 --- a/src/blocks/Slider/Slider.tsx +++ b/src/blocks/Slider/Slider.tsx @@ -9,6 +9,7 @@ import {Swiper as SwiperReact, SwiperSlide} from 'swiper/react'; import Anchor from '../../components/Anchor/Anchor'; import AnimateBlock from '../../components/AnimateBlock/AnimateBlock'; import Title from '../../components/Title/Title'; +import {Grid} from '../../gravity-blocks/grid'; import {ClassNameProps, Refable, SliderProps as SliderParams, SliderType} from '../../models'; import {block} from '../../utils'; @@ -91,103 +92,105 @@ export const SliderBlock = ({ }); return ( - <div - className={b( - { - 'one-slide': childrenCount === 1, - 'only-arrows': - (!title || (typeof title !== 'string' && !title?.text)) && - !description && - arrows, - 'without-dots': !dots || isLocked, - type, - }, - blockClassName, - )} - > - {anchorId && <Anchor id={anchorId} />} - <Title - title={title} - subtitle={description} - className={b('header', {'no-description': !description})} - /> - <AnimateBlock className={b('animate-slides')} animate={animated}> - <SwiperReact - modules={[Autoplay, A11y, Pagination]} - className={b('slider', className)} - onSwiper={onSwiper} - speed={1000} - autoplay={autoplay} - autoHeight={adaptive} - initialSlide={initialSlide} - noSwiping={false} - breakpoints={breakpoints} - onSlideChange={onSlideChange} - onSlideChangeTransitionStart={onSlideChangeTransitionStart} - onSlideChangeTransitionEnd={onSlideChangeTransitionEnd} - onActiveIndexChange={onActiveIndexChange} - onBreakpoint={onBreakpoint} - onLock={() => setIsLocked(true)} - onUnlock={() => setIsLocked(false)} - watchOverflow - watchSlidesProgress - touchStartPreventDefault={false} - touchAngle={45} - threshold={10} - longSwipes={true} - longSwipesRatio={0.5} - resistance={true} - resistanceRatio={0.5} - a11y={{ - slideLabelMessage: '', - paginationBulletMessage: i18n('dot-label', {index: '{{index}}'}), - }} - {...paginationProps} - > - {React.Children.map(children, (elem, index) => ( - <SwiperSlide className={b('slide')} key={index}> - {({isVisible}) => ( - <div - className={b('slide-item')} - aria-hidden={!isA11yControlHidden && !isVisible} - > - {elem} - </div> - )} - </SwiperSlide> - ))} - </SwiperReact> - {arrows && !isLocked && ( - <React.Fragment> - <div aria-hidden={isA11yControlHidden}> - <Arrow - className={b('arrow', {prev: true})} - type="left" - transparent={type === SliderType.HeaderCard} - onClick={onPrev} - size={arrowSize} - extraProps={{tabIndex: controlTabIndex}} - /> - <Arrow - className={b('arrow', {next: true})} - type="right" - transparent={type === SliderType.HeaderCard} - onClick={onNext} - size={arrowSize} - extraProps={{tabIndex: controlTabIndex}} - /> - </div> - </React.Fragment> + <Grid> + <div + className={b( + { + 'one-slide': childrenCount === 1, + 'only-arrows': + (!title || (typeof title !== 'string' && !title?.text)) && + !description && + arrows, + 'without-dots': !dots || isLocked, + type, + }, + blockClassName, )} - <div className={b('footer')}> - {disclaimer ? ( - <div className={b('disclaimer', {size: disclaimer?.size || 'm'})}> - {disclaimer?.text} - </div> - ) : null} - </div> - </AnimateBlock> - </div> + > + {anchorId && <Anchor id={anchorId} />} + <Title + title={title} + subtitle={description} + className={b('header', {'no-description': !description})} + /> + <AnimateBlock className={b('animate-slides')} animate={animated}> + <SwiperReact + modules={[Autoplay, A11y, Pagination]} + className={b('slider', className)} + onSwiper={onSwiper} + speed={1000} + autoplay={autoplay} + autoHeight={adaptive} + initialSlide={initialSlide} + noSwiping={false} + breakpoints={breakpoints} + onSlideChange={onSlideChange} + onSlideChangeTransitionStart={onSlideChangeTransitionStart} + onSlideChangeTransitionEnd={onSlideChangeTransitionEnd} + onActiveIndexChange={onActiveIndexChange} + onBreakpoint={onBreakpoint} + onLock={() => setIsLocked(true)} + onUnlock={() => setIsLocked(false)} + watchOverflow + watchSlidesProgress + touchStartPreventDefault={false} + touchAngle={45} + threshold={10} + longSwipes={true} + longSwipesRatio={0.5} + resistance={true} + resistanceRatio={0.5} + a11y={{ + slideLabelMessage: '', + paginationBulletMessage: i18n('dot-label', {index: '{{index}}'}), + }} + {...paginationProps} + > + {React.Children.map(children, (elem, index) => ( + <SwiperSlide className={b('slide')} key={index}> + {({isVisible}) => ( + <div + className={b('slide-item')} + aria-hidden={!isA11yControlHidden && !isVisible} + > + {elem} + </div> + )} + </SwiperSlide> + ))} + </SwiperReact> + {arrows && !isLocked && ( + <React.Fragment> + <div aria-hidden={isA11yControlHidden}> + <Arrow + className={b('arrow', {prev: true})} + type="left" + transparent={type === SliderType.HeaderCard} + onClick={onPrev} + size={arrowSize} + extraProps={{tabIndex: controlTabIndex}} + /> + <Arrow + className={b('arrow', {next: true})} + type="right" + transparent={type === SliderType.HeaderCard} + onClick={onNext} + size={arrowSize} + extraProps={{tabIndex: controlTabIndex}} + /> + </div> + </React.Fragment> + )} + <div className={b('footer')}> + {disclaimer ? ( + <div className={b('disclaimer', {size: disclaimer?.size || 'm'})}> + {disclaimer?.text} + </div> + ) : null} + </div> + </AnimateBlock> + </div> + </Grid> ); }; diff --git a/src/blocks/Slider/__stories__/Slider.stories.tsx b/src/blocks/Slider/__stories__/Slider.stories.tsx index 4528a485f4..2e0ec1f0e3 100644 --- a/src/blocks/Slider/__stories__/Slider.stories.tsx +++ b/src/blocks/Slider/__stories__/Slider.stories.tsx @@ -10,12 +10,16 @@ import { } from '../../../models'; import {BannerCard, BasicCard, Quote} from '../../../sub-blocks'; import Slider, {SliderBlock, SliderProps} from '../Slider'; +import {form} from '../form'; import data from './data.json'; export default { title: 'Blocks/Slider', component: Slider, + parameters: { + inputs: form, + }, } as Meta; const renderChild = (childArgs: SubBlockModels, index?: number) => { diff --git a/src/blocks/Slider/form.ts b/src/blocks/Slider/form.ts new file mode 100644 index 0000000000..06f2641eb3 --- /dev/null +++ b/src/blocks/Slider/form.ts @@ -0,0 +1,41 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {SliderBlock as SliderBlockSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + SliderBlockSchema['slider-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + dots: true, + type: 'slider-block', + title: 'Slider Block with Quote Cards', + description: 'You can insert any card inside block', + slidesToShow: 1, + arrows: true, + children: [ + { + type: 'quote', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + url: 'https://example.com', + author: { + firstName: 'Lorem', + secondName: 'ipsum', + description: 'Lorem ipsum', + }, + }, + { + type: 'quote', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + url: 'https://example.com', + author: { + firstName: 'Lorem', + secondName: 'ipsum', + description: 'Lorem ipsum', + }, + }, + ], +}; diff --git a/src/blocks/Slider/icon.ts b/src/blocks/Slider/icon.ts new file mode 100644 index 0000000000..0e6853d2f4 --- /dev/null +++ b/src/blocks/Slider/icon.ts @@ -0,0 +1,55 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<g filter="url(#filter0_dd_2013_12945)"> +<rect x="8.93024" y="23" width="4" height="4" rx="2" fill="#B0BDD9"/> +</g> +<rect x="15.1628" y="10.2906" width="21.845" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="15.1628" y="10.2906" width="21.845" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="18.5116" y="28.593" width="15.1473" height="2" rx="0.55814" fill="#262626"/> +<rect x="18.5116" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +<rect x="39.2403" y="10.2906" width="21.845" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="39.2403" y="10.2906" width="21.845" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="42.5892" y="28.593" width="15.1473" height="2" rx="0.55814" fill="#262626"/> +<rect x="42.5892" y="31.7094" width="10" height="2" rx="0.55814" fill="#262626"/> +<rect x="63.3178" y="10.2906" width="21.845" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="63.3178" y="10.2906" width="21.845" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="66.6667" y="28.593" width="15.1473" height="2" rx="0.55814" fill="#262626"/> +<rect x="66.6667" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +<g filter="url(#filter1_dd_2013_12945)"> +<rect x="87.3954" y="23" width="4" height="4" rx="2" fill="#B0BDD9"/> +</g> +<defs> +<filter id="filter0_dd_2013_12945" x="2.23256" y="17.4186" width="17.3953" height="17.3953" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="0.55814"/> +<feGaussianBlur stdDeviation="1.11628"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2013_12945"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="1.11628"/> +<feGaussianBlur stdDeviation="3.34884"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/> +<feBlend mode="normal" in2="effect1_dropShadow_2013_12945" result="effect2_dropShadow_2013_12945"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_2013_12945" result="shape"/> +</filter> +<filter id="filter1_dd_2013_12945" x="80.6977" y="17.4186" width="17.3953" height="17.3953" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="0.55814"/> +<feGaussianBlur stdDeviation="1.11628"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2013_12945"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="1.11628"/> +<feGaussianBlur stdDeviation="3.34884"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/> +<feBlend mode="normal" in2="effect1_dropShadow_2013_12945" result="effect2_dropShadow_2013_12945"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_2013_12945" result="shape"/> +</filter> +</defs> +</svg>`, +); diff --git a/src/blocks/Slider/index.ts b/src/blocks/Slider/index.ts new file mode 100644 index 0000000000..9f27ac1aac --- /dev/null +++ b/src/blocks/Slider/index.ts @@ -0,0 +1,17 @@ +import SliderBlock from './Slider'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const SliderBlockConfig = { + type: '@gravity-ui/page-constructor/slider-block', + component: SliderBlock, + schema: { + name: 'Slider Block', + group: '@gravity-ui/page-constructor/CardContainers', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default SliderBlockConfig; diff --git a/src/blocks/Slider/index_deprecated.ts b/src/blocks/Slider/index_deprecated.ts new file mode 100644 index 0000000000..1f6a76821f --- /dev/null +++ b/src/blocks/Slider/index_deprecated.ts @@ -0,0 +1,18 @@ +import SliderBlock from './Slider'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const SliderBlockConfig = { + type: 'slider-block', + component: SliderBlock, + schema: { + name: 'Slider Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default SliderBlockConfig; diff --git a/src/blocks/Slider/schema.ts b/src/blocks/Slider/schema.ts index 94f35b02c5..07e7eb65de 100644 --- a/src/blocks/Slider/schema.ts +++ b/src/blocks/Slider/schema.ts @@ -5,7 +5,7 @@ import { ChildrenCardsProps, sliderSizesObject, textSize, -} from '../../schema/validators/common'; +} from '../../gravity-blocks/schema/validators/common'; const LoadableProps = { additionalProperties: false, diff --git a/src/blocks/SliderOld/SliderOld.tsx b/src/blocks/SliderOld/SliderOld.tsx index e6eea21d0b..013c26074b 100644 --- a/src/blocks/SliderOld/SliderOld.tsx +++ b/src/blocks/SliderOld/SliderOld.tsx @@ -11,10 +11,10 @@ import AnimateBlock from '../../components/AnimateBlock/AnimateBlock'; import OutsideClick from '../../components/OutsideClick/OutsideClick'; import Title from '../../components/Title/Title'; import {BREAKPOINTS} from '../../constants'; -import {MobileContext} from '../../context/mobileContext'; -import {SSRContext} from '../../context/ssrContext'; -import {StylesContext} from '../../context/stylesContext/StylesContext'; -import useFocus from '../../hooks/useFocus'; +import {MobileContext} from '../../gravity-blocks/context/mobileContext'; +import {SSRContext} from '../../gravity-blocks/context/ssrContext'; +import {StylesContext} from '../../gravity-blocks/context/stylesContext/StylesContext'; +import useFocus from '../../gravity-blocks/hooks/useFocus'; import { ClassNameProps, Refable, diff --git a/src/blocks/SliderOld/__stories__/Slider.stories.tsx b/src/blocks/SliderOld/__stories__/Slider.stories.tsx index 0872f8c2e9..8b1636fd2d 100644 --- a/src/blocks/SliderOld/__stories__/Slider.stories.tsx +++ b/src/blocks/SliderOld/__stories__/Slider.stories.tsx @@ -6,6 +6,7 @@ import {blockTransform} from '../../../../.storybook/utils'; import {subBlockMap} from '../../../constructor-items'; import {SliderOldBlockModel, SubBlock} from '../../../models'; import SliderOld, {SliderOldProps} from '../SliderOld'; +import {form} from '../form'; import data from './data.json'; @@ -34,6 +35,9 @@ export default { </div> ), ], + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<SliderOldBlockModel> = (args) => transformAndRender(args); diff --git a/src/blocks/SliderOld/form.ts b/src/blocks/SliderOld/form.ts new file mode 100644 index 0000000000..da02be8334 --- /dev/null +++ b/src/blocks/SliderOld/form.ts @@ -0,0 +1,14 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {SliderOldBlock} from './schema'; + +export const form = generateFormFieldsFromAjvSchema( + SliderOldBlock['slider-old-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + title: 'Lorem ipsum dolor sit amet', + children: [], +}; diff --git a/src/blocks/SliderOld/index.ts b/src/blocks/SliderOld/index.ts new file mode 100644 index 0000000000..65e0482b68 --- /dev/null +++ b/src/blocks/SliderOld/index.ts @@ -0,0 +1,17 @@ +import {BlockData} from '../../constructor-items'; + +import SliderOldBlock from './SliderOld'; +import {defaultValue, form} from './form'; + +const SliderOldBlockConfig: BlockData = { + type: '@gravity-ui/page-constructor/slider-old-block', + component: SliderOldBlock, + schema: { + name: 'Slider Old Block', + group: '@gravity-ui/page-constructor/UnfinishedBlocks', + inputs: form, + default: defaultValue, + }, +}; + +export default SliderOldBlockConfig; diff --git a/src/blocks/SliderOld/index_deprecated.ts b/src/blocks/SliderOld/index_deprecated.ts new file mode 100644 index 0000000000..15db675d11 --- /dev/null +++ b/src/blocks/SliderOld/index_deprecated.ts @@ -0,0 +1,18 @@ +import {BlockData} from '../../constructor-items'; + +import SliderOldBlock from './SliderOld'; +import {defaultValue, form} from './form'; + +const SliderOldBlockConfig: BlockData = { + type: 'slider-old-block', + component: SliderOldBlock, + schema: { + name: 'Slider Old Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + }, +}; + +export default SliderOldBlockConfig; diff --git a/src/blocks/SliderOld/schema.ts b/src/blocks/SliderOld/schema.ts index d5a4d84cd5..3794fc7cc3 100644 --- a/src/blocks/SliderOld/schema.ts +++ b/src/blocks/SliderOld/schema.ts @@ -5,7 +5,7 @@ import { ChildrenCardsProps, sliderSizesObject, textSize, -} from '../../schema/validators/common'; +} from '../../gravity-blocks/schema/validators/common'; const LoadableProps = { additionalProperties: false, diff --git a/src/blocks/Table/Table.tsx b/src/blocks/Table/Table.tsx index b292709574..356829ddda 100644 --- a/src/blocks/Table/Table.tsx +++ b/src/blocks/Table/Table.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import {Table, YFMWrapper} from '../../components'; -import {Col, Grid, GridColumnSize, Row} from '../../grid'; +import {Col, Grid, GridColumnSize, Row} from '../../gravity-blocks/grid'; import {TableBlockProps} from '../../models'; import {block} from '../../utils'; diff --git a/src/blocks/Table/__stories__/Table.stories.tsx b/src/blocks/Table/__stories__/Table.stories.tsx index 4abf8058aa..31749db1a1 100644 --- a/src/blocks/Table/__stories__/Table.stories.tsx +++ b/src/blocks/Table/__stories__/Table.stories.tsx @@ -3,12 +3,16 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {TableBlockModel, TableBlockProps} from '../../../models'; import Table, {TableBlock} from '../Table'; +import {form} from '../form'; import data from './data.json'; export default { component: Table, title: 'Blocks/Table', + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<TableBlockModel> = (args) => ( diff --git a/src/blocks/Table/form.ts b/src/blocks/Table/form.ts new file mode 100644 index 0000000000..d79c935c69 --- /dev/null +++ b/src/blocks/Table/form.ts @@ -0,0 +1,27 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {TableBlock as TableBlockSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + TableBlockSchema['table-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + type: 'table-block', + title: 'Lorem ipsum dolor sit amet', + table: { + content: [ + ['Lorem', 'ipsum 1', 'dolor 2', 'sit 3'], + ['Lorem 1', '0', '0', '0'], + ['Lorem 2', '0', '0', '1'], + ['Lorem 3', '0', '0', '1'], + ['Lorem 4', '0', '1', '1'], + ['Lorem 5', '1', '1', '1'], + ], + legend: ['ipsum 1', 'ipsum 2'], + justify: ['start', 'center', 'center', 'center'], + }, +}; diff --git a/src/blocks/Table/index.ts b/src/blocks/Table/index.ts new file mode 100644 index 0000000000..c7f0ad96da --- /dev/null +++ b/src/blocks/Table/index.ts @@ -0,0 +1,15 @@ +import TableBlock from './Table'; +import {defaultValue, form} from './form'; + +const TableBlockConfig = { + type: 'table-block', + component: TableBlock, + schema: { + name: 'Table Block', + group: '@gravity-ui/page-constructor/UnfinishedBlocks', + inputs: form, + default: defaultValue, + }, +}; + +export default TableBlockConfig; diff --git a/src/blocks/Table/index_deprecated.ts b/src/blocks/Table/index_deprecated.ts new file mode 100644 index 0000000000..d608793726 --- /dev/null +++ b/src/blocks/Table/index_deprecated.ts @@ -0,0 +1,16 @@ +import TableBlock from './Table'; +import {defaultValue, form} from './form'; + +const TableBlockConfig = { + type: 'table-block', + component: TableBlock, + schema: { + name: 'Table Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + }, +}; + +export default TableBlockConfig; diff --git a/src/blocks/Table/schema.ts b/src/blocks/Table/schema.ts index a6b1ecd2ea..464cd0d319 100644 --- a/src/blocks/Table/schema.ts +++ b/src/blocks/Table/schema.ts @@ -1,4 +1,8 @@ -import {BaseProps, BlockBaseProps, JustifyProps} from '../../schema/validators/common'; +import { + BaseProps, + BlockBaseProps, + JustifyProps, +} from '../../gravity-blocks/schema/validators/common'; export const TableBlock = { 'table-block': { @@ -11,6 +15,7 @@ export const TableBlock = { contentType: 'text', }, table: { + type: 'object', additionalProperties: false, required: ['content'], properties: { diff --git a/src/blocks/Tabs/TabContent/TabContent.tsx b/src/blocks/Tabs/TabContent/TabContent.tsx index bb503316f6..76ab7a42a8 100644 --- a/src/blocks/Tabs/TabContent/TabContent.tsx +++ b/src/blocks/Tabs/TabContent/TabContent.tsx @@ -6,9 +6,9 @@ import {FullscreenImage, YFMWrapper} from '../../../components'; import {getMediaImage} from '../../../components/Media/Image/utils'; import Media from '../../../components/Media/Media'; import {getHeight} from '../../../components/VideoBlock/VideoBlock'; -import {ProjectSettingsContext} from '../../../context/projectSettingsContext'; -import {useTheme} from '../../../context/theme'; -import {Col, GridColumnOrderClasses, Row} from '../../../grid'; +import {ProjectSettingsContext} from '../../../gravity-blocks/context/projectSettingsContext'; +import {useTheme} from '../../../gravity-blocks/context/theme'; +import {Col, GridColumnOrderClasses, Row} from '../../../gravity-blocks/grid'; import {ContentSize, TabsBlockItem} from '../../../models'; import {block, getThemedValue} from '../../../utils'; import {mergeVideoMicrodata} from '../../../utils/microdata'; diff --git a/src/blocks/Tabs/Tabs.tsx b/src/blocks/Tabs/Tabs.tsx index 24fcb535e4..2ab27b5fbf 100644 --- a/src/blocks/Tabs/Tabs.tsx +++ b/src/blocks/Tabs/Tabs.tsx @@ -5,7 +5,7 @@ import {getUniqId} from '@gravity-ui/uikit'; import AnimateBlock from '../../components/AnimateBlock/AnimateBlock'; import ButtonTabs, {ButtonTabsItemProps} from '../../components/ButtonTabs/ButtonTabs'; import Title from '../../components/Title/Title'; -import {Col, GridJustifyContent, Row} from '../../grid'; +import {Col, Grid, GridJustifyContent, Row} from '../../gravity-blocks/grid'; import {TabsBlockProps} from '../../models'; import {block} from '../../utils'; @@ -56,42 +56,44 @@ export const TabsBlock = ({ ); return ( - <AnimateBlock className={b()} onScroll={() => setPlay(true)} animate={animated}> - <Title - title={title} - subtitle={description} - className={b('title', {centered: centered})} - /> - <Row justifyContent={centered ? GridJustifyContent.Center : undefined}> - <Col sizes={tabsColSizes}> - <ButtonTabs - items={tabs} - onSelectTab={onSelectTab} - activeTab={activeTab} - className={b('tabs', {centered: centered})} - getTabElementId={getTabElementId} - getTabContentElementId={getTabContentElementId} - /> - </Col> - </Row> - {items.map((tabData) => { - const {tabName} = tabData; + <Grid> + <AnimateBlock className={b()} onScroll={() => setPlay(true)} animate={animated}> + <Title + title={title} + subtitle={description} + className={b('title', {centered: centered})} + /> + <Row justifyContent={centered ? GridJustifyContent.Center : undefined}> + <Col sizes={tabsColSizes}> + <ButtonTabs + items={tabs} + onSelectTab={onSelectTab} + activeTab={activeTab} + className={b('tabs', {centered: centered})} + getTabElementId={getTabElementId} + getTabContentElementId={getTabContentElementId} + /> + </Col> + </Row> + {items.map((tabData) => { + const {tabName} = tabData; - return ( - <TabContent - key={tabName} - tabData={tabData} - isActive={tabName === activeTab} - isReverse={isReverse} - contentSize={contentSize} - centered={centered} - play={play} - getTabElementId={getTabElementId} - getTabContentElementId={getTabContentElementId} - /> - ); - })} - </AnimateBlock> + return ( + <TabContent + key={tabName} + tabData={tabData} + isActive={tabName === activeTab} + isReverse={isReverse} + contentSize={contentSize} + centered={centered} + play={play} + getTabElementId={getTabElementId} + getTabContentElementId={getTabContentElementId} + /> + ); + })} + </AnimateBlock> + </Grid> ); }; diff --git a/src/blocks/Tabs/TabsTextContent/TabsTextContent.tsx b/src/blocks/Tabs/TabsTextContent/TabsTextContent.tsx index 33850e05b5..85396b561b 100644 --- a/src/blocks/Tabs/TabsTextContent/TabsTextContent.tsx +++ b/src/blocks/Tabs/TabsTextContent/TabsTextContent.tsx @@ -1,4 +1,4 @@ -import {Col} from '../../../grid'; +import {Col} from '../../../gravity-blocks/grid'; import {ImageDeviceProps, ImageObjectProps, TabsBlockItem, TabsBlockProps} from '../../../models'; import {Content} from '../../../sub-blocks'; import {block} from '../../../utils'; diff --git a/src/blocks/Tabs/__stories__/Tabs.stories.tsx b/src/blocks/Tabs/__stories__/Tabs.stories.tsx index c04e9f7afd..f12b3f31e6 100644 --- a/src/blocks/Tabs/__stories__/Tabs.stories.tsx +++ b/src/blocks/Tabs/__stories__/Tabs.stories.tsx @@ -3,12 +3,16 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {TabsBlockModel, TabsBlockProps} from '../../../models'; import Tabs, {TabsBlock} from '../Tabs'; +import {form} from '../form'; import data from './data.json'; export default { title: 'Blocks/Tabs', component: Tabs, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<TabsBlockModel> = (args) => ( diff --git a/src/blocks/Tabs/form.ts b/src/blocks/Tabs/form.ts new file mode 100644 index 0000000000..a02dc7e6f3 --- /dev/null +++ b/src/blocks/Tabs/form.ts @@ -0,0 +1,25 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {TabsBlock as TabsBlockSchema} from './schema'; + +export const form = generateFormFieldsFromAjvSchema( + TabsBlockSchema['tabs-block'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + title: 'Tabs Block', + items: [ + { + tabName: 'First Tab', + text: 'First Tab Content', + title: 'First Tab Title', + }, + { + text: 'Second Tab Content', + title: 'Second Tab Title', + tabName: 'Second Tab', + }, + ], +}; diff --git a/src/blocks/Tabs/icon.ts b/src/blocks/Tabs/icon.ts new file mode 100644 index 0000000000..21db33fc5b --- /dev/null +++ b/src/blocks/Tabs/icon.ts @@ -0,0 +1,16 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.97674" fill="white"/> +<rect x="9.11628" y="8.5" width="10" height="2" rx="0.569767" fill="black"/> +<rect x="20.1163" y="8.5" width="10" height="2" rx="0.569767" fill="black"/> +<rect x="31.1163" y="8.5" width="10" height="2" rx="0.569767" fill="black"/> +<rect x="42.1163" y="8.5" width="10" height="2" rx="0.569767" fill="black"/> +<rect x="9.11628" y="12.5" width="52" height="29" rx="3" fill="black"/> +<rect x="63.1163" y="12.5" width="13" height="2" rx="0.569767" fill="#262626"/> +<rect x="63.1163" y="15.5" width="23.7674" height="2" rx="0.569767" fill="#262626"/> +<rect x="63.1163" y="18.5" width="23.7674" height="2" rx="0.569767" fill="#262626"/> +<rect x="63.1163" y="22.5" width="7" height="2" rx="0.569767" fill="#262626"/> +</svg>`, +); diff --git a/src/blocks/Tabs/index.ts b/src/blocks/Tabs/index.ts new file mode 100644 index 0000000000..50d09fac09 --- /dev/null +++ b/src/blocks/Tabs/index.ts @@ -0,0 +1,17 @@ +import TabsBlock from './Tabs'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const TabsBlockConfig = { + type: '@gravity-ui/page-constructor/tabs-block', + component: TabsBlock, + schema: { + name: 'Tabs Block', + group: '@gravity-ui/page-constructor/Blocks', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default TabsBlockConfig; diff --git a/src/blocks/Tabs/index_deprecated.ts b/src/blocks/Tabs/index_deprecated.ts new file mode 100644 index 0000000000..3ff7362f9d --- /dev/null +++ b/src/blocks/Tabs/index_deprecated.ts @@ -0,0 +1,18 @@ +import TabsBlock from './Tabs'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const TabsBlockConfig = { + type: 'tabs-block', + component: TabsBlock, + schema: { + name: 'Tabs Block', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default TabsBlockConfig; diff --git a/src/blocks/Tabs/schema.ts b/src/blocks/Tabs/schema.ts index 1a67c85adc..75b56c7202 100644 --- a/src/blocks/Tabs/schema.ts +++ b/src/blocks/Tabs/schema.ts @@ -11,8 +11,8 @@ import { mediaDirection, sizeNumber, withTheme, -} from '../../schema/validators/common'; -import {filteredArray} from '../../schema/validators/utils'; +} from '../../gravity-blocks/schema/validators/common'; +import {filteredArray} from '../../gravity-blocks/schema/validators/utils'; import {ContentBase} from '../../sub-blocks/Content/schema'; const TabsItemContentProps = omit(ContentBase, ['size', 'colSizes', 'centered', 'theme']); diff --git a/src/blocks/index.ts b/src/blocks/index.ts index 709050ba73..3ff349f505 100644 --- a/src/blocks/index.ts +++ b/src/blocks/index.ts @@ -1,23 +1,138 @@ -export {default as BannerBlock} from './Banner/Banner'; -export {default as CompaniesBlock} from './Companies/Companies'; -export {default as InfoBlock} from './Info/Info'; -export {default as MediaBlock} from './Media/Media'; -export {default as MapBlock} from './Map/Map'; -export {default as SliderOldBlock} from './SliderOld/SliderOld'; -export {default as SliderBlock} from './Slider/Slider'; -export type {Swiper, SwiperOptions} from './Slider/Slider'; -export {default as ExtendedFeaturesBlock} from './ExtendedFeatures/ExtendedFeatures'; -export {default as PromoFeaturesBlock} from './PromoFeaturesBlock/PromoFeaturesBlock'; -export {default as QuestionsBlock} from './Questions/Questions'; -export {default as FoldableListBlock} from './FoldableList/FoldableList'; -export {default as TableBlock} from './Table/Table'; -export {default as TabsBlock} from './Tabs/Tabs'; -export {default as HeaderBlock} from './Header/Header'; -export {default as HeroBlock} from './Hero/Hero'; -export {default as IconsBlock} from './Icons/Icons'; -export {default as HeaderSliderBlock} from './HeaderSlider/HeaderSlider'; -export {default as CardLayoutBlock} from './CardLayout/CardLayout'; -export {default as ContentLayoutBlock} from './ContentLayout/ContentLayout'; -export {default as ShareBlock} from './Share/Share'; -export {default as FilterBlock} from './FilterBlock/FilterBlock'; -export {default as FormBlock} from './Form/Form'; +import {BlockData} from '../constructor-items'; +import BackgroundCardConfig from '../sub-blocks/BackgroundCard'; +import BackgroundCardConfigDeprecated from '../sub-blocks/BackgroundCard/index_deprecated'; +import BannerCardConfig from '../sub-blocks/BannerCard'; +import BannerCardConfigDeprecated from '../sub-blocks/BannerCard/index_deprecated'; +import BasicCardConfig from '../sub-blocks/BasicCard'; +import BasicCardConfigDeprecated from '../sub-blocks/BasicCard/index_deprecated'; +import ContentConfig from '../sub-blocks/Content'; +import ContentConfigDeprecated from '../sub-blocks/Content/index_deprecated'; +import DividerConfig from '../sub-blocks/Divider'; +import DividerConfigDeprecated from '../sub-blocks/Divider/index_deprecated'; +import ImageCardConfig from '../sub-blocks/ImageCard'; +import ImageCardConfigDeprecated from '../sub-blocks/ImageCard/index_deprecated'; +import LayoutItemConfig from '../sub-blocks/LayoutItem'; +import LayoutItemConfigDeprecated from '../sub-blocks/LayoutItem/index_deprecated'; +import MediaCardConfig from '../sub-blocks/MediaCard'; +import MediaCardConfigDeprecated from '../sub-blocks/MediaCard/index_deprecated'; +import PriceCardConfig from '../sub-blocks/PriceCard'; +import PriceCardConfigDeprecated from '../sub-blocks/PriceCard/index_deprecated'; +import PriceDetailedConfig from '../sub-blocks/PriceDetailed'; +import PriceDetailedConfigDeprecated from '../sub-blocks/PriceDetailed/index_deprecated'; +import QuoteConfig from '../sub-blocks/Quote'; +import QuoteConfigDeprecated from '../sub-blocks/Quote/index_deprecated'; + +import BannerBlockConfig from './Banner'; +import BannerBlockConfigDeprecated from './Banner/index_deprecated'; +import CardLayoutBlockConfig from './CardLayout'; +import CardLayoutBlockConfigDeprecated from './CardLayout/index_deprecated'; +import CompaniesBlockConfig from './Companies'; +import CompaniesBlockConfigDeprecated from './Companies/index_deprecated'; +import ContentLayoutBlockConfig from './ContentLayout'; +import ContentLayoutBlockConfigDeprecated from './ContentLayout/index_deprecated'; +import ExtendedFeaturesBlockConfig from './ExtendedFeatures'; +import ExtendedFeaturesBlockConfigDeprecated from './ExtendedFeatures/index_deprecated'; +import FilterBlockConfig from './FilterBlock'; +import FilterBlockConfigDeprecated from './FilterBlock/index_deprecated'; +import FoldableListBlockConfig from './FoldableList'; +import FoldableListBlockConfigDeprecated from './FoldableList/index_deprecated'; +import FormBlockConfig from './Form'; +import FormBlockConfigDeprecated from './Form/index_deprecated'; +import HeaderBlockConfig from './Header'; +import HeaderBlockConfigDeprecated from './Header/index_deprecated'; +import HeaderSliderBlockConfig from './HeaderSlider'; +import HeaderSliderBlockConfigDeprecated from './HeaderSlider/index_deprecated'; +import HeroBlockConfig from './Hero'; +import HeroBlockConfigDeprecated from './Hero/index_deprecated'; +import IconsBlockConfig from './Icons'; +import IconsBlockConfigDeprecated from './Icons/index_deprecated'; +import InfoBlockConfig from './Info'; +import InfoBlockConfigDeprecated from './Info/index_deprecated'; +import MapBlockConfig from './Map'; +import MapBlockConfigDeprecated from './Map/index_deprecated'; +import MediaBlockConfig from './Media'; +import MediaBlockConfigDeprecated from './Media/index_deprecated'; +import PromoFeaturesBlockConfig from './PromoFeaturesBlock'; +import PromoFeaturesBlockConfigDeprecated from './PromoFeaturesBlock/index_deprecated'; +import QuestionsBlockConfig from './Questions'; +import QuestionsBlockConfigDeprecated from './Questions/index_deprecated'; +import ShareBlockConfig from './Share'; +import ShareBlockConfigDeprecated from './Share/index_deprecated'; +import SliderBlockConfig from './Slider'; +import SliderBlockConfigDeprecated from './Slider/index_deprecated'; +import SliderOldBlockConfig from './SliderOld'; +import SliderOldBlockConfigDeprecated from './SliderOld/index_deprecated'; +import TableBlockConfig from './Table'; +import TableBlockConfigDeprecated from './Table/index_deprecated'; +import TabsBlockConfig from './Tabs'; +import TabsBlockConfigDeprecated from './Tabs/index_deprecated'; + +export const blocks: Array<BlockData> = [ + BannerBlockConfig, + BannerBlockConfigDeprecated, + CardLayoutBlockConfig, + CardLayoutBlockConfigDeprecated, + CompaniesBlockConfig, + CompaniesBlockConfigDeprecated, + ContentLayoutBlockConfig, + ContentLayoutBlockConfigDeprecated, + ExtendedFeaturesBlockConfig, + ExtendedFeaturesBlockConfigDeprecated, + FilterBlockConfig, + FilterBlockConfigDeprecated, + FoldableListBlockConfig, + FoldableListBlockConfigDeprecated, + FormBlockConfig, + FormBlockConfigDeprecated, + HeaderBlockConfig, + HeaderBlockConfigDeprecated, + HeaderSliderBlockConfig, + HeaderSliderBlockConfigDeprecated, + HeroBlockConfig, + HeroBlockConfigDeprecated, + IconsBlockConfig, + IconsBlockConfigDeprecated, + InfoBlockConfig, + InfoBlockConfigDeprecated, + MapBlockConfig, + MapBlockConfigDeprecated, + MediaBlockConfig, + MediaBlockConfigDeprecated, + PromoFeaturesBlockConfig, + PromoFeaturesBlockConfigDeprecated, + QuestionsBlockConfig, + QuestionsBlockConfigDeprecated, + ShareBlockConfig, + ShareBlockConfigDeprecated, + SliderBlockConfig, + SliderBlockConfigDeprecated, + SliderOldBlockConfig, + SliderOldBlockConfigDeprecated, + TableBlockConfig, + TableBlockConfigDeprecated, + TabsBlockConfig, + TabsBlockConfigDeprecated, + + BackgroundCardConfig, + BackgroundCardConfigDeprecated, + BannerCardConfig, + BannerCardConfigDeprecated, + BasicCardConfig, + BasicCardConfigDeprecated, + ContentConfig, + ContentConfigDeprecated, + DividerConfig, + DividerConfigDeprecated, + ImageCardConfig, + ImageCardConfigDeprecated, + LayoutItemConfig, + LayoutItemConfigDeprecated, + MediaCardConfig, + MediaCardConfigDeprecated, + PriceCardConfig, + PriceCardConfigDeprecated, + PriceDetailedConfig, + PriceDetailedConfigDeprecated, + QuoteConfig, + QuoteConfigDeprecated, +]; diff --git a/src/common/constants.ts b/src/common/constants.ts new file mode 100644 index 0000000000..d11b03d6ce --- /dev/null +++ b/src/common/constants.ts @@ -0,0 +1 @@ +export const POST_MESSAGE_SOURCE = 'page-constructor-editor'; diff --git a/src/common/postMessage.ts b/src/common/postMessage.ts new file mode 100644 index 0000000000..4a40cebd18 --- /dev/null +++ b/src/common/postMessage.ts @@ -0,0 +1,53 @@ +import * as React from 'react'; + +import {POST_MESSAGE_SOURCE} from './constants'; +import {ActionMessageTypes, EventMessageTypes, PostMessageAPIMessage} from './types'; + +export function isValidPostMessage(data: unknown): data is Record<string, unknown> { + return ( + typeof data === 'object' && + data !== null && + (data as Record<string, unknown>).source === POST_MESSAGE_SOURCE + ); +} + +export function requestActionPostMessage<K extends keyof ActionMessageTypes>( + action: K, + data: ActionMessageTypes[K], + destinationElement: Window, +) { + const message = {action, data, source: POST_MESSAGE_SOURCE} as PostMessageAPIMessage<K>; + destinationElement.postMessage(message, '*'); +} + +export function listenPostMessageEvents<K extends keyof EventMessageTypes>( + action: K, + callback: (data: EventMessageTypes[K]) => void, +) { + const onMessage = (e: MessageEvent) => { + if (!isValidPostMessage(e.data)) { + return undefined; + } + + const message = e.data as PostMessageAPIMessage<K>; + if (message.action === action) { + return callback(message.data); + } + + return undefined; + }; + + window.addEventListener('message', onMessage); + return () => window.removeEventListener('message', onMessage); +} + +export function usePostMessageAPIListener<K extends keyof EventMessageTypes>( + action: K, + callback: (data: EventMessageTypes[K]) => void, + deps: unknown[] = [], +) { + React.useEffect(() => { + return listenPostMessageEvents(action, callback); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [...deps]); +} diff --git a/src/common/store.ts b/src/common/store.ts new file mode 100644 index 0000000000..22141a838d --- /dev/null +++ b/src/common/store.ts @@ -0,0 +1,59 @@ +import {Fields} from '../form-generator-v2/types'; +import {PageContent} from '../models'; + +import {ItemConfig} from './types'; +import {RectMapEntry} from './types/rect'; +import {initializeStore} from './utils'; + +/** Undo/redo slice for editor-v2 (not synced to preview iframe payload). */ +export interface EditorHistorySnapshot { + content: PageContent; + selectedBlock: number[] | null; +} + +export interface EditorState { + height?: number; + deviceWidth?: string; + zoom: number; + + manipulateOverlayMode: 'insert' | 'reorder' | false; + selectedBlock: number[] | null; + initialized: boolean; + isPreviewMode: boolean; + + content: PageContent; + blocks: Array<ItemConfig>; + subBlocks: Array<ItemConfig>; + global: Fields; + + preInsertBlockType: string | null; + preReorderBlockPath: number[] | null; + + rectMap: RectMapEntry[]; + + historyPast: EditorHistorySnapshot[]; + historyFuture: EditorHistorySnapshot[]; +} + +export const initialStore: EditorState = { + height: 100, + deviceWidth: '100%', + zoom: 100, + manipulateOverlayMode: false, + selectedBlock: null, + initialized: false, + isPreviewMode: false, + content: {blocks: []}, + blocks: [], + subBlocks: [], + global: [], + preInsertBlockType: null, + preReorderBlockPath: null, + + rectMap: [], + + historyPast: [], + historyFuture: [], +}; + +export const createPCEditorStore = initializeStore<EditorState>(initialStore, () => ({})); diff --git a/src/common/types/actions.ts b/src/common/types/actions.ts new file mode 100644 index 0000000000..094ccc995a --- /dev/null +++ b/src/common/types/actions.ts @@ -0,0 +1,23 @@ +import {PageContent} from '../../models'; +import {EditorState} from '../store'; + +import {RectMapEntry} from './rect'; + +export type MessageTypes = EventMessageTypes & ActionMessageTypes; + +export type EventMessageTypes = { + ON_INIT: {height: number}; + ON_RESIZE: {height: number}; + ON_UPDATE_RECT_MAP: {rects: RectMapEntry[]}; + ON_SUPPORTED_BLOCKS: Pick<EditorState, 'blocks' | 'subBlocks' | 'global'>; + ON_INITIAL_CONTENT: PageContent; + /** Iframe → parent: user pressed Cmd/Ctrl+Z while preview had focus (parent runs editor undo). */ + ON_EDITOR_UNDO: {}; + /** Iframe → parent: Cmd/Ctrl+Shift+Z */ + ON_EDITOR_REDO: {}; +}; + +export type ActionMessageTypes = { + GET_SUPPORTED_BLOCKS: {}; + GET_INITIAL_CONTENT: {}; +}; diff --git a/src/common/types/common.ts b/src/common/types/common.ts new file mode 100644 index 0000000000..9d1cd10439 --- /dev/null +++ b/src/common/types/common.ts @@ -0,0 +1,19 @@ +import * as React from 'react'; + +import {BlockConfig} from '../../form-generator-v2/types'; + +export interface ItemConfig { + type: string; + schema: BlockConfig; +} + +export interface PageConstructorWrapperProps extends React.PropsWithChildren {} + +export type PageConstructorWrapper<WrapperProps> = React.ComponentType< + WrapperProps & PageConstructorWrapperProps +>; + +export interface PageConstructorSettings<WrapperProps> { + wrapper?: PageConstructorWrapper<WrapperProps>; + wrapperProps?: WrapperProps; +} diff --git a/src/common/types/index.ts b/src/common/types/index.ts new file mode 100644 index 0000000000..e39581a2a2 --- /dev/null +++ b/src/common/types/index.ts @@ -0,0 +1,4 @@ +export * from './actions'; +export * from './common'; +export * from './messages'; +export * from './rect'; diff --git a/src/common/types/messages.ts b/src/common/types/messages.ts new file mode 100644 index 0000000000..779193fb3a --- /dev/null +++ b/src/common/types/messages.ts @@ -0,0 +1,15 @@ +import {POST_MESSAGE_SOURCE} from '../constants'; +import {EditorState} from '../store'; + +import {MessageTypes} from './actions'; + +export type PostMessageAPIMessage<K extends keyof MessageTypes> = { + action: K; + data: MessageTypes[K]; + source: typeof POST_MESSAGE_SOURCE; +}; + +export type StoreSyncMessage = { + state: EditorState; + source: typeof POST_MESSAGE_SOURCE; +}; diff --git a/src/common/types/rect.ts b/src/common/types/rect.ts new file mode 100644 index 0000000000..1cb444583d --- /dev/null +++ b/src/common/types/rect.ts @@ -0,0 +1,29 @@ +export interface SerializableRect { + x: number; + y: number; + width: number; + height: number; + top: number; + left: number; + right: number; + bottom: number; +} + +export interface RectMapEntry { + path: number[]; + rect: SerializableRect; + dropZone?: boolean; +} + +export function toSerializableRect(rect: DOMRect): SerializableRect { + return { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + }; +} diff --git a/src/common/utils.ts b/src/common/utils.ts new file mode 100644 index 0000000000..666e535732 --- /dev/null +++ b/src/common/utils.ts @@ -0,0 +1,35 @@ +import {StoreApi, create} from 'zustand'; +import {devtools, subscribeWithSelector} from 'zustand/middleware'; + +export function initializeStore<State extends {}, Methods extends {} = {}>( + initialState: State, + methods: ( + set: StoreApi<State & Methods>['setState'], + get: StoreApi<State & Methods>['getState'], + ) => Methods, +) { + return (overrideInitialState?: Partial<State>) => { + return create< + State & Methods, + [ + ['zustand/subscribeWithSelector', State & Methods], + ['zustand/devtools', never], + ['zustand/persist', State & Methods], + ] + >( + subscribeWithSelector( + devtools((set, get) => { + return { + ...initialState, + ...overrideInitialState, + ...methods(set, get), + }; + }), + ), + ); + }; +} + +export const removeFn = (object: object) => { + return JSON.parse(JSON.stringify(object)); +}; diff --git a/src/components/AnimateBlock/AnimateBlock.tsx b/src/components/AnimateBlock/AnimateBlock.tsx index 6d3c381f90..3c5eed954d 100644 --- a/src/components/AnimateBlock/AnimateBlock.tsx +++ b/src/components/AnimateBlock/AnimateBlock.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import {Waypoint} from 'react-waypoint'; -import {AnimateContext, AnimateContextProps} from '../../context/animateContext/AnimateContext'; +import {AnimateContext, AnimateContextProps} from '../../gravity-blocks/context/animateContext'; import {QAProps} from '../../models'; import {block} from '../../utils'; diff --git a/src/components/Author/schema.ts b/src/components/Author/schema.ts index ba9644e66c..d8baef2f7c 100644 --- a/src/components/Author/schema.ts +++ b/src/components/Author/schema.ts @@ -1,4 +1,4 @@ -import {BaseProps, authorItem} from '../../schema/validators/common'; +import {BaseProps, authorItem} from '../../gravity-blocks/schema/validators/common'; export const author = { author: { diff --git a/src/components/BackLink/BackLink.tsx b/src/components/BackLink/BackLink.tsx index e5fdd82073..91d7d239ae 100644 --- a/src/components/BackLink/BackLink.tsx +++ b/src/components/BackLink/BackLink.tsx @@ -3,8 +3,8 @@ import * as React from 'react'; import {ArrowLeft} from '@gravity-ui/icons'; import {Button, ButtonSize, Icon, ButtonProps as UIKitButtonProps} from '@gravity-ui/uikit'; -import {LocationContext} from '../../context/locationContext'; -import {useAnalytics} from '../../hooks'; +import {LocationContext} from '../../gravity-blocks/context/locationContext'; +import {useAnalytics} from '../../gravity-blocks/hooks'; import {DefaultEventNames, Tabbable} from '../../models'; export type Theme = 'default' | 'special'; diff --git a/src/components/BackLink/__stories__/BackLink.stories.tsx b/src/components/BackLink/__stories__/BackLink.stories.tsx index 2a119a6880..550c2b2324 100644 --- a/src/components/BackLink/__stories__/BackLink.stories.tsx +++ b/src/components/BackLink/__stories__/BackLink.stories.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import {Meta, StoryFn} from '@storybook/react'; -import {Col, Row} from '../../../grid'; +import {Col, Row} from '../../../gravity-blocks/grid'; import BackLink, {BackLinkProps} from '../BackLink'; import data from './data.json'; diff --git a/src/components/BackLink/__tests__/BackLink.test.tsx b/src/components/BackLink/__tests__/BackLink.test.tsx index 66c9509619..88d6998391 100644 --- a/src/components/BackLink/__tests__/BackLink.test.tsx +++ b/src/components/BackLink/__tests__/BackLink.test.tsx @@ -2,7 +2,7 @@ import {ButtonSize} from '@gravity-ui/uikit'; import {render, screen} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import {History, LocationContext} from '../../../context/locationContext'; +import {History, LocationContext} from '../../../gravity-blocks/context/locationContext'; import BackLink, {Theme} from '../BackLink'; const backLinkProps = { diff --git a/src/components/BackgroundMedia/BackgroundMedia.tsx b/src/components/BackgroundMedia/BackgroundMedia.tsx index b676147cc0..fbd549d65a 100644 --- a/src/components/BackgroundMedia/BackgroundMedia.tsx +++ b/src/components/BackgroundMedia/BackgroundMedia.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {MobileContext} from '../../context/mobileContext'; +import {MobileContext} from '../../gravity-blocks/context/mobileContext'; import {BackgroundMediaProps} from '../../models'; import {block, getQaAttrubutes} from '../../utils'; import AnimateBlock from '../AnimateBlock/AnimateBlock'; diff --git a/src/components/BalancedMasonry/BalancedMasonry.tsx b/src/components/BalancedMasonry/BalancedMasonry.tsx index aaf1367a87..ab59b482a2 100644 --- a/src/components/BalancedMasonry/BalancedMasonry.tsx +++ b/src/components/BalancedMasonry/BalancedMasonry.tsx @@ -4,7 +4,7 @@ import debounce from 'lodash/debounce'; import first from 'lodash/first'; import minBy from 'lodash/minBy'; -import {SSRContext} from '../../context/ssrContext'; +import {SSRContext} from '../../gravity-blocks/context/ssrContext'; import {QAProps} from '../../models'; import {block, getQaAttrubutes} from '../../utils'; diff --git a/src/components/BlockBase/BlockBase.scss b/src/components/BlockBase/BlockBase.scss index 01221dfc34..88c2fc6566 100644 --- a/src/components/BlockBase/BlockBase.scss +++ b/src/components/BlockBase/BlockBase.scss @@ -14,12 +14,12 @@ $block: '.#{$ns}block-base'; @include add-specificity(&) { @media only screen and (max-width: map-get($gridBreakpoints, 'sm')) { - margin-top: $indentM; + padding-top: $indentM; padding-bottom: $indentM; - &:first-child { - margin-top: var(--pc-first-block-mobile-indent, #{$indentXL}); - } + //&:first-child { + // padding-top: var(--pc-first-block-mobile-indent, #{$indentXL}); + //} } } diff --git a/src/components/BlockBase/BlockBase.tsx b/src/components/BlockBase/BlockBase.tsx index ba8c54886f..4cbc5591d3 100644 --- a/src/components/BlockBase/BlockBase.tsx +++ b/src/components/BlockBase/BlockBase.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {Col} from '../../grid'; +import {Col} from '../../gravity-blocks/grid'; import {BlockBaseProps, ClassNameProps, QAProps} from '../../models'; import {block} from '../../utils'; import Anchor from '../Anchor/Anchor'; diff --git a/src/components/BlockBase/__tests__/BlockBase.test.tsx b/src/components/BlockBase/__tests__/BlockBase.test.tsx index a5f8d1495a..b44801c57e 100644 --- a/src/components/BlockBase/__tests__/BlockBase.test.tsx +++ b/src/components/BlockBase/__tests__/BlockBase.test.tsx @@ -2,7 +2,7 @@ import {render, screen} from '@testing-library/react'; import {testCustomClassName} from '../../../../test-utils/shared/common'; import {qaIdByDefault} from '../../../components/Anchor/Anchor'; -import {GridColumnSize, IndentValue} from '../../../grid'; +import {GridColumnSize, IndentValue} from '../../../gravity-blocks/grid'; import {ClassNameProps} from '../../../models'; import BlockBase, {BlockBaseFullProps} from '../BlockBase'; diff --git a/src/components/BrandFooter/BrandFooter.tsx b/src/components/BrandFooter/BrandFooter.tsx index 6f6120c50c..5500932cc8 100644 --- a/src/components/BrandFooter/BrandFooter.tsx +++ b/src/components/BrandFooter/BrandFooter.tsx @@ -1,9 +1,9 @@ import {Link} from '@gravity-ui/uikit'; -import {useTheme} from '../../context/theme'; -import {BrandIconDark} from '../../icons/BrandIconDark'; -import {BrandIconLight} from '../../icons/BrandIconLight'; -import {BrandName} from '../../icons/BrandName'; +import {useTheme} from '../../gravity-blocks/context/theme'; +import {BrandIconDark} from '../../gravity-blocks/icons/BrandIconDark'; +import {BrandIconLight} from '../../gravity-blocks/icons/BrandIconLight'; +import {BrandName} from '../../gravity-blocks/icons/BrandName'; import type {ClassNameProps} from '../../models'; import {Theme} from '../../models'; import {block} from '../../utils'; diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 1c73da4c0d..ae1b09de83 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -8,9 +8,9 @@ import { ButtonProps as UIKitButtonProps, } from '@gravity-ui/uikit'; -import {LocaleContext} from '../../context/localeContext/localeContext'; -import {useAnalytics} from '../../hooks'; -import {Github} from '../../icons'; +import {LocaleContext} from '../../gravity-blocks/context/localeContext'; +import {useAnalytics} from '../../gravity-blocks/hooks'; +import {Github} from '../../gravity-blocks/icons'; import {ButtonProps as ButtonParams, DefaultEventNames, QAProps} from '../../models'; import {block, setUrlTld} from '../../utils'; import {getGravityIcon} from '../../utils/icons'; diff --git a/src/components/Button/__stories__/Button.stories.tsx b/src/components/Button/__stories__/Button.stories.tsx index c8c1e5ee16..6f0fa86f67 100644 --- a/src/components/Button/__stories__/Button.stories.tsx +++ b/src/components/Button/__stories__/Button.stories.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import {Meta, StoryFn} from '@storybook/react'; -import {Col, Grid, GridAlignItems, Row} from '../../../grid'; +import {Col, Grid, GridAlignItems, Row} from '../../../gravity-blocks/grid'; import Button, {ButtonProps} from '../Button'; import {CONTRAST_THEMES, SIZES, THEMES} from './constants'; diff --git a/src/components/CardBase/CardBase.tsx b/src/components/CardBase/CardBase.tsx index dd34f4a4ad..e3cdc91c40 100644 --- a/src/components/CardBase/CardBase.tsx +++ b/src/components/CardBase/CardBase.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import {Link} from '@gravity-ui/uikit'; -import {useAnalytics} from '../../hooks'; +import {useAnalytics} from '../../gravity-blocks/hooks'; import { AnalyticsEventsBase, CardBaseProps as CardBaseParams, diff --git a/src/components/CardBase/__tests__/CardBase.test.tsx b/src/components/CardBase/__tests__/CardBase.test.tsx index d4311e075e..64ffa39c41 100644 --- a/src/components/CardBase/__tests__/CardBase.test.tsx +++ b/src/components/CardBase/__tests__/CardBase.test.tsx @@ -6,7 +6,10 @@ import userEvent from '@testing-library/user-event'; import {TARGETS} from '../../../../test-utils/constants'; import {testCustomClassName} from '../../../../test-utils/shared/common'; import {PageConstructorProvider} from '../../../containers/PageConstructor'; -import {AnalyticsContextProps} from '../../../context/analyticsContext'; +import { + AnalyticsContext, + AnalyticsContextProps, +} from '../../../gravity-blocks/context/analyticsContext'; import {CardBorder} from '../../../models'; import {getQaAttrubutes} from '../../../utils'; import CardBase, {CardBasePropsType} from '../CardBase'; @@ -180,11 +183,18 @@ describe('CardBase', () => { const user = userEvent.setup(); render( - <PageConstructorProvider analytics={analyticsContext}> - <CardBase url={url} target={'_blank'} qa={qaId} analyticsEvents={[{name: 'click'}]}> - <CardBase.Content>Content</CardBase.Content> - </CardBase> - </PageConstructorProvider>, + <AnalyticsContext.Provider value={analyticsContext}> + <PageConstructorProvider> + <CardBase + url={url} + target={'_blank'} + qa={qaId} + analyticsEvents={[{name: 'click'}]} + > + <CardBase.Content>Content</CardBase.Content> + </CardBase> + </PageConstructorProvider> + </AnalyticsContext.Provider>, ); const cardBase = screen.getByTestId(qaId); diff --git a/src/components/ContentIcon/ContentIcon.tsx b/src/components/ContentIcon/ContentIcon.tsx index 8700991f66..ce1b695f42 100644 --- a/src/components/ContentIcon/ContentIcon.tsx +++ b/src/components/ContentIcon/ContentIcon.tsx @@ -1,4 +1,4 @@ -import {useTheme} from '../../context/theme'; +import {useTheme} from '../../gravity-blocks/context/theme'; import {ClassNameProps, GravityIconProps, ImageProps, QAProps, SVGIcon} from '../../models'; import {ThemeSupporting, getThemedValue} from '../../utils'; import Icon from '../Icon/Icon'; diff --git a/src/components/Control/__stories__/Control.stories.tsx b/src/components/Control/__stories__/Control.stories.tsx index 1e5453fcbf..0b00dc88fb 100644 --- a/src/components/Control/__stories__/Control.stories.tsx +++ b/src/components/Control/__stories__/Control.stories.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import {Meta, StoryFn} from '@storybook/react'; -import {Col, GridAlignItems, Row} from '../../../grid'; +import {Col, GridAlignItems, Row} from '../../../gravity-blocks/grid'; import Control, {ControlProps} from '../Control'; import data from './data.json'; diff --git a/src/components/FileLink/FileLink.tsx b/src/components/FileLink/FileLink.tsx index eb06a122d6..4d38acfc30 100644 --- a/src/components/FileLink/FileLink.tsx +++ b/src/components/FileLink/FileLink.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import {Label, LabelProps} from '@gravity-ui/uikit'; -import {LocationContext} from '../../context/locationContext'; +import {LocationContext} from '../../gravity-blocks/context/locationContext'; import {FileLinkProps, TextSize} from '../../models'; import {block, getLinkProps} from '../../utils'; diff --git a/src/components/FileLink/__stories__/FileLink.stories.tsx b/src/components/FileLink/__stories__/FileLink.stories.tsx index a9b683664d..0168c24e97 100644 --- a/src/components/FileLink/__stories__/FileLink.stories.tsx +++ b/src/components/FileLink/__stories__/FileLink.stories.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import {Meta, StoryFn} from '@storybook/react'; -import {Col, Row} from '../../../grid'; +import {Col, Row} from '../../../gravity-blocks/grid'; import {FileLinkProps} from '../../../models'; import {FileLink} from '../../index'; diff --git a/src/components/Foldable/Foldable.tsx b/src/components/Foldable/Foldable.tsx index 81ed7838cd..6e443caea6 100644 --- a/src/components/Foldable/Foldable.tsx +++ b/src/components/Foldable/Foldable.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import useHeightCalculator from '../../hooks/useHeightCalculator'; +import useHeightCalculator from '../../gravity-blocks/hooks/useHeightCalculator'; import {QAProps} from '../../models'; import {block, getQaAttrubutes} from '../../utils'; diff --git a/src/components/FullscreenImage/FullscreenImage.tsx b/src/components/FullscreenImage/FullscreenImage.tsx index 7fb6c4c689..55e069e1da 100644 --- a/src/components/FullscreenImage/FullscreenImage.tsx +++ b/src/components/FullscreenImage/FullscreenImage.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import {ChevronsExpandUpRight, Xmark} from '@gravity-ui/icons'; import {Icon, Modal} from '@gravity-ui/uikit'; -import {SliderBlock} from '../../blocks'; +import {default as SliderBlock} from '../../blocks/Slider/Slider'; import {ImageProps as ModelImageProps, SliderType} from '../../models'; import {block} from '../../utils'; import Image, {ImageProps} from '../Image/Image'; diff --git a/src/components/FullscreenMedia/FullscreenMedia.tsx b/src/components/FullscreenMedia/FullscreenMedia.tsx index 4d73ba3535..36107bf5e4 100644 --- a/src/components/FullscreenMedia/FullscreenMedia.tsx +++ b/src/components/FullscreenMedia/FullscreenMedia.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import {ChevronsExpandUpRight, Xmark} from '@gravity-ui/icons'; import {Button, Icon, Modal} from '@gravity-ui/uikit'; -import {MobileContext} from '../../context/mobileContext'; +import {MobileContext} from '../../gravity-blocks/context/mobileContext'; import {block} from '../../utils'; import {MediaAllProps} from '../Media/Media'; diff --git a/src/components/HeaderBreadcrumbs/HeaderBreadcrumbs.tsx b/src/components/HeaderBreadcrumbs/HeaderBreadcrumbs.tsx index 8950246f26..6ff2f73248 100644 --- a/src/components/HeaderBreadcrumbs/HeaderBreadcrumbs.tsx +++ b/src/components/HeaderBreadcrumbs/HeaderBreadcrumbs.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {useAnalytics} from '../../hooks'; +import {useAnalytics} from '../../gravity-blocks/hooks'; import {DefaultEventNames, HeaderBreadCrumbsProps} from '../../models'; import {block, getQaAttrubutes} from '../../utils'; diff --git a/src/components/Image/Image.tsx b/src/components/Image/Image.tsx index 5031e546b4..20b4f79d0e 100644 --- a/src/components/Image/Image.tsx +++ b/src/components/Image/Image.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import {BREAKPOINTS} from '../../constants'; -import {ProjectSettingsContext} from '../../context/projectSettingsContext'; -import {useImageSize} from '../../hooks'; +import {ProjectSettingsContext} from '../../gravity-blocks/context/projectSettingsContext'; +import {useImageSize} from '../../gravity-blocks/hooks'; import {Device, ImageDeviceProps, ImageObjectProps, QAProps} from '../../models'; import {getQaAttrubutes} from '../../utils'; import {isCompressible} from '../../utils/imageCompress'; diff --git a/src/components/Image/dynamic-form.ts b/src/components/Image/dynamic-form.ts new file mode 100644 index 0000000000..4871a5d268 --- /dev/null +++ b/src/components/Image/dynamic-form.ts @@ -0,0 +1,87 @@ +import _ from 'lodash'; + +import {ConfigInput} from '../../form-generator'; + +const devices = ['desktop', 'tablet', 'mobile']; + +const imageBaseInputs: ConfigInput[] = [ + { + type: 'text', + name: 'alt', + title: 'Alternative', + }, + { + type: 'boolean', + name: 'disableCompress', + title: 'Disable Compress', + }, +]; + +const imageStyleInputs: ConfigInput[] = [ + { + type: 'text', + name: 'style.backgroundColor', + title: 'Background Color', + }, + { + type: 'text', + name: 'style.height', + title: 'Height', + }, + { + type: 'text', + name: 'style.width', + title: 'Width', + }, + { + type: 'text', + name: 'style.color', + title: 'Color', + }, +]; + +const devicesInputs: ConfigInput[] = devices.map((device) => ({ + type: 'text', + title: _.capitalize(device), + name: `${device}`, +})); + +export const imageInputs: ConfigInput[] = [ + { + type: 'oneOf', + name: '', + key: 'imageType', + title: 'Image Type', + options: [ + { + title: 'Simple', + value: 'simple', + properties: [ + { + type: 'text', + name: '', // image props + title: 'Image URL', + }, + ], + }, + { + title: 'Complex', + value: 'complex', + properties: [ + { + type: 'text', + name: 'src', + title: 'Source', + }, + ...imageStyleInputs, + ...imageBaseInputs, + ], + }, + { + title: 'Device Based', + value: 'deviseBased', + properties: [...devicesInputs, ...imageBaseInputs], + }, + ], + }, +]; diff --git a/src/components/Image/schema.ts b/src/components/Image/schema.ts index c632476eaf..f5bd7fb0de 100644 --- a/src/components/Image/schema.ts +++ b/src/components/Image/schema.ts @@ -1,4 +1,4 @@ -import {filteredItem} from '../../schema/validators/utils'; +import {filteredItem} from '../../gravity-blocks/schema/validators/utils'; export const imageUrlPattern = '^((http[s]?|ftp):\\/)?\\/?([^:\\/\\s]+)((\\/\\w+)*\\/)([\\w\\-\\.]+[^#?\\s]+)(.*)?(#[\\w\\-]+)?$'; diff --git a/src/components/ImageBase/ImageBase.tsx b/src/components/ImageBase/ImageBase.tsx index 09b5d18199..75b4032e9e 100644 --- a/src/components/ImageBase/ImageBase.tsx +++ b/src/components/ImageBase/ImageBase.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {ImageContext} from '../../context/imageContext/imageContext'; +import {ImageContext} from '../../gravity-blocks/context/imageContext'; import {ImageObjectProps} from '../../models'; export interface ImageBaseProps extends Partial<ImageObjectProps> { diff --git a/src/components/InnerForm/InnerForm.tsx b/src/components/InnerForm/InnerForm.tsx index d9c5249424..7bf2313cf4 100644 --- a/src/components/InnerForm/InnerForm.tsx +++ b/src/components/InnerForm/InnerForm.tsx @@ -5,8 +5,8 @@ import { FormsContext, HubspotFormsContextProps, YandexFormsContextProps, -} from '../../context/formsContext/FormsContext'; -import {useTheme} from '../../context/theme'; +} from '../../gravity-blocks/context/formsContext/FormsContext'; +import {useTheme} from '../../gravity-blocks/context/theme'; import {FormBlockData, isHubspotDataForm, isYandexDataForm} from '../../models'; import {HubspotForm} from '../../sub-blocks'; import {getThemedValue} from '../../utils'; diff --git a/src/components/Link/Link.tsx b/src/components/Link/Link.tsx index 6b9e63303c..06e31be7c7 100644 --- a/src/components/Link/Link.tsx +++ b/src/components/Link/Link.tsx @@ -3,9 +3,9 @@ import * as React from 'react'; import {ChevronRight} from '@gravity-ui/icons'; import {Icon} from '@gravity-ui/uikit'; -import {LocaleContext} from '../../context/localeContext'; -import {LocationContext} from '../../context/locationContext'; -import {useAnalytics} from '../../hooks'; +import {LocaleContext} from '../../gravity-blocks/context/localeContext'; +import {LocationContext} from '../../gravity-blocks/context/locationContext'; +import {useAnalytics} from '../../gravity-blocks/hooks'; import { ClassNameProps, DefaultEventNames, diff --git a/src/components/Link/__stories__/Link.stories.tsx b/src/components/Link/__stories__/Link.stories.tsx index eb581c50fc..f0a0316051 100644 --- a/src/components/Link/__stories__/Link.stories.tsx +++ b/src/components/Link/__stories__/Link.stories.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import {Meta, StoryFn} from '@storybook/react'; -import {Col, GridAlignItems, Row} from '../../../grid'; +import {Col, GridAlignItems, Row} from '../../../gravity-blocks/grid'; import Link, {LinkFullProps} from '../Link'; import data from './data.json'; diff --git a/src/components/Map/GoogleMap.tsx b/src/components/Map/GoogleMap.tsx index 57065e4907..6944efcaab 100644 --- a/src/components/Map/GoogleMap.tsx +++ b/src/components/Map/GoogleMap.tsx @@ -3,9 +3,9 @@ import * as React from 'react'; import {Lang} from '@gravity-ui/uikit'; import debounce from 'lodash/debounce'; -import {LocaleContext} from '../../context/localeContext/localeContext'; -import {MapsContext} from '../../context/mapsContext/mapsContext'; -import {MobileContext} from '../../context/mobileContext'; +import {LocaleContext} from '../../gravity-blocks/context/localeContext'; +import {MapsContext} from '../../gravity-blocks/context/mapsContext/mapsContext'; +import {MobileContext} from '../../gravity-blocks/context/mobileContext'; import {GMapProps} from '../../models'; import {block} from '../../utils'; diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index 30635807d6..df0aadb21e 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {MapType, MapsContext} from '../../context/mapsContext/mapsContext'; +import {MapType, MapsContext} from '../../gravity-blocks/context/mapsContext/mapsContext'; import {GMapProps, MapProps, YMapProps} from '../../models'; import GoogleMap from './GoogleMap'; diff --git a/src/components/Map/YMap/YandexMap.tsx b/src/components/Map/YMap/YandexMap.tsx index d24167b8ab..56e07c6dc4 100644 --- a/src/components/Map/YMap/YandexMap.tsx +++ b/src/components/Map/YMap/YandexMap.tsx @@ -3,9 +3,9 @@ import * as React from 'react'; import {Spin} from '@gravity-ui/uikit'; import debounce from 'lodash/debounce'; -import {LocaleContext} from '../../../context/localeContext/localeContext'; -import {MapsContext} from '../../../context/mapsContext/mapsContext'; -import {MobileContext} from '../../../context/mobileContext'; +import {LocaleContext} from '../../../gravity-blocks/context/localeContext'; +import {MapsContext} from '../../../gravity-blocks/context/mapsContext/mapsContext'; +import {MobileContext} from '../../../gravity-blocks/context/mobileContext'; import {YMapMarker, YMapMarkerLabelPrivate, YMapMarkerPrivate, YMapProps} from '../../../models'; import {block} from '../../../utils'; import ErrorWrapper from '../../ErrorWrapper/ErrorWrapper'; diff --git a/src/components/Map/YMap/YandexMapApiLoader.ts b/src/components/Map/YMap/YandexMapApiLoader.ts index 7773946a6f..8d00af1ec7 100644 --- a/src/components/Map/YMap/YandexMapApiLoader.ts +++ b/src/components/Map/YMap/YandexMapApiLoader.ts @@ -1,4 +1,4 @@ -import {Maplangs} from '../../../context/mapsContext/mapsContext'; +import {Maplangs} from '../../../gravity-blocks/context/mapsContext/mapsContext'; import {loadScript} from '../../../utils'; export enum MapApiStatus { diff --git a/src/components/Map/__stories__/ApiKeyInput.tsx b/src/components/Map/__stories__/ApiKeyInput.tsx index 8578a3f4d4..56b8ea7132 100644 --- a/src/components/Map/__stories__/ApiKeyInput.tsx +++ b/src/components/Map/__stories__/ApiKeyInput.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import {TextInput} from '@gravity-ui/uikit'; import {Button} from '../..'; -import {useMapApiKey} from '../../../context/mapsContext/useMap'; +import {useMapApiKey} from '../../../gravity-blocks/context/mapsContext/useMap'; import './ApiKeyInput.scss'; diff --git a/src/components/Map/__stories__/Map.stories.tsx b/src/components/Map/__stories__/Map.stories.tsx index 44ef2c8423..e65bec8313 100644 --- a/src/components/Map/__stories__/Map.stories.tsx +++ b/src/components/Map/__stories__/Map.stories.tsx @@ -1,8 +1,11 @@ import {Meta, StoryFn} from '@storybook/react'; import {scriptsSrc, ymapApiKeyForStorybook} from '../../../../.storybook/maps'; -import {MapType} from '../../../context/mapsContext/mapsContext'; -import {MapProvider, gmapApiKeyIdInLS} from '../../../context/mapsContext/mapsProvider'; +import {MapType} from '../../../gravity-blocks/context/mapsContext/mapsContext'; +import { + MapProvider, + gmapApiKeyIdInLS, +} from '../../../gravity-blocks/context/mapsContext/mapsProvider'; import {MapProps} from '../../../models'; import Map from '../Map'; diff --git a/src/components/Media/DataLens/DataLens.tsx b/src/components/Media/DataLens/DataLens.tsx index 25a1b59175..ed68a82777 100644 --- a/src/components/Media/DataLens/DataLens.tsx +++ b/src/components/Media/DataLens/DataLens.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {LocaleContext} from '../../../context/localeContext'; +import {LocaleContext} from '../../../gravity-blocks/context/localeContext'; import {MediaComponentDataLensProps} from '../../../models'; import {block} from '../../../utils'; diff --git a/src/components/Media/Image/Image.tsx b/src/components/Media/Image/Image.tsx index 896dc2d97b..6f62cf8f9a 100644 --- a/src/components/Media/Image/Image.tsx +++ b/src/components/Media/Image/Image.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import {Interpolation, animated, config, useSpring} from '@react-spring/web'; import debounce from 'lodash/debounce'; -import {SliderBlock} from '../../../blocks'; +import {default as SliderBlock} from '../../../blocks/Slider/Slider'; import {ImageProps, MediaComponentImageProps, QAProps, SliderType} from '../../../models'; import {block, getQaAttrubutes} from '../../../utils'; import BackgroundImage from '../../BackgroundImage/BackgroundImage'; diff --git a/src/components/Media/Media.tsx b/src/components/Media/Media.tsx index b1fb71a9f7..7fb29a0c4f 100644 --- a/src/components/Media/Media.tsx +++ b/src/components/Media/Media.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {InnerContext} from '../../context/innerContext'; +import {useMicrodata} from '../../gravity-blocks/context/microdataContext'; import {MediaProps, QAProps} from '../../models'; import {block, getQaAttrubutes} from '../../utils'; import {sanitizeMicrodata} from '../../utils/microdata'; @@ -59,7 +59,7 @@ export const Media = (props: MediaAllProps) => { } = props; const [hasVideoFallback, setHasVideoFallback] = React.useState(false); - const {microdata} = React.useContext(InnerContext); + const microdata = useMicrodata(); const qaAttributes = getQaAttrubutes(qa, 'video'); diff --git a/src/components/MediaBase/MediaBase.tsx b/src/components/MediaBase/MediaBase.tsx index 3d94b538da..ee36210430 100644 --- a/src/components/MediaBase/MediaBase.tsx +++ b/src/components/MediaBase/MediaBase.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import AnimateBlock from '../../components/AnimateBlock/AnimateBlock'; -import {Col, Grid, GridColumnSize, Row} from '../../grid'; +import {Col, Grid, GridColumnSize, Row} from '../../gravity-blocks/grid'; import {MediaBaseBlockProps} from '../../models'; import {block} from '../../utils'; import Title from '../Title/Title'; diff --git a/src/components/ReactPlayer/ReactPlayer.tsx b/src/components/ReactPlayer/ReactPlayer.tsx index 0b41506e81..8713521e3f 100644 --- a/src/components/ReactPlayer/ReactPlayer.tsx +++ b/src/components/ReactPlayer/ReactPlayer.tsx @@ -6,9 +6,9 @@ import debounce from 'lodash/debounce'; import _ReactPlayer from 'react-player'; import type {ReactPlayerProps} from 'react-player'; -import {MobileContext} from '../../context/mobileContext'; -import {VideoContext} from '../../context/videoContext'; -import {useAnalytics, useImageSize, useMount} from '../../hooks'; +import {MobileContext} from '../../gravity-blocks/context/mobileContext'; +import {VideoContext} from '../../gravity-blocks/context/videoContext'; +import {useAnalytics, useImageSize, useMount} from '../../gravity-blocks/hooks'; import { AnalyticsEvent, ClassNameProps, diff --git a/src/components/RootCn/index.tsx b/src/components/RootCn/index.tsx index fa923b6365..e99f6a96bc 100644 --- a/src/components/RootCn/index.tsx +++ b/src/components/RootCn/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {useTheme} from '../../context/theme'; +import {useTheme} from '../../gravity-blocks/context/theme'; import {ClassNameProps} from '../../models'; import {rootCn} from '../../utils'; diff --git a/src/components/RouterLink/RouterLink.tsx b/src/components/RouterLink/RouterLink.tsx index ba80ca4aec..08bb1ea5f1 100644 --- a/src/components/RouterLink/RouterLink.tsx +++ b/src/components/RouterLink/RouterLink.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {LocationContext} from '../../context/locationContext'; +import {LocationContext} from '../../gravity-blocks/context/locationContext'; export interface RouterLinkProps { href: string; diff --git a/src/components/Title/Title.tsx b/src/components/Title/Title.tsx index fef1f019e1..60ef6426b5 100644 --- a/src/components/Title/Title.tsx +++ b/src/components/Title/Title.tsx @@ -1,4 +1,4 @@ -import {Col, GridColumnSizesType, GridJustifyContent} from '../../grid'; +import {Col, GridColumnSizesType, GridJustifyContent} from '../../gravity-blocks/grid'; import {ClassNameProps, QAProps, TitleItemProps, TitleProps as TitleParams} from '../../models'; import {block, getQaAttrubutes} from '../../utils'; import YFMWrapper from '../YFMWrapper/YFMWrapper'; diff --git a/src/components/Title/TitleItem.tsx b/src/components/Title/TitleItem.tsx index 717ab76e73..e7df12f56f 100644 --- a/src/components/Title/TitleItem.tsx +++ b/src/components/Title/TitleItem.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import {ToggleArrow, YFMWrapper} from '../'; -import {LocationContext} from '../../context/locationContext'; -import {MobileContext} from '../../context/mobileContext'; -import {useAnalytics} from '../../hooks'; +import {LocationContext} from '../../gravity-blocks/context/locationContext'; +import {MobileContext} from '../../gravity-blocks/context/mobileContext'; +import {useAnalytics} from '../../gravity-blocks/hooks'; import {AnalyticsEventsBase, QAProps, TextSize, TitleItemProps} from '../../models'; import {block, getHeaderTag, getLinkProps} from '../../utils'; import Anchor from '../Anchor/Anchor'; diff --git a/src/components/ToggleArrow/ToggleArrow.tsx b/src/components/ToggleArrow/ToggleArrow.tsx index e501c5953e..285b573cd9 100644 --- a/src/components/ToggleArrow/ToggleArrow.tsx +++ b/src/components/ToggleArrow/ToggleArrow.tsx @@ -1,6 +1,6 @@ import {Icon} from '@gravity-ui/uikit'; -import {Chevron, NavigationChevron} from '../../icons'; +import {Chevron, NavigationChevron} from '../../gravity-blocks/icons'; import {block} from '../../utils'; import './ToggleArrow.scss'; diff --git a/src/components/VideoBlock/VideoBlock.tsx b/src/components/VideoBlock/VideoBlock.tsx index 1da63b40a8..e2dc823d06 100644 --- a/src/components/VideoBlock/VideoBlock.tsx +++ b/src/components/VideoBlock/VideoBlock.tsx @@ -5,7 +5,7 @@ import {Icon, useActionHandlers, useUniqId} from '@gravity-ui/uikit'; import debounce from 'lodash/debounce'; import {v4 as uuidv4} from 'uuid'; -import {useAnalytics} from '../../hooks/useAnalytics'; +import {useAnalytics} from '../../gravity-blocks/hooks/useAnalytics'; import {AnalyticsEventsBase, DefaultEventNames} from '../../models/common'; import {block, getPageSearchParams} from '../../utils'; import Image from '../Image/Image'; diff --git a/src/components/YandexForm/YandexForm.tsx b/src/components/YandexForm/YandexForm.tsx index 213de82827..14c0102d2a 100644 --- a/src/components/YandexForm/YandexForm.tsx +++ b/src/components/YandexForm/YandexForm.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import {LocaleContext} from '../../context/localeContext'; -import {MobileContext} from '../../context/mobileContext'; -import {ProjectSettingsContext} from '../../context/projectSettingsContext'; -import {useAnalytics} from '../../hooks'; +import {LocaleContext} from '../../gravity-blocks/context/localeContext'; +import {MobileContext} from '../../gravity-blocks/context/mobileContext'; +import {ProjectSettingsContext} from '../../gravity-blocks/context/projectSettingsContext'; +import {useAnalytics} from '../../gravity-blocks/hooks'; import {YandexFormProps} from '../../models'; import {DefaultEventNames} from '../../models/common'; import {block} from '../../utils'; diff --git a/src/components/YandexForm/schema.ts b/src/components/YandexForm/schema.ts index 33c4194803..c93c067b70 100644 --- a/src/components/YandexForm/schema.ts +++ b/src/components/YandexForm/schema.ts @@ -1,4 +1,4 @@ -import {BaseProps} from '../../schema/validators/common'; +import {BaseProps} from '../../gravity-blocks/schema/validators/common'; export const YandexFormProps = { type: 'object', diff --git a/src/components/editor/ChildrenItemWrap/ChildrenItemWrap.scss b/src/components/editor/ChildrenItemWrap/ChildrenItemWrap.scss new file mode 100644 index 0000000000..f307cf4231 --- /dev/null +++ b/src/components/editor/ChildrenItemWrap/ChildrenItemWrap.scss @@ -0,0 +1,8 @@ +@import '../../../../styles/mixins.scss'; +@import '../../../../styles/variables.scss'; + +$block: '.#{$ns}item-wrap'; + +#{$block} { + height: 100%; +} diff --git a/src/components/editor/ChildrenItemWrap/ChildrenItemWrap.tsx b/src/components/editor/ChildrenItemWrap/ChildrenItemWrap.tsx new file mode 100644 index 0000000000..1bf95a3222 --- /dev/null +++ b/src/components/editor/ChildrenItemWrap/ChildrenItemWrap.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; + +import {usePCEditorChildrenItemWrap} from '../../../hooks/usePCEditorChildrenItemWrap'; +import {ClassNameProps} from '../../../models'; +import {block} from '../../../utils'; + +import './ChildrenItemWrap.scss'; + +const b = block('item-wrap'); + +export interface ChildrenItemWrapProps extends React.PropsWithChildren<ClassNameProps> { + index: number; +} + +const ChildrenItemWrap = ({index, children, className}: ChildrenItemWrapProps) => { + const {blockRef} = usePCEditorChildrenItemWrap(index); + return ( + <div ref={blockRef} className={b(null, className)}> + {children} + </div> + ); +}; + +export default ChildrenItemWrap; diff --git a/src/components/editor/ChildrensWrap/ChildrensWrap.scss b/src/components/editor/ChildrensWrap/ChildrensWrap.scss new file mode 100644 index 0000000000..3338e32cc5 --- /dev/null +++ b/src/components/editor/ChildrensWrap/ChildrensWrap.scss @@ -0,0 +1,11 @@ +@import '../../../../styles/mixins.scss'; +@import '../../../../styles/variables.scss'; + +$block: '.#{$ns}childrens-wrap'; + +#{$block} { + // TODO: Custom drop zone styles + &_drop-zone { + padding: 20px; + } +} diff --git a/src/components/editor/ChildrensWrap/ChildrensWrap.tsx b/src/components/editor/ChildrensWrap/ChildrensWrap.tsx new file mode 100644 index 0000000000..193a8e743b --- /dev/null +++ b/src/components/editor/ChildrensWrap/ChildrensWrap.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; + +import _ from 'lodash'; + +import {BlockIdContext} from '../../../context/blockIdContext'; +import {generateChildrenPathFromArray} from '../../../editor-v2'; +import {usePCEditorBlockRegister} from '../../../hooks/usePCEditorBlockRegister'; +import {usePCEditorStore} from '../../../hooks/usePCEditorStore'; +import {block} from '../../../utils'; + +import './ChildrensWrap.scss'; + +const b = block('childrens-wrap'); + +export interface ChildrensWrapProps extends React.PropsWithChildren {} + +const ChildrensWrap = ({children}: ChildrensWrapProps) => { + const {manipulateOverlayMode, content} = usePCEditorStore(); + + const parentBlockId = React.useContext(BlockIdContext); + + const newBlockIndex = React.useMemo(() => { + const contentConfig = _.get(content.blocks, generateChildrenPathFromArray(parentBlockId)); + return contentConfig?.children?.length ?? 0; + }, [content.blocks, parentBlockId]); + + const path = React.useMemo( + () => [...parentBlockId, newBlockIndex], + [parentBlockId, newBlockIndex], + ); + + const blockRef = usePCEditorBlockRegister(path, true); + + if (manipulateOverlayMode === 'insert' && newBlockIndex === 0) { + return <div ref={blockRef} className={b({'drop-zone': true})}></div>; + } + + if (manipulateOverlayMode === 'insert') { + return ( + <div ref={blockRef} className={b()}> + {children} + </div> + ); + } + + return children; +}; + +export default ChildrensWrap; diff --git a/src/components/editor/EmptyBlocksWrapper/EmptyBlocksWrapper.scss b/src/components/editor/EmptyBlocksWrapper/EmptyBlocksWrapper.scss new file mode 100644 index 0000000000..d3bc3498b3 --- /dev/null +++ b/src/components/editor/EmptyBlocksWrapper/EmptyBlocksWrapper.scss @@ -0,0 +1,59 @@ +@import '../../../../styles/mixins.scss'; +@import '../../../../styles/variables.scss'; + +$block: '.#{$ns}empty-blocks-wrapper'; + +#{$block} { + display: flex; + align-items: center; + justify-content: center; + min-height: 400px; + padding: 40px; + margin: 20px; + border: 3px dashed var(--g-color-base-brand); + border-radius: 16px; + background-color: var(--g-color-base-generic); + user-select: none; + transition: all 0.2s ease; + + &:hover { + background-color: var(--g-color-base-generic-hover); + border-color: var(--g-color-text-link); + } + + &__content { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + text-align: center; + } + + &__icon { + width: 64px; + height: 64px; + display: flex; + align-items: center; + justify-content: center; + font-size: 32px; + font-weight: 300; + color: var(--g-color-base-brand); + background-color: var(--g-color-base-background); + border-radius: 50%; + border: 2px dashed var(--g-color-base-brand); + line-height: 1; + } + + &__text { + font-size: var(--g-text-header-2-font-size); + line-height: var(--g-text-header-2-line-height); + font-weight: 500; + color: var(--g-color-text-primary); + } + + &__hint { + font-size: var(--g-text-body-1-font-size); + line-height: var(--g-text-body-1-line-height); + color: var(--g-color-text-secondary); + } +} diff --git a/src/components/editor/EmptyBlocksWrapper/EmptyBlocksWrapper.tsx b/src/components/editor/EmptyBlocksWrapper/EmptyBlocksWrapper.tsx new file mode 100644 index 0000000000..da285517e0 --- /dev/null +++ b/src/components/editor/EmptyBlocksWrapper/EmptyBlocksWrapper.tsx @@ -0,0 +1,36 @@ +import {usePCEditorBlockRegister} from '../../../hooks/usePCEditorBlockRegister'; +import {usePCEditorStore} from '../../../hooks/usePCEditorStore'; +import {block} from '../../../utils'; + +import './EmptyBlocksWrapper.scss'; + +const b = block('empty-blocks-wrapper'); + +export interface EmptyBlocksWrapperProps {} + +const EMPTY_DROP_PATH = [0]; + +const EmptyBlocksWrapper = () => { + const { + initialized, + content: {blocks}, + } = usePCEditorStore(); + + const blockRef = usePCEditorBlockRegister(EMPTY_DROP_PATH); + + if (!initialized || blocks.length > 0) { + return null; + } + + return ( + <div ref={blockRef} className={b()}> + <div className={b('content')}> + <div className={b('icon')}>+</div> + <div className={b('text')}>Перетащите блок сюда</div> + <div className={b('hint')}>Блоки находятся на панели слева</div> + </div> + </div> + ); +}; + +export default EmptyBlocksWrapper; diff --git a/src/constructor-items.ts b/src/constructor-items.ts index cda2913cec..b5a814c6b7 100644 --- a/src/constructor-items.ts +++ b/src/constructor-items.ts @@ -1,87 +1,55 @@ -import { - BannerBlock, - CardLayoutBlock, - CompaniesBlock, - ContentLayoutBlock, - ExtendedFeaturesBlock, - FilterBlock, - FoldableListBlock, - FormBlock, - HeaderBlock, - HeaderSliderBlock, - HeroBlock, - IconsBlock, - InfoBlock, - MapBlock, - MediaBlock, - PromoFeaturesBlock, - QuestionsBlock, - ShareBlock, - SliderBlock, - SliderOldBlock, - TableBlock, - TabsBlock, -} from './blocks'; -import {BlockType, NavigationItemType, SubBlockType} from './models'; +import * as React from 'react'; + +import BannerBlockConfig from './blocks/Banner'; +import CardLayoutBlockConfig from './blocks/CardLayout'; +import CompaniesBlockConfig from './blocks/Companies'; +import ContentLayoutBlockConfig from './blocks/ContentLayout'; +import ExtendedFeaturesBlockConfig from './blocks/ExtendedFeatures'; +// import FilterBlockConfig from './blocks/FilterBlock'; +import FormBlockConfig from './blocks/Form'; +import HeaderBlockConfig from './blocks/Header'; +import HeaderSliderBlockConfig from './blocks/HeaderSlider'; +import IconsBlockConfig from './blocks/Icons'; +import InfoBlockConfig from './blocks/Info'; +import MapBlockConfig from './blocks/Map'; +import MediaBlockConfig from './blocks/Media'; +import PromoFeaturesBlockConfig from './blocks/PromoFeaturesBlock'; +import QuestionsBlockConfig from './blocks/Questions'; +import ShareBlockConfig from './blocks/Share'; +import SliderBlockConfig from './blocks/Slider'; +import TableBlockConfig from './blocks/Table'; +import TabsBlockConfig from './blocks/Tabs'; +import {BlockConfig} from './form-generator-v2/types'; import { GithubButton, NavigationButton, NavigationDropdown, NavigationLink, -} from './navigation/components/NavigationItem'; -import SocialIcon from './navigation/components/SocialIcon/SocialIcon'; -import { - BackgroundCard, - BannerCard, - BasicCard, - Content, - Divider, - ImageCard, - LayoutItem, - MediaCard, - PriceCard, - PriceDetailed, - Quote, -} from './sub-blocks'; +} from './gravity-blocks/navigation'; +import SocialIcon from './gravity-blocks/navigation/components/SocialIcon/SocialIcon'; +import {BlockType, NavigationItemType, SubBlockType} from './models'; +import BackgroundCardConfig from './sub-blocks/BackgroundCard'; +import BannerCardConfig from './sub-blocks/BannerCard'; +import BasicCardConfig from './sub-blocks/BasicCard'; +import ContentConfig from './sub-blocks/Content'; +import DividerConfig from './sub-blocks/Divider'; +import ImageCardConfig from './sub-blocks/ImageCard'; +import LayoutItemConfig from './sub-blocks/LayoutItem'; +import MediaCardConfig from './sub-blocks/MediaCard'; +import PriceCardConfig from './sub-blocks/PriceCard'; +import QuoteConfig from './sub-blocks/Quote'; -export const blockMap = { - [BlockType.SliderOldBlock]: SliderOldBlock, - [BlockType.ExtendedFeaturesBlock]: ExtendedFeaturesBlock, - [BlockType.PromoFeaturesBlock]: PromoFeaturesBlock, - [BlockType.QuestionsBlock]: QuestionsBlock, - [BlockType.FoldableListBlock]: FoldableListBlock, - [BlockType.BannerBlock]: BannerBlock, - [BlockType.CompaniesBlock]: CompaniesBlock, - [BlockType.MediaBlock]: MediaBlock, - [BlockType.InfoBlock]: InfoBlock, - [BlockType.TableBlock]: TableBlock, - [BlockType.TabsBlock]: TabsBlock, - [BlockType.HeaderBlock]: HeaderBlock, - [BlockType.HeroBlock]: HeroBlock, - [BlockType.IconsBlock]: IconsBlock, - [BlockType.HeaderSliderBlock]: HeaderSliderBlock, - [BlockType.CardLayoutBlock]: CardLayoutBlock, - [BlockType.ContentLayoutBlock]: ContentLayoutBlock, - [BlockType.ShareBlock]: ShareBlock, - [BlockType.MapBlock]: MapBlock, - [BlockType.FilterBlock]: FilterBlock, - [BlockType.FormBlock]: FormBlock, - [BlockType.SliderBlock]: SliderBlock, -}; +/** + * TODO: remove it + * @deprecated use blockDataMap + **/ +export const blockMap = {}; -export const subBlockMap = { - [SubBlockType.Divider]: Divider, - [SubBlockType.PriceDetailed]: PriceDetailed, - [SubBlockType.MediaCard]: MediaCard, - [SubBlockType.BannerCard]: BannerCard, - [SubBlockType.LayoutItem]: LayoutItem, - [SubBlockType.BackgroundCard]: BackgroundCard, - [SubBlockType.BasicCard]: BasicCard, - [SubBlockType.Content]: Content, - [SubBlockType.Quote]: Quote, - [SubBlockType.PriceCard]: PriceCard, - [SubBlockType.ImageCard]: ImageCard, -}; +/** + * TODO: remove it + * @deprecated use blockDataMap + **/ +export const subBlockMap = {}; export const navItemMap = { [NavigationItemType.Button]: NavigationButton, @@ -90,3 +58,45 @@ export const navItemMap = { [NavigationItemType.Link]: NavigationLink, [NavigationItemType.GithubButton]: GithubButton, }; + +export interface BlockData { + type: string; + // TODO: remove any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + component: React.ComponentType<any>; + schema: BlockConfig; +} + +export const blockDataMap: Record<string, BlockData> = { + [BlockType.ExtendedFeaturesBlock]: ExtendedFeaturesBlockConfig, + [BlockType.PromoFeaturesBlock]: PromoFeaturesBlockConfig, + [BlockType.QuestionsBlock]: QuestionsBlockConfig, + [BlockType.BannerBlock]: BannerBlockConfig, + [BlockType.CompaniesBlock]: CompaniesBlockConfig, + [BlockType.MediaBlock]: MediaBlockConfig, + [BlockType.InfoBlock]: InfoBlockConfig, + [BlockType.TableBlock]: TableBlockConfig, + [BlockType.TabsBlock]: TabsBlockConfig, + [BlockType.HeaderBlock]: HeaderBlockConfig, + [BlockType.IconsBlock]: IconsBlockConfig, + [BlockType.HeaderSliderBlock]: HeaderSliderBlockConfig, + [BlockType.CardLayoutBlock]: CardLayoutBlockConfig, + [BlockType.ContentLayoutBlock]: ContentLayoutBlockConfig, + [BlockType.ShareBlock]: ShareBlockConfig, + [BlockType.MapBlock]: MapBlockConfig, + // TODO: fix items prop for editor compatibility + // [BlockType.FilterBlock]: FilterBlockConfig, + [BlockType.FormBlock]: FormBlockConfig, + [BlockType.SliderBlock]: SliderBlockConfig, + + [SubBlockType.Divider]: DividerConfig, + [SubBlockType.MediaCard]: MediaCardConfig, + [SubBlockType.BannerCard]: BannerCardConfig, + [SubBlockType.LayoutItem]: LayoutItemConfig, + [SubBlockType.BackgroundCard]: BackgroundCardConfig, + [SubBlockType.BasicCard]: BasicCardConfig, + [SubBlockType.Content]: ContentConfig, + [SubBlockType.Quote]: QuoteConfig, + [SubBlockType.PriceCard]: PriceCardConfig, + [SubBlockType.ImageCard]: ImageCardConfig, +}; diff --git a/src/containers/PageConstructor/PageConstructor.tsx b/src/containers/PageConstructor/PageConstructor.tsx index ccb8a8a240..bcbbdc5e46 100644 --- a/src/containers/PageConstructor/PageConstructor.tsx +++ b/src/containers/PageConstructor/PageConstructor.tsx @@ -1,44 +1,48 @@ import * as React from 'react'; -import '@diplodoc/transform/dist/js/yfm.js'; - -import BackgroundMedia from '../../components/BackgroundMedia/BackgroundMedia'; -import BrandFooter from '../../components/BrandFooter/BrandFooter'; +import type {PageConstructorWrapper} from '../../common/types'; import RootCn from '../../components/RootCn'; -import {blockMap, navItemMap, subBlockMap} from '../../constructor-items'; -import {AnimateContext} from '../../context/animateContext'; +import {BlockData, blockMap, navItemMap, subBlockMap} from '../../constructor-items'; +import {BlockRegistryContext, useBlockRegistryProvider} from '../../context/blockRegistryContext'; +import {BlocksContext} from '../../context/blocksContext'; import {InnerContext} from '../../context/innerContext'; -import {ProjectSettingsContext} from '../../context/projectSettingsContext'; -import {useTheme} from '../../context/theme'; -import {Grid} from '../../grid'; +import {Fields} from '../../form-generator-v2/types'; +import {usePCEditorInitializeEvents} from '../../hooks/usePCEditorInitializeEvents'; +import {usePCEditorStore} from '../../hooks/usePCEditorStore'; import { - BlockType, - BlockTypes, + BlockWrapperDataProps, CustomConfig, CustomItems, - HeaderBlockTypes, - NavigationData, - NavigationItemTypes, PageContent, ShouldRenderBlock, - SubBlockTypes, } from '../../models'; -import Layout from '../../navigation/containers/Layout/Layout'; -import { - block as cnBlock, - getCustomItems, - getCustomTypes, - getHeaderBlock, - getOrderedBlocks, - getThemedValue, -} from '../../utils'; - -import {ConstructorBlocks} from './components/ConstructorBlocks'; -import {ConstructorHeader} from './components/ConstructorItem'; +import {block as cnBlock, getCustomItems} from '../../utils'; + +import {ConstructorBlocks} from './components'; import {ConstructorRow} from './components/ConstructorRow'; import './PageConstructor.scss'; +export interface PageConstructorExtension< + GlobalConfig extends Object = {}, + WrapperProps extends Object = {}, + BlockWrapperProps extends Object = {}, +> { + name: string; + id: string; + settings: { + ContentWrapper?: PageConstructorWrapper<WrapperProps>; + contentWrapperProps?: WrapperProps; + globalInputs?: Fields; + globalDefaults?: GlobalConfig; + blockWrapper?: React.ComponentType< + BlockWrapperDataProps<BlockWrapperProps> & React.PropsWithChildren + >; + blockWrapperProps?: BlockWrapperProps; + blockInputs?: Fields; + }; +} + const b = cnBlock('page-constructor'); export type ItemMap = typeof blockMap & typeof subBlockMap & CustomItems; @@ -49,95 +53,126 @@ export interface PageConstructorProps { content?: PageContent; shouldRenderBlock?: ShouldRenderBlock; custom?: CustomConfig; - renderMenu?: () => React.ReactNode; - navigation?: NavigationData; - isBranded?: boolean; - microdata?: { - contentUpdatedDate?: string; - }; + blocks?: Array<BlockData>; + extensions?: Array<PageConstructorExtension>; } -export const Constructor = (props: PageConstructorProps) => { +export const PageConstructor = (props: PageConstructorProps) => { const { - content: {blocks = [], background} = {}, - renderMenu, + content: initialContent = {blocks: []}, shouldRenderBlock, - navigation, custom, - isBranded, - microdata, + blocks: availableLocalBlocks = [], + extensions: extensionsProp, } = props; - const {context} = React.useMemo( + const extensions = React.useMemo(() => extensionsProp ?? [], [extensionsProp]); + + const {blocks: availableGlobalBlocks} = React.useContext(BlocksContext); + + const availableBlocks = React.useMemo( + () => [...availableGlobalBlocks, ...availableLocalBlocks], + [availableGlobalBlocks, availableLocalBlocks], + ); + + const globalDefaults = extensions.reduce( + (acc, extension) => ({ + ...acc, + ...(extension.settings.globalDefaults || {}), + }), + {}, + ); + + const [content, setContent] = React.useState<PageContent>({ + ...globalDefaults, + ...initialContent, + }); + + const store = usePCEditorStore(); + const {initialized} = store; + + const blockRegistry = useBlockRegistryProvider(); + + const blockWrappers = React.useMemo( + () => + extensions.flatMap((ext) => + ext.settings.blockWrapper + ? [ + { + wrapper: ext.settings.blockWrapper, + props: ext.settings.blockWrapperProps ?? {}, + }, + ] + : [], + ), + [extensions], + ); + + const blockInputs = React.useMemo( + () => + extensions.reduce<Fields>( + (acc, ext) => [...acc, ...((ext.settings.blockInputs || []) as Fields)], + [], + ), + [extensions], + ); + + usePCEditorInitializeEvents({ + initialContent: content, + setContent, + blocks: availableBlocks, + global: extensions.reduce<Fields>( + (acc, extension) => [...acc, ...((extension.settings.globalInputs || []) as Fields)], + [], + ), + blockInputs, + registry: blockRegistry, + }); + + const context = React.useMemo( () => ({ - context: { - blockTypes: [...BlockTypes, ...getCustomTypes(['blocks', 'headers'], custom)], - subBlockTypes: [...SubBlockTypes, ...getCustomTypes(['subBlocks'], custom)], - headerBlockTypes: [...HeaderBlockTypes, ...getCustomTypes(['headers'], custom)], - navigationBlockTypes: [ - ...NavigationItemTypes, - ...getCustomTypes(['navigation'], custom), - ], - itemMap: { - ...blockMap, - ...subBlockMap, - ...getCustomItems(['blocks', 'headers', 'subBlocks'], custom), - }, - navItemMap: { - ...navItemMap, - ...getCustomItems(['navigation'], custom), - }, - loadables: custom?.loadable, - shouldRenderBlock, - customization: { - decorators: custom?.decorators, - }, - microdata, + blocks: availableBlocks, + navItemMap: { + ...navItemMap, + ...getCustomItems(['navigation'], custom), }, + loadables: custom?.loadable, + shouldRenderBlock, + blockWrappers, + content, + setContent, }), - [custom, shouldRenderBlock, microdata], + [custom, shouldRenderBlock, availableBlocks, content, setContent, blockWrappers], ); - const theme = useTheme(); + const restBlocks = content.blocks; - const header = getHeaderBlock(blocks, context.headerBlockTypes); - const restBlocks = getOrderedBlocks(blocks, context.headerBlockTypes); - const themedBackground = getThemedValue(background, theme); - - return ( - <InnerContext.Provider value={context}> - <RootCn className={b()}> - <div className={b('wrapper')}> - {themedBackground && ( - <BackgroundMedia {...themedBackground} className={b('background')} /> - )} - <Layout navigation={navigation}> - {renderMenu && renderMenu()} - {header && ( - <ConstructorHeader data={header} blockKey={BlockType.HeaderBlock} /> - )} - <Grid> - {restBlocks && ( - <ConstructorRow> - <ConstructorBlocks items={restBlocks} /> - </ConstructorRow> - )} - </Grid> - </Layout> - {isBranded && <BrandFooter />} - </div> - </RootCn> - </InnerContext.Provider> + const blocksContent = restBlocks && ( + <ConstructorRow> + <ConstructorBlocks items={restBlocks} /> + </ConstructorRow> ); -}; -export const PageConstructor = (props: PageConstructorProps) => { - const {isAnimationEnabled = true} = React.useContext(ProjectSettingsContext); - const {content: {animated = isAnimationEnabled} = {}, ...rest} = props; + // Apply extensions (wrappers) from outermost to innermost + const wrappedContent = extensions.reduceRight<React.ReactNode>( + (children, extension) => + extension.settings.ContentWrapper ? ( + <extension.settings.ContentWrapper + {...(extension.settings.contentWrapperProps || {})} + > + {children} + </extension.settings.ContentWrapper> + ) : ( + children + ), + blocksContent || null, + ); return ( - <AnimateContext.Provider value={{animated}}> - <Constructor content={props.content} {...rest} /> - </AnimateContext.Provider> + <BlockRegistryContext.Provider value={blockRegistry}> + <InnerContext.Provider value={context}> + <RootCn className={b('', {['with-editor']: initialized})}>{wrappedContent}</RootCn> + </InnerContext.Provider> + </BlockRegistryContext.Provider> ); }; diff --git a/src/containers/PageConstructor/Provider.tsx b/src/containers/PageConstructor/Provider.tsx index 7e3203039a..0336226d00 100644 --- a/src/containers/PageConstructor/Provider.tsx +++ b/src/containers/PageConstructor/Provider.tsx @@ -1,69 +1,22 @@ import * as React from 'react'; -import {DEFAULT_THEME} from '../../components/constants'; -import {AnalyticsContext, AnalyticsContextProps} from '../../context/analyticsContext'; -import { - DEFAULT_FORMS_CONTEXT_VALUE, - FormsContext, - FormsContextProps, -} from '../../context/formsContext/FormsContext'; -import {ImageContext, ImageContextProps} from '../../context/imageContext'; -import {LocaleContext, LocaleContextProps} from '../../context/localeContext'; -import {LocationContext, LocationContextProps} from '../../context/locationContext'; -import {MapsContext, MapsContextType, initialMapValue} from '../../context/mapsContext/mapsContext'; -import {MobileContext} from '../../context/mobileContext'; -import { - ProjectSettingsContext, - ProjectSettingsContextProps, -} from '../../context/projectSettingsContext'; -import {SSRContext, SSRContextProps} from '../../context/ssrContext'; -import {ThemeContext} from '../../context/theme'; -import {WindowWidthProvider} from '../../context/windowWidthContext'; -import {Theme} from '../../models'; +import {BlockData} from '../../constructor-items'; +import {BlocksContext} from '../../context/blocksContext'; +import {PCEditorStoreProvider} from '../../context/editorStoreContext'; export interface PageConstructorProviderProps { - isMobile?: boolean; - locale?: LocaleContextProps; - location?: LocationContextProps; - ssrConfig?: SSRContextProps; - theme?: Theme; - mapsContext?: MapsContextType; - projectSettings?: ProjectSettingsContextProps; - analytics?: AnalyticsContextProps; - forms?: FormsContextProps; - image?: ImageContextProps; + blocks?: Array<BlockData>; } export const PageConstructorProvider = ( props: React.PropsWithChildren<PageConstructorProviderProps>, ) => { - const { - isMobile, - mapsContext = initialMapValue, - locale = {}, - location = {}, - analytics = {}, - ssrConfig = {}, - projectSettings = {}, - theme = DEFAULT_THEME, - children, - image = {}, - forms = DEFAULT_FORMS_CONTEXT_VALUE, - } = props; + const {children, blocks = []} = props; /* eslint-disable react/jsx-key */ const context = [ - <ThemeContext.Provider value={{theme}} />, - <ProjectSettingsContext.Provider value={projectSettings} />, - <LocaleContext.Provider value={locale} />, - <ImageContext.Provider value={image} />, - <LocationContext.Provider value={location} />, - <MobileContext.Provider value={Boolean(isMobile)} />, - <MapsContext.Provider value={mapsContext} />, - <AnalyticsContext.Provider value={analytics} />, - <FormsContext.Provider value={forms} />, - <SSRContext.Provider value={{isServer: ssrConfig?.isServer}} />, - <WindowWidthProvider />, + <BlocksContext.Provider value={{blocks}} />, + <PCEditorStoreProvider />, ].reduceRight((prev, provider) => React.cloneElement(provider, {}, prev), children); /* eslint-enable react/jsx-key */ diff --git a/src/containers/PageConstructor/__stories__/components/CustomBlocksTemplate.tsx b/src/containers/PageConstructor/__stories__/components/CustomBlocksTemplate.tsx index acda170277..3bfc50a2eb 100644 --- a/src/containers/PageConstructor/__stories__/components/CustomBlocksTemplate.tsx +++ b/src/containers/PageConstructor/__stories__/components/CustomBlocksTemplate.tsx @@ -1,11 +1,15 @@ import {StoryFn} from '@storybook/react'; import {CustomConfig} from '../../../../models'; -import {PageConstructor, PageConstructorProps} from '../../PageConstructor'; +import { + PageConstructor, + PageConstructorExtension, + PageConstructorProps, +} from '../../PageConstructor'; import {CustomBlock} from './CustomBlock'; import {CustomCard} from './CustomCard'; -import {customDecorator} from './CustomDecorator'; +import {customDecoratorExtension} from './CustomDecorator'; import {CustomHeader} from './CustomHeader'; import {CustomLoadableCard, loadCustomCardData} from './CustomLoadableCard'; import {CustomNavigationItem} from './CustomNavigationItem'; @@ -23,7 +27,6 @@ const customConfig: CustomConfig = { navigation: { ['custom-navigation-item']: CustomNavigationItem, }, - decorators: {block: [customDecorator]}, loadable: { ['custom-loadable-card']: { fetch: loadCustomCardData, @@ -32,6 +35,8 @@ const customConfig: CustomConfig = { }, }; +const customExtensions: PageConstructorExtension[] = [customDecoratorExtension()]; + export const CustomBlocksTemplate: StoryFn<PageConstructorProps> = (args) => ( - <PageConstructor {...args} custom={customConfig} /> + <PageConstructor {...args} custom={customConfig} extensions={customExtensions} /> ); diff --git a/src/containers/PageConstructor/__stories__/components/CustomDecorator/CustomDecorator.tsx b/src/containers/PageConstructor/__stories__/components/CustomDecorator/CustomDecorator.tsx index f5df3693c2..6fe3171dcd 100644 --- a/src/containers/PageConstructor/__stories__/components/CustomDecorator/CustomDecorator.tsx +++ b/src/containers/PageConstructor/__stories__/components/CustomDecorator/CustomDecorator.tsx @@ -4,47 +4,54 @@ import {Link} from '@gravity-ui/uikit'; import {yfmTransform} from '../../../../../../.storybook/utils'; import {BlockBase, YFMWrapper} from '../../../../../components'; -import {BlockDecorator} from '../../../../../models'; +import {BlockWrapperDataProps} from '../../../../../models'; import {cn} from '../../../../../utils'; +import {PageConstructorExtension} from '../../../PageConstructor'; import './CustomDecorator.scss'; const b = cn('custom-decorator'); const CUSTOM_DECORATOR_CODE = ` -const customDecorator = ...; - -const customConfig: CustomConfig = { - ... - decorators: { - block: [customDecorator], - }, +const CustomDecoratorWrapper = ({type, children}) => { + if (type !== 'banner-block') { + return <React.Fragment>{children}</React.Fragment>; + } + return <div className="custom-wrapper">{children}</div>; }; -... +const customExtension = () => ({ + name: 'Custom Decorator', + id: 'my-app/custom-decorator', + settings: { + blockWrapper: CustomDecoratorWrapper, + }, +}); -<PageConstructor {...props} custom={customConfig} /> +<PageConstructor extensions={[BlockBaseExtension(), customExtension()]} /> ` .trim() .replace(/</g, '<') .replace(/>/g, '>'); const CUSTOM_DECORATOR_DESCRIPTION = ` -**Custom decorators let you modify how blocks are displayed on the page. Every block on the page goes through them, but you can specify how different block types are handled.** +**Block wrapper extensions let you modify how blocks are displayed on the page. Every block goes through them, but you can specify how different block types are handled.** -To create and use a custom decorator you need to: -1. Create your own decorator function -3. Add it to your \`CustomConfig\` -3. Pass this config to \`<PageConstructor />\` +To create and use a custom block wrapper extension you need to: +1. Create a wrapper component that receives \`type\`, \`index\`, all block data props, and \`children\` +2. Create an extension factory that returns a \`PageConstructorExtension\` with \`blockWrapper\` +3. Pass it to \`<PageConstructor extensions={[...]} />\` Check out this Stories' \`content\` control to see page data. The code block links to the current example's source. `; -export const customDecorator: BlockDecorator = ({type, children}) => { +export const CustomDecoratorBlockWrapper: React.FC< + BlockWrapperDataProps & React.PropsWithChildren +> = ({type, children}) => { if (type !== 'banner-block') { - return children as React.ReactElement; + return <React.Fragment>{children}</React.Fragment>; } return ( @@ -65,3 +72,11 @@ export const customDecorator: BlockDecorator = ({type, children}) => { </BlockBase> ); }; + +export const customDecoratorExtension = (): PageConstructorExtension => ({ + name: 'Custom Decorator', + id: 'page-constructor-stories/custom-decorator', + settings: { + blockWrapper: CustomDecoratorBlockWrapper, + }, +}); diff --git a/src/containers/PageConstructor/__stories__/components/CustomDecorator/index.ts b/src/containers/PageConstructor/__stories__/components/CustomDecorator/index.ts index d34228e0b9..852436087b 100644 --- a/src/containers/PageConstructor/__stories__/components/CustomDecorator/index.ts +++ b/src/containers/PageConstructor/__stories__/components/CustomDecorator/index.ts @@ -1 +1 @@ -export {customDecorator} from './CustomDecorator'; +export {customDecoratorExtension} from './CustomDecorator'; diff --git a/src/containers/PageConstructor/__stories__/components/CustomHeader/CustomHeader.tsx b/src/containers/PageConstructor/__stories__/components/CustomHeader/CustomHeader.tsx index 90a405eec9..1125ac1760 100644 --- a/src/containers/PageConstructor/__stories__/components/CustomHeader/CustomHeader.tsx +++ b/src/containers/PageConstructor/__stories__/components/CustomHeader/CustomHeader.tsx @@ -1,7 +1,7 @@ import {Link} from '@gravity-ui/uikit'; import {yfmTransform} from '../../../../../../.storybook/utils'; -import {HeaderBlock} from '../../../../../blocks'; +import {default as HeaderBlock} from '../../../../../blocks/Header/Header'; import {YFMWrapper} from '../../../../../components'; import {HeaderBlockProps} from '../../../../../models'; import {cn} from '../../../../../utils'; diff --git a/src/containers/PageConstructor/components/ConstructorBlock/ConstructorBlock.tsx b/src/containers/PageConstructor/components/ConstructorBlock/ConstructorBlock.tsx index b41b5bc182..e77a4f3239 100644 --- a/src/containers/PageConstructor/components/ConstructorBlock/ConstructorBlock.tsx +++ b/src/containers/PageConstructor/components/ConstructorBlock/ConstructorBlock.tsx @@ -1,36 +1,32 @@ import * as React from 'react'; -import pick from 'lodash/pick'; - -import BlockBase from '../../../../components/BlockBase/BlockBase'; -import {BlockDecoration} from '../../../../customization/BlockDecoration'; -import {BlockDecorationProps, ConstructorBlock as ConstructorBlockType} from '../../../../models'; -import {block} from '../../../../utils'; +import {InnerContext} from '../../../../context/innerContext'; +import {usePCEditorChildrenItemWrap} from '../../../../hooks/usePCEditorChildrenItemWrap'; +import {ConstructorBlock as ConstructorBlockType, ConstructorItem} from '../../../../models'; import './ConstructorBlock.scss'; -interface ConstructorBlockProps extends Pick<BlockDecorationProps, 'index'> { +interface ConstructorBlockProps { + index?: number; data: ConstructorBlockType; } -const b = block('constructor-block'); - export const ConstructorBlock = ({ index = 0, data, children, }: React.PropsWithChildren<ConstructorBlockProps>) => { - const {type} = data; - const blockBaseProps = React.useMemo( - () => pick(data, ['anchor', 'visible', 'resetPaddings', 'indent']), - [data], - ); + const {blockRef} = usePCEditorChildrenItemWrap(index); + const {blockWrappers = []} = React.useContext(InnerContext); - return ( - <BlockDecoration type={type} index={index} {...blockBaseProps}> - <BlockBase className={b({type})} {...blockBaseProps}> - {children} - </BlockBase> - </BlockDecoration> + const wrappedContent = blockWrappers.reduce<React.ReactNode>( + (content, {wrapper: Wrapper, props}) => ( + <Wrapper type={data.type} props={props} content={data as ConstructorItem} index={index}> + {content} + </Wrapper> + ), + children, ); + + return <div ref={blockRef}>{wrappedContent}</div>; }; diff --git a/src/containers/PageConstructor/components/ConstructorBlocks/ConstructorBlocks.tsx b/src/containers/PageConstructor/components/ConstructorBlocks/ConstructorBlocks.tsx index 5ce480b4b2..037b1fa3db 100644 --- a/src/containers/PageConstructor/components/ConstructorBlocks/ConstructorBlocks.tsx +++ b/src/containers/PageConstructor/components/ConstructorBlocks/ConstructorBlocks.tsx @@ -2,10 +2,9 @@ import * as React from 'react'; import get from 'lodash/get'; +import EmptyBlocksWrapper from '../../../../components/editor/EmptyBlocksWrapper/EmptyBlocksWrapper'; import {InnerContext} from '../../../../context/innerContext'; -import {BlockDecoration} from '../../../../customization/BlockDecoration'; import { - BlockType, ConstructorBlock as ConstructorBlockType, LoadableProps, SubBlock, @@ -19,20 +18,19 @@ export interface ConstructorBlocksProps { items: ConstructorBlockType[]; } -export const ConstructorBlocks = ({items}: ConstructorBlocksProps) => { - const {blockTypes, loadables, itemMap, shouldRenderBlock} = React.useContext(InnerContext); +export const ConstructorBlocks: React.FC<ConstructorBlocksProps> = ({items}) => { + const {loadables, shouldRenderBlock, blocks} = React.useContext(InnerContext); const renderer = ( parentId = '', + withoutConstructorBlockWrapper = false, item: ConstructorBlockType, index: number, ): React.ReactElement | null => { - if (!itemMap[item.type]) { - return parentId ? null : ( - <BlockDecoration type={item.type as BlockType} index={index}> - {null} - </BlockDecoration> - ); + const blockData = blocks.find(({type}) => item.type === type); + + if (!blockData) { + return null; } let itemElement; @@ -63,26 +61,31 @@ export const ConstructorBlocks = ({items}: ConstructorBlocksProps) => { } else { let children; if ('children' in item && item.children) { - children = (item.children as SubBlock[]).map(renderer.bind(null, blockId)); + children = (item.children as SubBlock[]).map(renderer.bind(null, blockId, true)); } itemElement = ( - <ConstructorItem data={item} key={blockId} blockKey={blockId}> + <ConstructorItem data={item} key={blockId} blockKey={index}> {children} </ConstructorItem> ); } - return blockTypes.includes(item.type) ? ( + return withoutConstructorBlockWrapper ? ( + itemElement + ) : ( //TODO: replace ConstructorBlock (and delete it) with BlockBase when all // components relying on constructor inner structure like Slider or blog-constructor will be refactored <ConstructorBlock data={item} key={blockId} index={index}> {itemElement} </ConstructorBlock> - ) : ( - itemElement ); }; - return <React.Fragment>{items.map(renderer.bind(null, ''))}</React.Fragment>; + // Показываем EmptyBlocksWrapper когда нет блоков (только в режиме редактора) + if (items.length === 0) { + return <EmptyBlocksWrapper />; + } + + return <React.Fragment>{items.map(renderer.bind(null, '', false))}</React.Fragment>; }; diff --git a/src/containers/PageConstructor/components/ConstructorItem/ConstructorItem.scss b/src/containers/PageConstructor/components/ConstructorItem/ConstructorItem.scss new file mode 100644 index 0000000000..d3ae9d0874 --- /dev/null +++ b/src/containers/PageConstructor/components/ConstructorItem/ConstructorItem.scss @@ -0,0 +1,9 @@ +@import '../../../../../styles/variables'; +@import '../../../../../styles/mixins'; + +$block: '.#{$ns}constructor-item'; + +#{$block} { + height: inherit; + width: inherit; +} diff --git a/src/containers/PageConstructor/components/ConstructorItem/ConstructorItem.tsx b/src/containers/PageConstructor/components/ConstructorItem/ConstructorItem.tsx index ce6af6d3f6..07e7dd5629 100644 --- a/src/containers/PageConstructor/components/ConstructorItem/ConstructorItem.tsx +++ b/src/containers/PageConstructor/components/ConstructorItem/ConstructorItem.tsx @@ -2,13 +2,17 @@ import * as React from 'react'; import {BlockIdContext} from '../../../../context/blockIdContext'; import {InnerContext} from '../../../../context/innerContext'; -import {BlockDecoration} from '../../../../customization/BlockDecoration'; -import {getVisibleClasses} from '../../../../grid/utils'; -import {BlockType, ConstructorBlock} from '../../../../models'; +import {usePCEditorBlockRegister} from '../../../../hooks/usePCEditorBlockRegister'; +import {ConstructorBlock} from '../../../../models'; +import {block} from '../../../../utils'; + +import './ConstructorItem.scss'; + +const b = block('constructor-item'); export interface ConstructorItemProps { data: ConstructorBlock; - blockKey: string; + blockKey: number; } export const ConstructorItem = ({ @@ -16,33 +20,28 @@ export const ConstructorItem = ({ blockKey, children, }: React.PropsWithChildren<ConstructorItemProps>) => { - const {itemMap} = React.useContext(InnerContext); + const {blocks} = React.useContext(InnerContext); + const parentId = React.useContext(BlockIdContext); const {type, ...rest} = data; - const Component = itemMap[type] as React.ComponentType< - React.ComponentProps<(typeof itemMap)[typeof type]> - >; + const path = React.useMemo(() => [...parentId, blockKey], [parentId, blockKey]); + const blockRef = usePCEditorBlockRegister(path); - return ( - <BlockIdContext.Provider value={blockKey}> - <Component {...rest}>{children}</Component> - </BlockIdContext.Provider> - ); -}; + const blockData = blocks.find(({type: blockType}) => blockType === type); -export const ConstructorHeader = ({ - data, - blockKey, -}: Pick<ConstructorItemProps, 'data' | 'blockKey'>) => { - const {visible} = data; + if (!blockData) { + return null; + } - const visibilityClasses = visible ? getVisibleClasses(visible) : ''; + const Component = blockData.component as React.ComponentType< + React.ComponentProps<(typeof blockData)['component']> + >; return ( - <div className={visibilityClasses}> - <BlockDecoration type={data.type as BlockType}> - <ConstructorItem data={data} key={data.type} blockKey={blockKey} /> - </BlockDecoration> - </div> + <BlockIdContext.Provider value={path} key={blockKey}> + <div ref={blockRef} className={b()}> + <Component {...rest}>{children}</Component> + </div> + </BlockIdContext.Provider> ); }; diff --git a/src/containers/PageConstructor/components/ConstructorLoadable/ConstructorLoadable.tsx b/src/containers/PageConstructor/components/ConstructorLoadable/ConstructorLoadable.tsx index 4488dd6f2c..199555be69 100644 --- a/src/containers/PageConstructor/components/ConstructorLoadable/ConstructorLoadable.tsx +++ b/src/containers/PageConstructor/components/ConstructorLoadable/ConstructorLoadable.tsx @@ -11,16 +11,25 @@ interface ConstructorLoadableProps } export const ConstructorLoadable = (props: ConstructorLoadableProps) => { - const {itemMap} = React.useContext(InnerContext); + const {blocks} = React.useContext(InnerContext); const {block, blockKey, config, serviceId, params} = props; const {type} = block; const {fetch, component: ChildComponent} = config; - const Component = itemMap[type] as React.Component< - React.ComponentProps<(typeof itemMap)[typeof type]> + + const parentId = React.useContext(BlockIdContext); + + const blockData = blocks.find(({type: blockType}) => blockType === type); + + if (!blockData) { + return null; + } + + const Component = blockData.component as React.ComponentType< + React.ComponentProps<typeof blockData.component> >; return ( - <BlockIdContext.Provider value={blockKey} key={blockKey}> + <BlockIdContext.Provider value={[...parentId, Number(blockKey)]} key={blockKey}> <Loadable key={blockKey} block={block} diff --git a/src/containers/PageConstructor/components/ConstructorRow/ConstructorRow.scss b/src/containers/PageConstructor/components/ConstructorRow/ConstructorRow.scss index bf83afe7c3..85c3060c7c 100644 --- a/src/containers/PageConstructor/components/ConstructorRow/ConstructorRow.scss +++ b/src/containers/PageConstructor/components/ConstructorRow/ConstructorRow.scss @@ -3,13 +3,4 @@ $block: '.#{$ns}constructor-row'; #{$block} { - &:last-child { - margin-bottom: -$contentLiftIndent; - } - - @media (max-width: map-get($gridBreakpoints, 'sm')) { - &:last-child { - margin-bottom: -$contentLiftIndentMobile; - } - } } diff --git a/src/containers/PageConstructor/components/ConstructorRow/ConstructorRow.tsx b/src/containers/PageConstructor/components/ConstructorRow/ConstructorRow.tsx index d3b7077ec6..5173b89ad5 100644 --- a/src/containers/PageConstructor/components/ConstructorRow/ConstructorRow.tsx +++ b/src/containers/PageConstructor/components/ConstructorRow/ConstructorRow.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; -import {Col, Row} from '../../../../grid'; import {block} from '../../../../utils'; import './ConstructorRow.scss'; @@ -8,8 +7,4 @@ import './ConstructorRow.scss'; const b = block('constructor-row'); export const ConstructorRow = ({children}: React.PropsWithChildren<{}>) => - children ? ( - <Row className={b()}> - <Col>{children}</Col> - </Row> - ) : null; + children ? <div className={b()}>{children}</div> : null; diff --git a/src/context/blockIdContext/blockIdContext.ts b/src/context/blockIdContext/blockIdContext.ts index 289ed964df..bc82a8b723 100644 --- a/src/context/blockIdContext/blockIdContext.ts +++ b/src/context/blockIdContext/blockIdContext.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -export type BlockIdContextProp = string; +export type BlockIdContextProp = number[]; -export const BlockIdContext = React.createContext<BlockIdContextProp>(''); +export const BlockIdContext = React.createContext<BlockIdContextProp>([]); diff --git a/src/context/blockRegistryContext/blockRegistryContext.ts b/src/context/blockRegistryContext/blockRegistryContext.ts new file mode 100644 index 0000000000..8cd1a7db70 --- /dev/null +++ b/src/context/blockRegistryContext/blockRegistryContext.ts @@ -0,0 +1,14 @@ +import * as React from 'react'; + +export type BlockRegistry = { + register: (pathKey: string, path: number[], element: HTMLElement, dropZone?: boolean) => void; + unregister: (pathKey: string) => void; + subscribe: (listener: () => void) => () => void; + getEntries: () => Array<{path: number[]; element: HTMLElement; dropZone?: boolean}>; +}; + +export const BlockRegistryContext = React.createContext<BlockRegistry | null>(null); + +export function pathKey(path: number[]): string { + return path.join('.'); +} diff --git a/src/context/blockRegistryContext/index.ts b/src/context/blockRegistryContext/index.ts new file mode 100644 index 0000000000..5a0ef61572 --- /dev/null +++ b/src/context/blockRegistryContext/index.ts @@ -0,0 +1,2 @@ +export * from './blockRegistryContext'; +export * from './useBlockRegistryProvider'; diff --git a/src/context/blockRegistryContext/useBlockRegistryProvider.ts b/src/context/blockRegistryContext/useBlockRegistryProvider.ts new file mode 100644 index 0000000000..59ff1a5f0a --- /dev/null +++ b/src/context/blockRegistryContext/useBlockRegistryProvider.ts @@ -0,0 +1,39 @@ +import * as React from 'react'; + +import {BlockRegistry} from './blockRegistryContext'; + +export function useBlockRegistryProvider(): BlockRegistry { + const entriesRef = React.useRef( + new Map<string, {path: number[]; element: HTMLElement; dropZone?: boolean}>(), + ); + const listenersRef = React.useRef(new Set<() => void>()); + + return React.useMemo<BlockRegistry>(() => { + const notify = () => { + for (const listener of listenersRef.current) { + listener(); + } + }; + + return { + register(pathKey, path, element, dropZone) { + entriesRef.current.set(pathKey, {path, element, dropZone}); + notify(); + }, + unregister(pathKey) { + if (entriesRef.current.delete(pathKey)) { + notify(); + } + }, + subscribe(listener) { + listenersRef.current.add(listener); + return () => { + listenersRef.current.delete(listener); + }; + }, + getEntries() { + return Array.from(entriesRef.current.values()); + }, + }; + }, []); +} diff --git a/src/context/blocksContext/blocksContext.ts b/src/context/blocksContext/blocksContext.ts new file mode 100644 index 0000000000..e71306b7f6 --- /dev/null +++ b/src/context/blocksContext/blocksContext.ts @@ -0,0 +1,9 @@ +import * as React from 'react'; + +import {BlockData} from '../../constructor-items'; + +export type BlocksContextProps = { + blocks: Array<BlockData>; +}; + +export const BlocksContext = React.createContext<BlocksContextProps>({blocks: []}); diff --git a/src/context/blocksContext/index.ts b/src/context/blocksContext/index.ts new file mode 100644 index 0000000000..3f9764d03d --- /dev/null +++ b/src/context/blocksContext/index.ts @@ -0,0 +1 @@ +export * from './blocksContext'; diff --git a/src/context/editorStoreContext/PCEditorStoreContext.tsx b/src/context/editorStoreContext/PCEditorStoreContext.tsx new file mode 100644 index 0000000000..1b9e53899c --- /dev/null +++ b/src/context/editorStoreContext/PCEditorStoreContext.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; + +import {StoreApi} from 'zustand'; + +import {EditorState, createPCEditorStore} from '../../common/store'; + +export interface PCEditorStoreContextProps { + state: StoreApi<EditorState>; +} + +export const PCEditorStoreContext = React.createContext<PCEditorStoreContextProps>({ + state: createPCEditorStore(), +}); diff --git a/src/context/editorStoreContext/PCEditorStoreProvider.tsx b/src/context/editorStoreContext/PCEditorStoreProvider.tsx new file mode 100644 index 0000000000..988186bcc1 --- /dev/null +++ b/src/context/editorStoreContext/PCEditorStoreProvider.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; + +import {StoreApi} from 'zustand'; + +import {isValidPostMessage} from '../../common/postMessage'; +import {EditorState, createPCEditorStore} from '../../common/store'; +import {StoreSyncMessage} from '../../common/types'; +import {sendEventPostMessage} from '../../hooks/usePostMessageAPI'; + +import {PCEditorStoreContext} from './PCEditorStoreContext'; + +interface PCEditorStoreProviderProps extends React.PropsWithChildren {} + +export const PCEditorStoreProvider = ({children}: PCEditorStoreProviderProps) => { + const storeRef = React.useRef<StoreApi<EditorState>>(); + + const syncStore = React.useCallback((message: StoreSyncMessage) => { + if (storeRef.current && message.state) { + storeRef.current.setState(message.state); + } + }, []); + + React.useEffect(() => { + const onMessage = (e: MessageEvent) => { + if (!isValidPostMessage(e.data)) { + return; + } + + const message = e.data as StoreSyncMessage; + syncStore(message); + }; + + window.addEventListener('message', onMessage); + return () => window.removeEventListener('message', onMessage); + }, [syncStore]); + + // When Page Constructor runs inside the editor preview iframe, keyboard focus stays in the iframe + // after clicking the canvas — parent window never receives Cmd+Z. Forward to parent via postMessage. + React.useEffect(() => { + if (typeof window === 'undefined' || window.parent === window) { + return undefined; + } + const onKeyDown = (e: KeyboardEvent) => { + if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== 'z') { + return; + } + + const target = e.target as HTMLElement | null; + if (target?.closest('input, textarea, select, [contenteditable="true"]')) { + return; + } + + e.preventDefault(); + + if (e.shiftKey) { + sendEventPostMessage('ON_EDITOR_REDO', {}); + } else { + sendEventPostMessage('ON_EDITOR_UNDO', {}); + } + }; + window.addEventListener('keydown', onKeyDown, true); + return () => window.removeEventListener('keydown', onKeyDown, true); + }, []); + + if (!storeRef.current) { + storeRef.current = createPCEditorStore(); + } + + return ( + <PCEditorStoreContext.Provider + value={{ + state: storeRef.current, + }} + > + {children} + </PCEditorStoreContext.Provider> + ); +}; diff --git a/src/context/editorStoreContext/index.ts b/src/context/editorStoreContext/index.ts new file mode 100644 index 0000000000..2cb38452e5 --- /dev/null +++ b/src/context/editorStoreContext/index.ts @@ -0,0 +1,2 @@ +export * from './PCEditorStoreContext'; +export * from './PCEditorStoreProvider'; diff --git a/src/context/innerContext/InnerContext.ts b/src/context/innerContext/InnerContext.ts index 404fa02c55..4f56a171cd 100644 --- a/src/context/innerContext/InnerContext.ts +++ b/src/context/innerContext/InnerContext.ts @@ -1,29 +1,28 @@ import * as React from 'react'; -import {ItemMap, NavItemMap} from '../../containers/PageConstructor/PageConstructor'; -import {CustomConfig, LoadableConfig, ShouldRenderBlock} from '../../models'; +import {BlockData} from '../../constructor-items'; +import {NavItemMap} from '../../containers/PageConstructor/PageConstructor'; +import {BlockWrapperDataProps, LoadableConfig, PageContent, ShouldRenderBlock} from '../../models'; + +export interface BlockWrapperEntry { + wrapper: React.ComponentType<BlockWrapperDataProps & React.PropsWithChildren>; + props?: object; +} export interface InnerContextType { - blockTypes: string[]; - subBlockTypes: string[]; - headerBlockTypes: string[]; - navigationBlockTypes: string[]; - itemMap: ItemMap; navItemMap: NavItemMap; loadables?: LoadableConfig; shouldRenderBlock?: ShouldRenderBlock; - customization?: Pick<CustomConfig, 'decorators'>; - microdata?: { - contentUpdatedDate?: string; - }; + blockWrappers?: BlockWrapperEntry[]; + blocks: Array<BlockData>; + content: PageContent; + setContent: React.Dispatch<React.SetStateAction<PageContent>>; } export const InnerContext = React.createContext<InnerContextType>({ - blockTypes: [], - subBlockTypes: [], - headerBlockTypes: [], - navigationBlockTypes: [], - itemMap: {} as ItemMap, navItemMap: {} as NavItemMap, - microdata: {}, + blocks: [], + content: {blocks: []}, + // eslint-disable-next-line @typescript-eslint/no-empty-function + setContent: () => {}, }); diff --git a/src/customization/BlockDecoration.tsx b/src/customization/BlockDecoration.tsx deleted file mode 100644 index 53ae0349b1..0000000000 --- a/src/customization/BlockDecoration.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; - -import {InnerContext} from '../context/innerContext'; -import {BlockDecorationProps} from '../models'; - -export const BlockDecoration = ({ - children: blockChildren, - ...rest -}: React.PropsWithChildren<BlockDecorationProps>) => { - const blockDecorators = React.useContext(InnerContext).customization?.decorators?.block; - - const content = blockDecorators - ? blockDecorators.reduce( - (children, decorator) => decorator({children, ...rest}), - blockChildren, - ) - : blockChildren; - - return <React.Fragment>{content}</React.Fragment>; -}; diff --git a/src/demo/ContentAndData.stories.tsx b/src/demo/ContentAndData.stories.tsx index 12e0313b24..f00340f353 100644 --- a/src/demo/ContentAndData.stories.tsx +++ b/src/demo/ContentAndData.stories.tsx @@ -1,16 +1,17 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../.storybook/utils'; -import {PageConstructor} from '../containers/PageConstructor'; +import {PageConstructor, PageConstructorProvider} from '../containers/PageConstructor'; +import {gravityBlocksExtension} from '../gravity-blocks/extensions'; +import {CustomComponent} from '../gravity-blocks/navigation/__stories__/CustomComponent/CustomComponent'; import {CustomConfig, NavigationData, PageContent} from '../models'; -import {CustomComponent} from '../navigation/__stories__/CustomComponent/CustomComponent'; import contentLayoutData from '../blocks/ContentLayout/__stories__/data.json'; import foldableListData from '../blocks/FoldableList/__stories__/data.json'; import questionsData from '../blocks/Questions/__stories__/data.json'; import tableData from '../blocks/Table/__stories__/data.json'; import tabsData from '../blocks/Tabs/__stories__/data.json'; -import navData from '../navigation/__stories__/data.json'; +import navData from '../gravity-blocks/navigation/__stories__/data.json'; export default { title: 'Lab/Tokenization/Blocks/ContentAndData', @@ -21,50 +22,56 @@ const Template: StoryFn<{navigation: NavigationData; custom?: CustomConfig}> = ( navigation, custom = {}, }) => ( - <PageConstructor - navigation={navigation} - custom={custom} - content={ - { - blocks: [ - // content-layout-block: default text only - blockTransform(contentLayoutData.default), - // content-layout-block: centered - blockTransform(contentLayoutData.textAlignCenter), - // content-layout-block: with background color - blockTransform(contentLayoutData.withBackgroundColor), - // content-layout-block: with background image + color - blockTransform(contentLayoutData.withImageAndBackgroundColor), - // content-layout-block: dark monochrome theme - blockTransform(contentLayoutData.theme[0]), - // content-layout-block: light monochrome theme - blockTransform(contentLayoutData.theme[1]), - // content-layout-block variants with list/links/buttons - ...contentLayoutData.contentVariables.map(blockTransform), + <PageConstructorProvider> + <PageConstructor + extensions={gravityBlocksExtension({ + globalDefaults: { + navigation, + }, + })} + custom={custom} + content={ + { + blocks: [ + // content-layout-block: default text only + blockTransform(contentLayoutData.default), + // content-layout-block: centered + blockTransform(contentLayoutData.textAlignCenter), + // content-layout-block: with background color + blockTransform(contentLayoutData.withBackgroundColor), + // content-layout-block: with background image + color + blockTransform(contentLayoutData.withImageAndBackgroundColor), + // content-layout-block: dark monochrome theme + blockTransform(contentLayoutData.theme[0]), + // content-layout-block: light monochrome theme + blockTransform(contentLayoutData.theme[1]), + // content-layout-block variants with list/links/buttons + ...contentLayoutData.contentVariables.map(blockTransform), - // tabs-block: default (all tab types: image, video, youtube) - blockTransform(tabsData.default.content), + // tabs-block: default (all tab types: image, video, youtube) + blockTransform(tabsData.default.content), - // questions-block: default with links - blockTransform(questionsData.default.content), - // questions-block: with bullet list items - blockTransform(questionsData.textWithListBullet.content), + // questions-block: default with links + blockTransform(questionsData.default.content), + // questions-block: with bullet list items + blockTransform(questionsData.textWithListBullet.content), - // foldable-list-block: default - blockTransform(foldableListData.default), - // foldable-list-block: with bullet list items - blockTransform(foldableListData.textWithListBullet), - // foldable-list-block: with dash list items - blockTransform(foldableListData.textWithListDash), + // foldable-list-block: default + blockTransform(foldableListData.default), + // foldable-list-block: with bullet list items + blockTransform(foldableListData.textWithListBullet), + // foldable-list-block: with dash list items + blockTransform(foldableListData.textWithListDash), - // table-block: numeric values (0/1) - blockTransform(tableData.default.content), - // table-block: tick markers - blockTransform(tableData.tick.content), - ], - } as PageContent - } - /> + // table-block: numeric values (0/1) + blockTransform(tableData.default.content), + // table-block: tick markers + blockTransform(tableData.tick.content), + ], + } as PageContent + } + /> + </PageConstructorProvider> ); export const Default = Template.bind({}); diff --git a/src/demo/FeaturesAndCards.stories.tsx b/src/demo/FeaturesAndCards.stories.tsx index 6e31e5b87c..9fefee41c1 100644 --- a/src/demo/FeaturesAndCards.stories.tsx +++ b/src/demo/FeaturesAndCards.stories.tsx @@ -1,7 +1,8 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../.storybook/utils'; -import {PageConstructor} from '../containers/PageConstructor'; +import {PageConstructor, PageConstructorProvider} from '../containers/PageConstructor'; +import {gravityBlocksExtension} from '../gravity-blocks/extensions'; import {CustomConfig, NavigationData, PageContent} from '../models'; import cardLayoutData from '../blocks/CardLayout/__stories__/data.json'; @@ -9,7 +10,7 @@ import extendedFeaturesData from '../blocks/ExtendedFeatures/__stories__/data.js import headerSliderData from '../blocks/HeaderSlider/__stories__/data.json'; import promoFeaturesData from '../blocks/PromoFeaturesBlock/__stories__/data.json'; import sliderData from '../blocks/Slider/__stories__/data.json'; -import navData from '../navigation/__stories__/data.json'; +import navData from '../gravity-blocks/navigation/__stories__/data.json'; export default { title: 'Lab/Tokenization/Blocks/FeaturesAndCards', @@ -20,99 +21,108 @@ const Template: StoryFn<{navigation: NavigationData; custom?: CustomConfig}> = ( navigation, custom = {}, }) => ( - <PageConstructor - navigation={navigation} - custom={custom} - content={ - { - blocks: [ - // promo-features-block: default theme - blockTransform({ - ...promoFeaturesData.common, - ...promoFeaturesData.defaultTheme.content, - }), - // promo-features-block: grey theme - blockTransform({ - ...promoFeaturesData.common, - ...promoFeaturesData.greyTheme.content, - }), + <PageConstructorProvider> + <PageConstructor + extensions={gravityBlocksExtension({ + globalDefaults: { + navigation, + }, + })} + custom={custom} + content={ + { + blocks: [ + // promo-features-block: default theme + blockTransform({ + ...promoFeaturesData.common, + ...promoFeaturesData.defaultTheme.content, + }), + // promo-features-block: grey theme + blockTransform({ + ...promoFeaturesData.common, + ...promoFeaturesData.greyTheme.content, + }), - // extended-features-block: default (3 cols) - blockTransform(extendedFeaturesData.default.content), - // extended-features-block: with labels - blockTransform({ - type: 'extended-features-block', - ...extendedFeaturesData.withLabel.content, - }), - // extended-features-block: 2 per row - blockTransform({ - type: 'extended-features-block', - ...extendedFeaturesData.colSizes.two, - }), - // extended-features-block: 4 per row - blockTransform({ - type: 'extended-features-block', - ...extendedFeaturesData.colSizes.four, - }), + // extended-features-block: default (3 cols) + blockTransform(extendedFeaturesData.default.content), + // extended-features-block: with labels + blockTransform({ + type: 'extended-features-block', + ...extendedFeaturesData.withLabel.content, + }), + // extended-features-block: 2 per row + blockTransform({ + type: 'extended-features-block', + ...extendedFeaturesData.colSizes.two, + }), + // extended-features-block: 4 per row + blockTransform({ + type: 'extended-features-block', + ...extendedFeaturesData.colSizes.four, + }), - // card-layout-block: basic cards - { - ...cardLayoutData.default.content, - children: [ - blockTransform(cardLayoutData.cards.basicCard), - blockTransform(cardLayoutData.cards.basicCard), - blockTransform(cardLayoutData.cards.basicCard), - ], - }, - // card-layout-block: layout items with images - { - type: 'card-layout-block', - title: 'Card layout with layout items', - children: [ - blockTransform(cardLayoutData.cards.layoutItem), - blockTransform(cardLayoutData.cards.layoutItem), - blockTransform(cardLayoutData.cards.layoutItem), - ], - }, - // card-layout-block: background cards - { - type: 'card-layout-block', - title: 'Card layout with background cards', - children: [ - blockTransform(cardLayoutData.cards.backgroundCard), - blockTransform(cardLayoutData.cards.backgroundCard), - blockTransform(cardLayoutData.cards.backgroundCard), - ], - }, - // card-layout-block: price cards - { - type: 'card-layout-block', - title: 'Card layout with price cards', - children: [ - blockTransform(cardLayoutData.cards.priceCard), - blockTransform(cardLayoutData.cards.priceCard), - blockTransform(cardLayoutData.cards.priceCard), - ], - }, + // card-layout-block: basic cards + { + ...cardLayoutData.default.content, + children: [ + blockTransform(cardLayoutData.cards.basicCard), + blockTransform(cardLayoutData.cards.basicCard), + blockTransform(cardLayoutData.cards.basicCard), + ], + }, + // card-layout-block: layout items with images + { + type: 'card-layout-block', + title: 'Card layout with layout items', + children: [ + blockTransform(cardLayoutData.cards.layoutItem), + blockTransform(cardLayoutData.cards.layoutItem), + blockTransform(cardLayoutData.cards.layoutItem), + ], + }, + // card-layout-block: background cards + { + type: 'card-layout-block', + title: 'Card layout with background cards', + children: [ + blockTransform(cardLayoutData.cards.backgroundCard), + blockTransform(cardLayoutData.cards.backgroundCard), + blockTransform(cardLayoutData.cards.backgroundCard), + ], + }, + // card-layout-block: price cards + { + type: 'card-layout-block', + title: 'Card layout with price cards', + children: [ + blockTransform(cardLayoutData.cards.priceCard), + blockTransform(cardLayoutData.cards.priceCard), + blockTransform(cardLayoutData.cards.priceCard), + ], + }, - // slider-block: basic cards - blockTransform(sliderData.default.content), - // slider-block: quote cards - blockTransform(sliderData.quoteCards.content), - // slider-block: banner cards (subtitle already HTML — no blockTransform) - sliderData.banners.content, + // slider-block: basic cards + blockTransform(sliderData.default.content), + // slider-block: quote cards + blockTransform(sliderData.quoteCards.content), + // slider-block: banner cards (subtitle already HTML — no blockTransform) + sliderData.banners.content, - // header-slider-block: default - blockTransform({type: 'header-slider-block', ...headerSliderData.default}), - // header-slider-block: with different slide themes - blockTransform({ - type: 'header-slider-block', - ...headerSliderData.withDifferentSlidesTheme, - }), - ], - } as PageContent - } - /> + // header-slider-block: default + blockTransform({ + type: 'header-slider-block', + ...headerSliderData.default, + }), + // header-slider-block: with different slide themes + blockTransform({ + type: 'header-slider-block', + ...headerSliderData.withDifferentSlidesTheme, + }), + ], + } as PageContent + } + /> + </PageConstructorProvider> ); export const Default = Template.bind({}); diff --git a/src/demo/HeroAndMedia.stories.tsx b/src/demo/HeroAndMedia.stories.tsx index a391703a3a..c8433d8413 100644 --- a/src/demo/HeroAndMedia.stories.tsx +++ b/src/demo/HeroAndMedia.stories.tsx @@ -1,7 +1,8 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../.storybook/utils'; -import {PageConstructor} from '../containers/PageConstructor'; +import {PageConstructor, PageConstructorProvider} from '../containers/PageConstructor'; +import {gravityBlocksExtension} from '../gravity-blocks/extensions'; import {CustomConfig, NavigationData, PageContent} from '../models'; import bannerData from '../blocks/Banner/__stories__/data.json'; @@ -9,7 +10,7 @@ import companiesData from '../blocks/Companies/__stories__/data.json'; import headerData from '../blocks/Header/__stories__/data.json'; import infoData from '../blocks/Info/__stories__/data.json'; import mediaData from '../blocks/Media/__stories__/data.json'; -import navData from '../navigation/__stories__/data.json'; +import navData from '../gravity-blocks/navigation/__stories__/data.json'; export default { title: 'Lab/Tokenization/Blocks/HeroAndMedia', @@ -20,52 +21,64 @@ const Template: StoryFn<{navigation: NavigationData; custom?: CustomConfig}> = ( navigation, custom = {}, }) => ( - <PageConstructor - navigation={navigation} - custom={custom} - content={ - { - blocks: [ - // header-block: default with action buttons - blockTransform(headerData.default), - // header-block: with breadcrumbs + light background - blockTransform(headerData.breadcrumbs[0]), - // header-block: with breadcrumbs + dark theme - blockTransform(headerData.breadcrumbs[1]), - // header-block: with image - blockTransform(headerData.image), - // header-block: with background image and color (media variant) - blockTransform({type: 'header-block', ...headerData.media.image}), + <PageConstructorProvider> + <PageConstructor + extensions={gravityBlocksExtension({ + globalDefaults: { + navigation, + }, + })} + custom={custom} + content={ + { + blocks: [ + // header-block: default with action buttons + blockTransform(headerData.default), + // header-block: with breadcrumbs + light background + blockTransform(headerData.breadcrumbs[0]), + // header-block: with breadcrumbs + dark theme + blockTransform(headerData.breadcrumbs[1]), + // header-block: with image + blockTransform(headerData.image), + // header-block: with background image and color (media variant) + blockTransform({type: 'header-block', ...headerData.media.image}), - // banner-block: light theme with themed image - blockTransform(bannerData.default.content), - // banner-block: forced dark theme - blockTransform(bannerData.darkTheme.content), + // banner-block: light theme with themed image + blockTransform(bannerData.default.content), + // banner-block: forced dark theme + blockTransform(bannerData.darkTheme.content), - // media-block: default with image - blockTransform(mediaData.default), - // media-block: image slider - blockTransform(mediaData.imageSlider), - // media-block: youtube embed - blockTransform({type: 'media-block', ...mediaData.video.youtube}), - // media-block: video with controls - blockTransform({type: 'media-block', ...mediaData.video.staticWithControls}), - // media-block: video with preview - blockTransform({type: 'media-block', ...mediaData.video.videoWithPreview}), + // media-block: default with image + blockTransform(mediaData.default), + // media-block: image slider + blockTransform(mediaData.imageSlider), + // media-block: youtube embed + blockTransform({type: 'media-block', ...mediaData.video.youtube}), + // media-block: video with controls + blockTransform({ + type: 'media-block', + ...mediaData.video.staticWithControls, + }), + // media-block: video with preview + blockTransform({ + type: 'media-block', + ...mediaData.video.videoWithPreview, + }), - // companies-block: title only - blockTransform(companiesData.default.content), - // companies-block: with description - blockTransform(companiesData.withDescription.content), + // companies-block: title only + blockTransform(companiesData.default.content), + // companies-block: with description + blockTransform(companiesData.withDescription.content), - // info-block: dark theme (default) - blockTransform(infoData.default), - // info-block: light theme with background color - blockTransform({type: 'info-block', ...infoData.light}), - ], - } as PageContent - } - /> + // info-block: dark theme (default) + blockTransform(infoData.default), + // info-block: light theme with background color + blockTransform({type: 'info-block', ...infoData.light}), + ], + } as PageContent + } + /> + </PageConstructorProvider> ); export const Default = Template.bind({}); diff --git a/src/demo/InteractiveAndForms.stories.tsx b/src/demo/InteractiveAndForms.stories.tsx index 2a90414dd8..e06cc44762 100644 --- a/src/demo/InteractiveAndForms.stories.tsx +++ b/src/demo/InteractiveAndForms.stories.tsx @@ -2,18 +2,19 @@ import {Meta, StoryFn} from '@storybook/react'; import {scriptsSrc, ymapApiKeyForStorybook} from '../../.storybook/maps'; import {blockTransform} from '../../.storybook/utils'; -import {PageConstructor} from '../containers/PageConstructor'; -import {MapType} from '../context/mapsContext/mapsContext'; -import {MapProvider} from '../context/mapsContext/mapsProvider'; +import {PageConstructor, PageConstructorProvider} from '../containers/PageConstructor'; +import {MapType} from '../gravity-blocks/context/mapsContext/mapsContext'; +import {MapProvider} from '../gravity-blocks/context/mapsContext/mapsProvider'; +import {gravityBlocksExtension} from '../gravity-blocks/extensions'; +import {CustomButton} from '../gravity-blocks/navigation/__stories__/CustomButton/CustomButton'; import {CustomConfig, NavigationData, PageContent} from '../models'; -import {CustomButton} from '../navigation/__stories__/CustomButton/CustomButton'; import filterData from '../blocks/FilterBlock/__stories__/data.json'; import formData from '../blocks/Form/__stories__/data.json'; import iconsData from '../blocks/Icons/__stories__/data.json'; import mapData from '../blocks/Map/__stories__/data.json'; import shareData from '../blocks/Share/__stories__/data.json'; -import navData from '../navigation/__stories__/data.json'; +import navData from '../gravity-blocks/navigation/__stories__/data.json'; export default { title: 'Lab/Tokenization/Blocks/InteractiveAndForms', @@ -24,68 +25,77 @@ const Template: StoryFn<{navigation: NavigationData; custom?: CustomConfig}> = ( navigation, custom = {}, }) => ( - <MapProvider - scriptSrc={scriptsSrc[MapType.Yandex]} - apiKey={ymapApiKeyForStorybook} - type={MapType.Yandex} - > - <PageConstructor - navigation={navigation} - custom={custom} - content={ - { - blocks: [ - // form-block: hubspot form - blockTransform(formData.default), - // form-block: with background color - blockTransform({...formData.default, ...formData.withBackground}), - // form-block: with background image - blockTransform({...formData.default, ...formData.withBackgroundImage}), - // form-block: yandex form - blockTransform(formData.yandexForm), + <PageConstructorProvider> + <MapProvider + scriptSrc={scriptsSrc[MapType.Yandex]} + apiKey={ymapApiKeyForStorybook} + type={MapType.Yandex} + > + <PageConstructor + extensions={gravityBlocksExtension({ + globalDefaults: { + navigation, + }, + })} + custom={custom} + content={ + { + blocks: [ + // form-block: hubspot form + blockTransform(formData.default), + // form-block: with background color + blockTransform({...formData.default, ...formData.withBackground}), + // form-block: with background image + blockTransform({ + ...formData.default, + ...formData.withBackgroundImage, + }), + // form-block: yandex form + blockTransform(formData.yandexForm), - // filter-block: with tag filtering and layout items - blockTransform(filterData.default), + // filter-block: with tag filtering and layout items + blockTransform(filterData.default), - // icons-block: minimal (no title) - blockTransform(iconsData.default.content), - // icons-block: with title and description - blockTransform(iconsData.withDescription.content), + // icons-block: minimal (no title) + blockTransform(iconsData.default.content), + // icons-block: with title and description + blockTransform(iconsData.withDescription.content), - // share-block: default (no title) - blockTransform(shareData.default.content), - // share-block: with custom title - blockTransform(shareData.customTitle.content), + // share-block: default (no title) + blockTransform(shareData.default.content), + // share-block: with custom title + blockTransform(shareData.customTitle.content), - // map-block: default with yandex map - blockTransform({ - type: 'map-block', - title: mapData.common.title, - description: mapData.common.description, - map: mapData.ymap, - }), - // map-block: with additional info and links - blockTransform({ - type: 'map-block', - title: mapData.common.title, - description: mapData.common.description, - additionalInfo: mapData.common.additionalInfo, - links: mapData.common.links, - map: {...mapData.ymap, id: 'common-places-2'}, - }), - // map-block: with buttons - blockTransform({ - type: 'map-block', - title: mapData.common.title, - description: mapData.common.description, - buttons: mapData.common.buttons, - map: {...mapData.ymap, id: 'common-places-3'}, - }), - ], - } as PageContent - } - /> - </MapProvider> + // map-block: default with yandex map + blockTransform({ + type: 'map-block', + title: mapData.common.title, + description: mapData.common.description, + map: mapData.ymap, + }), + // map-block: with additional info and links + blockTransform({ + type: 'map-block', + title: mapData.common.title, + description: mapData.common.description, + additionalInfo: mapData.common.additionalInfo, + links: mapData.common.links, + map: {...mapData.ymap, id: 'common-places-2'}, + }), + // map-block: with buttons + blockTransform({ + type: 'map-block', + title: mapData.common.title, + description: mapData.common.description, + buttons: mapData.common.buttons, + map: {...mapData.ymap, id: 'common-places-3'}, + }), + ], + } as PageContent + } + /> + </MapProvider> + </PageConstructorProvider> ); export const Default = Template.bind({}); diff --git a/src/editor-v2/components/BlockCard/BlockCard.scss b/src/editor-v2/components/BlockCard/BlockCard.scss new file mode 100644 index 0000000000..e55bf31a85 --- /dev/null +++ b/src/editor-v2/components/BlockCard/BlockCard.scss @@ -0,0 +1,46 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}block-card'; + +#{$block} { + padding: 8px; + cursor: grab; + user-select: none; + margin-bottom: 8px; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + text-align: center; + gap: 4px; + max-width: 300px; + + &:active { + background-color: var(--g-color-base-generic-hover); + cursor: grabbing; + } + + &__image { + &-img { + pointer-events: none; + height: 45px; + object-fit: contain; + object-position: center; + } + } + + &__name { + justify-self: flex-end; + color: var(--g-color-text-secondary); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + } + + &__icon { + color: var(--g-color-text-complementary); + } +} diff --git a/src/editor-v2/components/BlockCard/BlockCard.tsx b/src/editor-v2/components/BlockCard/BlockCard.tsx new file mode 100644 index 0000000000..6eacb4cf44 --- /dev/null +++ b/src/editor-v2/components/BlockCard/BlockCard.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; + +import {Card} from '@gravity-ui/uikit'; + +import {editorCn} from '../../utils/cn'; + +import {defaultIcon} from './defaultIcon'; + +import './BlockCard.scss'; + +const b = editorCn('block-card'); + +export interface BlockCardProps { + className?: string; + type: string; + name: string; + previewImg?: string; + onMouseDown: (type: string) => void; +} + +const BlockCard = ({className, type, name, previewImg, onMouseDown}: BlockCardProps) => { + const handleMouseDown = React.useCallback(() => { + onMouseDown(type); + }, [onMouseDown, type]); + + return ( + <Card className={b(null, className)} onMouseDown={handleMouseDown}> + <div className={b('image')}> + {previewImg ? ( + <img className={b('image-img')} src={previewImg} alt="preview of the block" /> + ) : ( + <img className={b('image-img')} src={defaultIcon} alt="preview of the block" /> + )} + </div> + <div className={b('name')}>{name}</div> + </Card> + ); +}; + +export default BlockCard; diff --git a/src/editor-v2/components/BlockCard/defaultIcon.ts b/src/editor-v2/components/BlockCard/defaultIcon.ts new file mode 100644 index 0000000000..11320e90f1 --- /dev/null +++ b/src/editor-v2/components/BlockCard/defaultIcon.ts @@ -0,0 +1,10 @@ +import {svgToDataUri} from '../../../utils'; + +export const defaultIcon = svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<rect x="8.93024" y="7" width="82" height="36" rx="3" fill="#262626"/> +<path d="M53.7548 21.64C53.7548 22.0667 53.6948 22.4467 53.5748 22.78C53.4681 23.1133 53.3148 23.42 53.1148 23.7C52.9281 23.98 52.7081 24.2333 52.4548 24.46C52.2148 24.6867 51.9548 24.9067 51.6748 25.12C51.3681 25.36 51.0814 25.58 50.8148 25.78C50.5614 25.9667 50.3414 26.16 50.1548 26.36C49.9681 26.5467 49.8214 26.7533 49.7148 26.98C49.6081 27.1933 49.5548 27.44 49.5548 27.72V27.94H47.6948V27.6C47.6948 27.1867 47.7414 26.8267 47.8348 26.52C47.9281 26.2 48.0681 25.9133 48.2548 25.66C48.4548 25.3933 48.6948 25.14 48.9748 24.9C49.2548 24.6467 49.5748 24.3867 49.9348 24.12C50.3614 23.7733 50.6948 23.42 50.9348 23.06C51.1881 22.6867 51.3148 22.24 51.3148 21.72C51.3148 21.4667 51.2748 21.2133 51.1948 20.96C51.1281 20.7067 51.0014 20.4867 50.8148 20.3C50.6281 20.1133 50.3748 19.96 50.0548 19.84C49.7348 19.72 49.3214 19.66 48.8148 19.66C48.4148 19.66 48.0548 19.6933 47.7348 19.76C47.4148 19.8267 47.1414 19.9067 46.9148 20C46.6881 20.08 46.5081 20.16 46.3748 20.24C46.2548 20.3067 46.1814 20.3467 46.1548 20.36V18.64C46.1814 18.6267 46.2681 18.58 46.4148 18.5C46.5748 18.42 46.7881 18.34 47.0548 18.26C47.3214 18.1667 47.6414 18.0867 48.0148 18.02C48.4014 17.9533 48.8348 17.92 49.3148 17.92C50.0748 17.92 50.7281 18.0133 51.2748 18.2C51.8348 18.3867 52.2948 18.6467 52.6548 18.98C53.0281 19.3133 53.3014 19.7067 53.4748 20.16C53.6614 20.6133 53.7548 21.1067 53.7548 21.64ZM48.7148 32.14C48.3148 32.14 47.9748 32 47.6948 31.72C47.4281 31.44 47.2948 31.1067 47.2948 30.72C47.2948 30.32 47.4281 29.98 47.6948 29.7C47.9748 29.42 48.3148 29.28 48.7148 29.28C49.1281 29.28 49.4681 29.42 49.7348 29.7C50.0148 29.98 50.1548 30.32 50.1548 30.72C50.1548 31.1067 50.0148 31.44 49.7348 31.72C49.4681 32 49.1281 32.14 48.7148 32.14Z" fill="#C0C8DB"/> +</svg> +`, +); diff --git a/src/editor-v2/components/MessageCard/MessageCard.scss b/src/editor-v2/components/MessageCard/MessageCard.scss new file mode 100644 index 0000000000..a3c19f5488 --- /dev/null +++ b/src/editor-v2/components/MessageCard/MessageCard.scss @@ -0,0 +1,68 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}message-card'; + +#{$block} { + padding: 16px; + border-radius: 8px; + display: flex; + align-items: flex-start; + gap: 12px; + + &__icon { + flex-shrink: 0; + margin-top: 2px; + } + + &__content { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; + } + + &__title { + margin: 0; + } + + &__description { + margin: 0; + } + + &_theme_success { + border: 1px solid var(--g-color-text-positive); + background-color: var(--g-color-base-positive-light); + + #{$block}__icon { + color: var(--g-color-text-positive); + } + } + + &_theme_error { + border: 1px solid var(--g-color-text-danger); + background-color: var(--g-color-base-danger-light); + + #{$block}__icon { + color: var(--g-color-text-danger); + } + } + + &_theme_warning { + border: 1px solid var(--g-color-text-warning); + background-color: var(--g-color-base-warning-light); + + #{$block}__icon { + color: var(--g-color-text-warning); + } + } + + &_theme_info { + border: 1px solid var(--g-color-text-info); + background-color: var(--g-color-base-info-light); + + #{$block}__icon { + color: var(--g-color-text-info); + } + } +} diff --git a/src/editor-v2/components/MessageCard/MessageCard.tsx b/src/editor-v2/components/MessageCard/MessageCard.tsx new file mode 100644 index 0000000000..c051f6afe7 --- /dev/null +++ b/src/editor-v2/components/MessageCard/MessageCard.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; + +import {Check, CircleInfo, TriangleExclamation, Xmark} from '@gravity-ui/icons'; +import {Icon, IconData, Text} from '@gravity-ui/uikit'; + +import {editorCn} from '../../utils/cn'; + +const b = editorCn('message-card'); + +import './MessageCard.scss'; + +export type MessageTheme = 'success' | 'error' | 'warning' | 'info'; + +const DEFAULT_ICONS: Record<MessageTheme, IconData> = { + success: Check, + error: Xmark, + warning: TriangleExclamation, + info: CircleInfo, +}; + +export interface MessageCardProps { + title: string; + description: string; + theme: MessageTheme; + icon?: IconData; +} + +export const MessageCard: React.FC<MessageCardProps> = ({title, description, theme, icon}) => { + const IconComponent = icon || DEFAULT_ICONS[theme]; + + return ( + <div className={b({theme})}> + <Icon className={b('icon')} data={IconComponent} size={20} /> + <div className={b('content')}> + <Text variant="subheader-1" className={b('title')}> + {title} + </Text> + <Text variant="body-1" color="secondary" className={b('description')}> + {description} + </Text> + </div> + </div> + ); +}; diff --git a/src/editor-v2/components/MessageCard/index.ts b/src/editor-v2/components/MessageCard/index.ts new file mode 100644 index 0000000000..6dea415f59 --- /dev/null +++ b/src/editor-v2/components/MessageCard/index.ts @@ -0,0 +1,2 @@ +export {MessageCard} from './MessageCard'; +export type {MessageCardProps, MessageTheme} from './MessageCard'; diff --git a/src/editor-v2/components/Panels/Panels.scss b/src/editor-v2/components/Panels/Panels.scss new file mode 100644 index 0000000000..4388f63440 --- /dev/null +++ b/src/editor-v2/components/Panels/Panels.scss @@ -0,0 +1,37 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}panels'; + +#{$block} { + &__button-wrap { + --pceditor-panels-button-gap: 8px; + position: absolute; + z-index: 1000; + + &_left { + top: var(--pceditor-panels-button-gap); + left: calc(100% + var(--pceditor-panels-button-gap)); + } + + &_right { + top: var(--pceditor-panels-button-gap); + right: calc(100% + var(--pceditor-panels-button-gap)); + } + } + + &__draggable { + position: relative; + + width: 8px; + height: 100%; + cursor: col-resize; + background-color: var(--g-color-line-generic); + + display: flex; + align-items: center; + justify-content: center; + + color: var(--g-color-base-background); + } +} diff --git a/src/editor-v2/components/Panels/Panels.tsx b/src/editor-v2/components/Panels/Panels.tsx new file mode 100644 index 0000000000..23c466b149 --- /dev/null +++ b/src/editor-v2/components/Panels/Panels.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; + +import {Grip, LayoutSideContentLeft, LayoutSideContentRight} from '@gravity-ui/icons'; +import {Button, Icon} from '@gravity-ui/uikit'; +import {ImperativePanelHandle, Panel, PanelGroup, PanelResizeHandle} from 'react-resizable-panels'; + +import {editorCn} from '../../utils/cn'; + +import './Panels.scss'; + +const b = editorCn('panels'); + +interface PanelsProps { + left: React.ReactElement; + middle: React.ReactElement; + right: React.ReactElement; +} + +export const Panels = ({left, right, middle}: PanelsProps) => { + const leftPanel = React.useRef<ImperativePanelHandle>(null); + const rightPanel = React.useRef<ImperativePanelHandle>(null); + + const expandPanel = (reference: React.RefObject<ImperativePanelHandle>) => { + const panel = reference.current; + if (panel) { + panel.expand(); + } + }; + + const isCollapsed = { + left: leftPanel.current?.isCollapsed() || false, + right: rightPanel.current?.isCollapsed() || false, + }; + + return ( + <PanelGroup + className={b('panel')} + autoSaveId="page-constructor-editor" + direction="horizontal" + > + <Panel ref={leftPanel} collapsible defaultSize={25} minSize={15}> + {left} + </Panel> + <PanelResizeHandle className={b('draggable')}> + <Grip className={b('grip')} /> + {isCollapsed.left && ( + <div className={b('button-wrap', {left: true})}> + <Button + className={b('button')} + view="action" + onClick={() => expandPanel(leftPanel)} + > + <Icon data={LayoutSideContentLeft} /> + </Button> + </div> + )} + </PanelResizeHandle> + <Panel minSize={20}>{middle}</Panel> + <PanelResizeHandle className={b('draggable')}> + <Grip className={b('grip')} /> + {isCollapsed.right && ( + <div className={b('button-wrap', {right: true})}> + <Button + className={b('button')} + view="action" + onClick={() => expandPanel(rightPanel)} + > + <Icon data={LayoutSideContentRight} /> + </Button> + </div> + )} + </PanelResizeHandle> + <Panel ref={rightPanel} collapsible minSize={15} defaultSize={25}> + {right} + </Panel> + </PanelGroup> + ); +}; diff --git a/src/editor-v2/components/Sidebar/Sidebar.scss b/src/editor-v2/components/Sidebar/Sidebar.scss new file mode 100644 index 0000000000..1ece6f7382 --- /dev/null +++ b/src/editor-v2/components/Sidebar/Sidebar.scss @@ -0,0 +1,20 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}sidebar'; + +#{$block} { + display: flex; + flex-direction: column; + overflow: hidden; + height: 100%; + + &__block { + width: 100%; + border-bottom: 1px solid var(--g-color-line-generic); + } + + &__tabs { + @include custom-scrollbar(); + } +} diff --git a/src/editor-v2/components/Sidebar/Sidebar.tsx b/src/editor-v2/components/Sidebar/Sidebar.tsx new file mode 100644 index 0000000000..267ad9a933 --- /dev/null +++ b/src/editor-v2/components/Sidebar/Sidebar.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; + +import {editorCn} from '../../utils/cn'; +import Tabs, {TabItemProps} from '../Tabs/Tabs'; + +import './Sidebar.scss'; + +const b = editorCn('sidebar'); + +interface SidebarProps { + tabs: TabItemProps[]; + defaultTab?: string; + top?: React.ElementType[]; + className?: string; +} + +export const Sidebar = ({className, tabs, top = []}: SidebarProps) => { + return ( + <div className={b(null, className)}> + {top.map((TopComponent, idx) => ( + <div key={idx} className={b('block')}> + <TopComponent /> + </div> + ))} + <Tabs items={tabs} /> + </div> + ); +}; diff --git a/src/editor-v2/components/Tabs/Tabs.scss b/src/editor-v2/components/Tabs/Tabs.scss new file mode 100644 index 0000000000..472aaff44d --- /dev/null +++ b/src/editor-v2/components/Tabs/Tabs.scss @@ -0,0 +1,85 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}tabs'; + +#{$block} { + display: flex; + flex-direction: column; + overflow: hidden; + height: 100%; + + .g-button#{$block}__item { + color: var(--g-color-text-hint); + font-weight: 500; + letter-spacing: 0.5px; + cursor: pointer; + font-size: var(--g-text-code-inline-1-font-size); + line-height: var(--g-text-code-inline-2-line-height); + align-items: center; + + &_active { + pointer-events: none; + color: var(--g-color-text-primary); + } + } + + &__tabs-wrapper { + width: 100%; + border-bottom: 1px solid var(--g-color-line-generic); + display: inline-flex; + flex-direction: row; + padding: 12px 4px; + flex: 0 0 auto; + box-sizing: border-box; + overflow-x: auto; + position: sticky; + top: 0; + background-color: var(--g-color-base-background); + /* This ensures the background is visible when sticky */ + z-index: 2; + /* Higher z-index for parent tabs-wrapper */ + } + + /* Make sure any nested tabs component also has proper overflow behavior */ + #{$block} { + overflow: visible; + + /* Ensure nested tabs-wrapper elements are also sticky but positioned below parent tabs-wrapper */ + /* This selector specifically targets tabs-wrapper elements inside a nested tabs component */ + #{$block} &__tabs-wrapper { + position: sticky; + /* Position below parent tabs-wrapper */ + z-index: 1; + background-color: var(--g-color-base-background); + } + } + + &__body { + flex: 1; + min-height: 1px; + height: 100%; + position: relative; + } + + &__panel { + display: flex; + flex-direction: column; + min-height: 0; + height: 100%; + + &[hidden] { + display: none; + } + } + + &__item { + @include custom-scrollbar(); + overflow-y: auto; + max-height: 100%; + + &_padding { + padding: 16px 12px; + } + } +} diff --git a/src/editor-v2/components/Tabs/Tabs.tsx b/src/editor-v2/components/Tabs/Tabs.tsx new file mode 100644 index 0000000000..69cb93d443 --- /dev/null +++ b/src/editor-v2/components/Tabs/Tabs.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; + +import {Button} from '@gravity-ui/uikit'; + +import {editorCn} from '../../utils/cn'; + +import './Tabs.scss'; + +const b = editorCn('tabs'); + +export interface TabItemProps { + id: string; + title: string; + component: React.ElementType; + withPadding?: boolean; +} + +export interface TabsProps { + className?: string; + items: TabItemProps[]; + defaultTab?: string | null; +} + +const Tabs = ({className, items, defaultTab}: TabsProps) => { + const [currentTab, setCurrentTab] = React.useState(defaultTab); + + const activeTab = React.useMemo(() => { + if (currentTab) { + const findTab = items.find(({id}) => id === currentTab); + if (findTab) { + return findTab; + } + } + + return items[0] || null; + }, [currentTab, items]); + + const handleClick = React.useCallback( + (tabItem: TabItemProps) => () => { + setCurrentTab(tabItem.id); + }, + [], + ); + + return ( + <div className={b(null, className)}> + {items.length > 1 && ( + <div className={b('tabs-wrapper')} role="tablist"> + {items.map((item) => { + const isActive = item.id === activeTab.id; + + return ( + <Button + view="flat" + size="s" + className={b('item', {active: isActive})} + key={item.id} + extraProps={{ + role: 'tab', + }} + onClick={handleClick(item)} + > + {item.title} + </Button> + ); + })} + </div> + )} + <div className={b('body')}> + {items.map((item) => { + const isActive = item.id === activeTab.id; + const TabComponent = item.component; + return ( + <div key={item.id} className={b('panel')} hidden={!isActive}> + <TabComponent + className={b('item', {padding: item.withPadding || false})} + /> + </div> + ); + })} + </div> + </div> + ); +}; + +export default Tabs; diff --git a/src/editor-v2/constants.ts b/src/editor-v2/constants.ts new file mode 100644 index 0000000000..22c3e53f27 --- /dev/null +++ b/src/editor-v2/constants.ts @@ -0,0 +1 @@ +export const ZOOM_STEPS = [25, 33, 50, 75, 100, 125, 150, 200, 250, 300]; diff --git a/src/editor-v2/constants/messages.ts b/src/editor-v2/constants/messages.ts new file mode 100644 index 0000000000..21fc108545 --- /dev/null +++ b/src/editor-v2/constants/messages.ts @@ -0,0 +1,12 @@ +export const MESSAGES = { + NO_BLOCK_SELECTED: { + title: 'Блок не выбран', + description: + 'Выберите блок на экране или в слоях в левом меню для редактирования его свойств', + }, + UNSUPPORTED_BLOCK: { + title: 'Неподдерживаемый блок', + description: + 'Данный тип блока не поддерживается в редакторе. Но вы также можете отредактировать его конфигурацию вручную в меню RAW.', + }, +} as const; diff --git a/src/editor-v2/containers/BigOverlay/BigOverlay.scss b/src/editor-v2/containers/BigOverlay/BigOverlay.scss new file mode 100644 index 0000000000..1d09dfdb1b --- /dev/null +++ b/src/editor-v2/containers/BigOverlay/BigOverlay.scss @@ -0,0 +1,31 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}big-overlay'; + +#{$block} { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + pointer-events: none; + overflow: hidden; + + $border: 3px var(--g-color-base-brand) solid; + + &__border { + pointer-events: none; + position: absolute; + width: 40px; + height: 40px; + margin-left: -20px; + margin-top: -20px; + background-color: var(--g-color-base-brand); + display: flex; + align-items: center; + justify-content: center; + border-radius: 10px; + //transition: top .1s ease, left .1s ease, height .1s ease, width .1s ease; + } +} diff --git a/src/editor-v2/containers/BigOverlay/BigOverlay.tsx b/src/editor-v2/containers/BigOverlay/BigOverlay.tsx new file mode 100644 index 0000000000..adcc724a9d --- /dev/null +++ b/src/editor-v2/containers/BigOverlay/BigOverlay.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; + +import {Stop} from '@gravity-ui/icons'; + +import {useMainEditorStore} from '../../hooks/useMainEditorStore'; +import {editorCn} from '../../utils/cn'; + +import './BigOverlay.scss'; + +const b = editorCn('big-overlay'); + +const BigOverlay = ({className}: {className?: string}) => { + const {manipulateOverlayMode} = useMainEditorStore(); + const [mousePosition, setMousePosition] = React.useState<{x: number; y: number} | undefined>( + undefined, + ); + const overlayRef = React.useRef<HTMLDivElement>(null); + + React.useEffect(() => { + const onMouseMove = (event: MouseEvent) => { + const rect = overlayRef.current?.getBoundingClientRect(); + setMousePosition({ + x: event.clientX - (rect?.left ?? 0), + y: event.clientY - (rect?.top ?? 0), + }); + }; + + const onMouseUp = () => { + setMousePosition(undefined); + }; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mousedown', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + + return () => { + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mousedown', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + }; + }, []); + + return ( + <div ref={overlayRef} className={b(null, className)}> + {mousePosition && manipulateOverlayMode ? ( + <div + className={b('border')} + style={{ + top: mousePosition.y, + left: mousePosition.x, + }} + > + <Stop height={20} width={20} /> + </div> + ) : null} + </div> + ); +}; + +export default BigOverlay; diff --git a/src/editor-v2/containers/BlockConfigForm/BlockConfigForm.scss b/src/editor-v2/containers/BlockConfigForm/BlockConfigForm.scss new file mode 100644 index 0000000000..8e679df837 --- /dev/null +++ b/src/editor-v2/containers/BlockConfigForm/BlockConfigForm.scss @@ -0,0 +1,31 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}block-config-form'; + +#{$block} { + height: 100%; + + &_empty { + padding: 16px 12px; + } + + &__title { + @include text-subheader-3; + padding: 16px 12px; + } + + &__form { + padding: 0 0 12px; + } + + &__empty { + padding: 12px; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + height: 100%; + width: 100%; + } +} diff --git a/src/editor-v2/containers/BlockConfigForm/BlockConfigForm.tsx b/src/editor-v2/containers/BlockConfigForm/BlockConfigForm.tsx new file mode 100644 index 0000000000..d25266fdac --- /dev/null +++ b/src/editor-v2/containers/BlockConfigForm/BlockConfigForm.tsx @@ -0,0 +1,70 @@ +import _ from 'lodash'; + +import FormGenerator from '../../../form-generator-v2/FormGenerator'; +import {MessageCard} from '../../components/MessageCard'; +import {MESSAGES} from '../../constants/messages'; +import {useMainEditorStore} from '../../hooks/useMainEditorStore'; +import {generateChildrenPathFromArray} from '../../utils'; +import {editorCn} from '../../utils/cn'; + +import './BlockConfigForm.scss'; + +const b = editorCn('block-config-form'); + +interface BlockConfigFormProps { + className?: string; +} + +const BlockConfigForm = ({className}: BlockConfigFormProps) => { + const {selectedBlock, content, blocks, subBlocks, updateField} = useMainEditorStore(); + + const currentBlockPath = selectedBlock ? generateChildrenPathFromArray(selectedBlock) : '[]'; + + const currentConfig = _.get(content.blocks, currentBlockPath || ''); + const currentSchema = [...blocks, ...subBlocks].find(({type}) => type === currentConfig?.type); + + const onUpdateByKey = (key: string, value: unknown) => { + updateField('blocks' + currentBlockPath + '.' + key, value); + }; + + if (!currentConfig) { + return ( + <div className={b({empty: true}, className)}> + <MessageCard + title={MESSAGES.NO_BLOCK_SELECTED.title} + description={MESSAGES.NO_BLOCK_SELECTED.description} + theme="info" + /> + </div> + ); + } + + if (!currentSchema) { + return ( + <div className={b({empty: true}, className)}> + <MessageCard + title={MESSAGES.UNSUPPORTED_BLOCK.title} + description={MESSAGES.UNSUPPORTED_BLOCK.description} + theme="warning" + /> + </div> + ); + } + + return ( + <div className={b(null, className)}> + <div className={b('title')}>{currentSchema.schema.name}</div> + <div className={b('form')}> + {currentSchema.schema.inputs && ( + <FormGenerator + contentConfig={currentConfig} + blockConfig={currentSchema.schema.inputs} + onUpdateByKey={onUpdateByKey} + /> + )} + </div> + </div> + ); +}; + +export default BlockConfigForm; diff --git a/src/editor-v2/containers/BlocksList/BlocksList.scss b/src/editor-v2/containers/BlocksList/BlocksList.scss new file mode 100644 index 0000000000..6f3a022dab --- /dev/null +++ b/src/editor-v2/containers/BlocksList/BlocksList.scss @@ -0,0 +1,88 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}blocks-list'; + +#{$block} { + display: flex; + flex-direction: column; + gap: 12px; + + &__search { + display: flex; + gap: 8px; + align-items: center; + } + + &__title { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 4px; + background: none; + border: none; + cursor: pointer; + width: 100%; + text-align: left; + user-select: none; + outline: none; + + color: var(--g-color-text-secondary); + font-size: var(--g-text-subheader-1-font-size); + line-height: var(--g-text-subheader-1-line-height); + font-weight: 500; + + border-radius: $editorControlBorderRadius; + transition: background-color $editorTransitionTime; + + &:hover { + background-color: var(--g-color-base-generic-hover); + } + + &:focus { + outline: none; + } + + &-icon { + flex-shrink: 0; + color: var(--g-color-text-hint); + transition: transform $editorTransitionTime; + } + } + + &__group { + &:first-child { + margin-top: 0; + } + + &:last-child { + border-bottom: none; + } + + &_collapsed { + #{$block}__title-icon { + transform: rotate(0deg); + } + } + + &:not(&_collapsed) { + #{$block}__title-icon { + transform: rotate(0deg); + } + } + + &-items { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 8px; + margin-top: 8px; + } + } + + &__search { + &-icon { + margin-left: 8px; + margin-right: 8px; + } + } +} diff --git a/src/editor-v2/containers/BlocksList/BlocksList.tsx b/src/editor-v2/containers/BlocksList/BlocksList.tsx new file mode 100644 index 0000000000..9959f75aec --- /dev/null +++ b/src/editor-v2/containers/BlocksList/BlocksList.tsx @@ -0,0 +1,175 @@ +import * as React from 'react'; + +import { + ChevronDown, + ChevronRight, + Eye, + EyeSlash, + Folder, + FolderOpen, + Magnifier, +} from '@gravity-ui/icons'; +import {DropdownMenu, Icon, TextInput} from '@gravity-ui/uikit'; + +import {ItemConfig} from '../../../common/types'; +import {ClassNameProps} from '../../../models'; +import BlockCard from '../../components/BlockCard/BlockCard'; +import {useMainEditorStore} from '../../hooks/useMainEditorStore'; +import {editorCn} from '../../utils/cn'; + +import './BlocksList.scss'; + +const b = editorCn('blocks-list'); + +interface BlockGroups { + [key: string]: ItemConfig[]; +} + +interface BlockListProps extends ClassNameProps {} + +const BlocksList = ({className}: BlockListProps) => { + const {blocks, enableInsertMode} = useMainEditorStore(); + const [search, setSearch] = React.useState(''); + const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(new Set()); + const [showHidden, setShowHidden] = React.useState(false); + + const onMouseDown = React.useCallback( + (blockType: string) => { + enableInsertMode(blockType); + }, + [enableInsertMode], + ); + + const toggleGroup = React.useCallback((groupKey: string) => { + setCollapsedGroups((prev) => { + const newSet = new Set(prev); + if (newSet.has(groupKey)) { + newSet.delete(groupKey); + } else { + newSet.add(groupKey); + } + return newSet; + }); + }, []); + + const expandAll = React.useCallback(() => { + setCollapsedGroups(new Set()); + }, []); + + const groups = React.useMemo(() => { + const blocksGroups = blocks.reduce<BlockGroups>((acc, currentBlock) => { + const group = currentBlock.schema.group; + + if (!showHidden && currentBlock.schema.hidden) { + return acc; + } + + if ( + search && + currentBlock.type.toLowerCase().indexOf(search.toLowerCase()) === -1 && + currentBlock.schema.name.toLowerCase().indexOf(search.toLowerCase()) === -1 + ) { + return acc; + } + if (group) { + if (!acc[group]) { + /* eslint-disable no-param-reassign */ + acc[group] = []; + } + acc[group].push(currentBlock); + } else { + if (!acc['Other']) { + /* eslint-disable no-param-reassign */ + acc['Other'] = []; + } + acc['Other'].push(currentBlock); + } + + return acc; + }, {} as BlockGroups); + + return Object.keys(blocksGroups) + .sort() + .reduce((sortedGroups, key) => { + sortedGroups[key] = blocksGroups[key]; + return sortedGroups; + }, {} as BlockGroups); + }, [blocks, search, showHidden]); + + const collapseAll = React.useCallback(() => { + setCollapsedGroups(new Set(Object.keys(groups))); + }, [groups]); + + const allGroupsExpanded = collapsedGroups.size === 0; + const allGroupsCollapsed = collapsedGroups.size === Object.keys(groups).length; + + return ( + <div className={b(null, className)}> + <div className={b('search')}> + <TextInput + size="m" + hasClear + placeholder="Search block" + onUpdate={setSearch} + value={search} + startContent={<Icon className={b('search-icon')} data={Magnifier} />} + /> + <DropdownMenu + items={[ + { + action: expandAll, + text: 'Раскрыть все', + iconStart: <Icon data={FolderOpen} />, + disabled: allGroupsExpanded, + }, + { + action: collapseAll, + text: 'Свернуть все', + iconStart: <Icon data={Folder} />, + disabled: allGroupsCollapsed, + }, + { + action: () => setShowHidden(!showHidden), + text: showHidden ? 'Скрыть скрытые' : 'Показать скрытые', + iconStart: <Icon data={showHidden ? EyeSlash : Eye} />, + }, + ]} + /> + </div> + {Object.entries(groups).map(([key, groupBlocks]) => { + const isCollapsed = collapsedGroups.has(key); + return ( + <div className={b('group', {collapsed: isCollapsed})} key={key}> + <button + className={b('title')} + onClick={() => toggleGroup(key)} + type="button" + > + <Icon + className={b('title-icon')} + data={isCollapsed ? ChevronRight : ChevronDown} + size={14} + /> + {key} + </button> + {!isCollapsed && ( + <div className={b('group-items')}> + {groupBlocks.map(({type, schema: {name, previewImg}}) => ( + <BlockCard + key={type} + type={type} + name={name} + previewImg={previewImg} + onMouseDown={onMouseDown} + /> + ))} + </div> + )} + </div> + ); + })} + </div> + ); +}; + +export default BlocksList; diff --git a/src/editor-v2/containers/Editor/Editor.scss b/src/editor-v2/containers/Editor/Editor.scss new file mode 100644 index 0000000000..da304c330d --- /dev/null +++ b/src/editor-v2/containers/Editor/Editor.scss @@ -0,0 +1,48 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}editor'; + +#{$block} { + margin: 0; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + height: 100%; + width: 100%; + position: relative; + + &__header { + height: $headerHeight; + width: 100%; + border-bottom: 1px solid var(--g-color-line-generic); + } + + &__body { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + width: 100%; + max-height: calc(100% - $headerHeight); + } + + &__canvas { + flex: 1; + } + + &__overlay { + position: absolute; + height: 100%; + width: 100%; + z-index: 100; + } + + &__debug-store { + position: fixed; + right: 8px; + bottom: 8px; + z-index: 1000; + } +} diff --git a/src/editor-v2/containers/Editor/Editor.tsx b/src/editor-v2/containers/Editor/Editor.tsx new file mode 100644 index 0000000000..a998babf2e --- /dev/null +++ b/src/editor-v2/containers/Editor/Editor.tsx @@ -0,0 +1,150 @@ +import * as React from 'react'; + +import {usePostMessageAPIListener} from '../../../common/postMessage'; +import {PageContent} from '../../../models'; +import {Panels} from '../../components/Panels/Panels'; +import {Sidebar} from '../../components/Sidebar/Sidebar'; +import BigOverlay from '../../containers/BigOverlay/BigOverlay'; +import MiddleScreen from '../../containers/MiddleScreen/MiddleScreen'; +import {MainEditorStoreProvider} from '../../context/editorStore'; +import {IframeProvider} from '../../context/iframeContext'; +import {useEditorTabs} from '../../hooks/useEditorTabs'; +import useMainEditorInitialize from '../../hooks/useMainEditorInitialize'; +import {useMainEditorStore} from '../../hooks/useMainEditorStore'; +import {editorCn} from '../../utils/cn'; +import Source from '../Source/Source'; +import ViewSwitches from '../ViewSwitches/ViewSwitches'; + +import './Editor.scss'; + +const b = editorCn('editor'); + +interface SidebarTabComponent { + id: string; + title: string; + component: React.ElementType; +} + +interface EditorSlots { + middleTop?: React.ElementType; + leftTop?: React.ElementType[]; + rightTop?: React.ElementType[]; + leftTabs?: SidebarTabComponent[]; + rightTabs?: SidebarTabComponent[]; +} + +export interface EditorProviderProps { + initialUrl: string; + disableUrlField?: boolean; + children: React.ReactNode; +} + +export interface EditorViewProps { + onUpdate?: (pageContent: PageContent) => void; + content?: PageContent; + slots?: EditorSlots; +} + +type EditorProps = Omit<EditorProviderProps, 'children'> & EditorViewProps; + +const EditorViewInternal = ({slots = {}, content}: EditorViewProps) => { + const store = useMainEditorStore(); + const {manipulateOverlayMode, disableMode, undo, redo} = store; + + useMainEditorInitialize(content); + + usePostMessageAPIListener( + 'ON_EDITOR_UNDO', + () => { + undo(); + }, + [undo], + ); + usePostMessageAPIListener( + 'ON_EDITOR_REDO', + () => { + redo(); + }, + [redo], + ); + + React.useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (!(e.metaKey || e.ctrlKey)) { + return; + } + + if (e.key.toLowerCase() !== 'z') { + return; + } + + const target = e.target as HTMLElement | null; + if (target?.closest('input, textarea, select, [contenteditable="true"]')) { + return; + } + + e.preventDefault(); + + if (e.shiftKey) { + redo(); + } else { + undo(); + } + }; + + window.addEventListener('keydown', onKeyDown, true); + + return () => window.removeEventListener('keydown', onKeyDown, true); + }, [redo, undo]); + + // Disable insert mode on any MouseUp event + // Maybe should be attached to body + const onMouseUp = React.useCallback( + (e: React.MouseEvent) => { + if (manipulateOverlayMode) { + e.preventDefault(); + disableMode(); + } + }, + [disableMode, manipulateOverlayMode], + ); + const {left, right} = useEditorTabs(slots); + + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions + <div className={b()} onMouseUp={onMouseUp}> + <div className={b('body')}> + <Panels + left={<Sidebar tabs={left} top={slots.leftTop} />} + right={ + <Sidebar + tabs={right} + top={[...(slots.rightTop || []), Source, ViewSwitches]} + defaultTab="block-config" + /> + } + middle={<MiddleScreen CustomTop={slots.middleTop} />} + /> + </div> + <BigOverlay className={b('overlay')} /> + </div> + ); +}; + +export const EditorProvider = ({initialUrl, disableUrlField, children}: EditorProviderProps) => { + return ( + <IframeProvider initialUrl={initialUrl} disableUrlField={disableUrlField}> + <MainEditorStoreProvider>{children}</MainEditorStoreProvider> + </IframeProvider> + ); +}; + +export const EditorView = (props: EditorViewProps) => <EditorViewInternal {...props} />; + +export const Editor = (props: EditorProps) => { + return ( + <EditorProvider initialUrl={props.initialUrl} disableUrlField={props.disableUrlField}> + <EditorView {...props} /> + </EditorProvider> + ); +}; diff --git a/src/editor-v2/containers/GlobalConfig/GlobalConfig.scss b/src/editor-v2/containers/GlobalConfig/GlobalConfig.scss new file mode 100644 index 0000000000..f17318cedf --- /dev/null +++ b/src/editor-v2/containers/GlobalConfig/GlobalConfig.scss @@ -0,0 +1,12 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}global-config'; + +#{$block} { + &__title { + @include text-subheader-3; + padding: 16px 12px; + border-bottom: 1px solid var(--g-color-line-generic); + } +} diff --git a/src/editor-v2/containers/GlobalConfig/GlobalConfig.tsx b/src/editor-v2/containers/GlobalConfig/GlobalConfig.tsx new file mode 100644 index 0000000000..3098c056d6 --- /dev/null +++ b/src/editor-v2/containers/GlobalConfig/GlobalConfig.tsx @@ -0,0 +1,28 @@ +import FormGenerator from '../../../form-generator-v2/FormGenerator'; +import {useMainEditorStore} from '../../hooks/useMainEditorStore'; +import {editorCn} from '../../utils/cn'; + +import './GlobalConfig.scss'; + +const b = editorCn('global-config'); + +export interface GlobalConfigProps { + className?: string; +} + +const GlobalConfig = ({className}: GlobalConfigProps) => { + const {global, content, updateField} = useMainEditorStore(); + + const onUpdate = (key: string, value: unknown) => { + updateField(key, value); + }; + + return ( + <div className={b(null, className)}> + <div className={b('title')}>Global Config</div> + <FormGenerator contentConfig={content} blockConfig={global} onUpdateByKey={onUpdate} /> + </div> + ); +}; + +export default GlobalConfig; diff --git a/src/editor-v2/containers/MiddleScreen/MiddleScreen.scss b/src/editor-v2/containers/MiddleScreen/MiddleScreen.scss new file mode 100644 index 0000000000..0119d9d515 --- /dev/null +++ b/src/editor-v2/containers/MiddleScreen/MiddleScreen.scss @@ -0,0 +1,137 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}middle-screen'; + +#{$block} { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + + &__topbar { + flex: 0 0 auto; + } + + &__content { + flex: 1 1 auto; + overflow-y: auto; + // height: 100%; + background-color: var(--g-color-text-secondary); + + &_fullscreen { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 1000; + background-color: #000; + } + } + + &__wrapper { + width: 100%; + height: 100%; + margin: auto; + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + position: relative; + } + + &__canvas { + @include custom-scrollbar(); + background-color: var(--g-color-base-background); + transform-origin: center top; + position: absolute; + height: 100%; + max-width: 100%; + + &_fullscreen { + overflow: hidden; + width: 100%; + height: 100%; + max-width: 100%; + } + + &_withBackground { + background-color: var(--g-color-base-generic-accent); + } + } + + &__iframe-container { + position: relative; + margin: 0 auto; + + &_fullscreen { + height: 100%; + } + } + + &__iframe { + display: block; + + &_fullscreen { + width: 100%; + height: 100%; + } + } + + &__exit-preview-container { + position: fixed; + top: 16px; + right: 24px; + z-index: 1001; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--g-color-base-background); + + &:hover { + opacity: 1; + + #{$block}__exit-preview-text { + max-width: 100%; + } + } + } + + &__exit-preview-text { + display: inline-block; + width: auto; + max-width: 3px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + transition: max-width 0.3s ease-in-out; + } + + &__overlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + } + + &__border { + position: absolute; + border: 3px var(--g-color-line-brand) solid; + border-radius: 10px; + } + + &__loading { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: var(--g-color-base-background); + display: flex; + align-items: center; + justify-content: center; + } +} diff --git a/src/editor-v2/containers/MiddleScreen/MiddleScreen.tsx b/src/editor-v2/containers/MiddleScreen/MiddleScreen.tsx new file mode 100644 index 0000000000..0f07617daa --- /dev/null +++ b/src/editor-v2/containers/MiddleScreen/MiddleScreen.tsx @@ -0,0 +1,140 @@ +import * as React from 'react'; + +import {Xmark} from '@gravity-ui/icons'; +import {ActionTooltip, Button, Icon, Loader} from '@gravity-ui/uikit'; + +import {usePostMessageAPIListener} from '../../../common/postMessage'; +import {IframeContext} from '../../context/iframeContext'; +import {useMainEditorStore} from '../../hooks/useMainEditorStore'; +import {editorCn} from '../../utils/cn'; +import Overlay from '../Overlay/Overlay'; + +import './MiddleScreen.scss'; + +const b = editorCn('middle-screen'); + +// the maximum height of the iframe +const IFRAME_MAX_HEIGHT = 50000; + +interface MiddleScreenProps { + className?: string; + CustomTop?: React.ElementType; +} + +const MiddleScreen = ({className, CustomTop}: MiddleScreenProps) => { + const {zoom, initialized, deviceWidth, isPreviewMode, togglePreviewMode} = useMainEditorStore(); + const {url, setIframeElement} = React.useContext(IframeContext); + const [canvasRef, setCanvasRef] = React.useState<HTMLDivElement | null>(null); + const [height, setHeight] = React.useState(0); + + const canvasStyle = React.useMemo( + () => ({ + transform: isPreviewMode ? 'none' : `scale(${zoom}%)`, + height: isPreviewMode ? '100%' : `${(100 / zoom) * 100}%`, + width: isPreviewMode ? '100%' : `${(100 / zoom) * 100}%`, + maxWidth: isPreviewMode ? '100%' : `${(100 / zoom) * 100}%`, + }), + [isPreviewMode, zoom], + ); + + const onResize = React.useCallback( + (newHeight: number) => { + const settedHeight = newHeight + 100; + setHeight(settedHeight > IFRAME_MAX_HEIGHT ? IFRAME_MAX_HEIGHT : settedHeight); + }, + [setHeight], + ); + + usePostMessageAPIListener('ON_RESIZE', ({height: newHeight}) => { + onResize(newHeight); + }); + + usePostMessageAPIListener('ON_INIT', ({height: newHeight}) => { + onResize(newHeight); + }); + + const isWithBackground = React.useMemo(() => { + return deviceWidth !== '100%'; + }, [deviceWidth]); + + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (isPreviewMode && e.key === 'Escape') togglePreviewMode(); + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isPreviewMode, togglePreviewMode]); + + return ( + <div className={b(null, className)}> + {CustomTop && !isPreviewMode ? ( + <div className={b('topbar')}> + <CustomTop /> + </div> + ) : null} + <div className={b('content', {fullscreen: isPreviewMode})}> + <div className={b('wrapper')}> + <div + ref={setCanvasRef} + className={b('canvas', { + hidden: !initialized, + fullscreen: isPreviewMode, + withBackground: isWithBackground, + })} + style={canvasStyle} + > + <div + className={b('iframe-container', {fullscreen: isPreviewMode})} + style={{width: isPreviewMode ? '100%' : deviceWidth}} + > + <iframe + ref={(instance) => { + if (instance) { + setIframeElement(instance); + } + }} + className={b('iframe', {fullscreen: isPreviewMode})} + src={url} + height={isPreviewMode ? '100%' : `${height}px`} + width={isPreviewMode ? '100%' : deviceWidth} + frameBorder="0" + title="Page Constructor Iframe" + /> + {!isPreviewMode && ( + <Overlay className={b('overlay')} canvasElement={canvasRef} /> + )} + </div> + {isPreviewMode && ( + <div className={b('exit-preview-container')}> + <ActionTooltip + title="Exit preview mode" + placement="left" + hotkey="Escape" + > + <Button + view="action" + className={b('exit-preview')} + onClick={togglePreviewMode} + aria-label="Exit preview mode" + title="Exit preview mode" + size="m" + > + <Icon size={20} data={Xmark} /> + </Button> + </ActionTooltip> + </div> + )} + {!initialized && ( + <div className={b('loading')}> + <Loader size={'l'} /> + </div> + )} + </div> + </div> + </div> + </div> + ); +}; + +export default MiddleScreen; diff --git a/src/editor-v2/containers/Overlay/Overlay.scss b/src/editor-v2/containers/Overlay/Overlay.scss new file mode 100644 index 0000000000..9cc1a03c8f --- /dev/null +++ b/src/editor-v2/containers/Overlay/Overlay.scss @@ -0,0 +1,97 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}overlay'; + +#{$block} { + $border-width: 3px; + $border-select: 5px var(--g-color-base-brand) solid; + $border: $border-width var(--g-color-base-brand) solid; + $border-radius: 8px; + + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + + &__hit { + position: absolute; + cursor: pointer; + } + + &__border { + position: absolute; + border: $border; + border-radius: $border-radius; + box-sizing: border-box; + box-shadow: 4px 4px 8px 0 var(--g-color-sfx-shadow); + pointer-events: none; + z-index: 100; + + &_hover { + opacity: 0.5; + z-index: 99; + } + } + + &__line { + position: absolute; + box-sizing: border-box; + border-radius: $border-radius; + pointer-events: none; + z-index: 10; + + &_position { + border: $border-select; + + &_left, + &_top { + border-top: $border-select; + border-left: $border-select; + border-right: none; + border-bottom: none; + } + + &_right, + &_bottom { + border-right: $border-select; + border-bottom: $border-select; + border-top: none; + border-left: none; + } + } + } + + &__actions { + pointer-events: auto; + position: absolute; + bottom: -$border-width; + left: 50%; + transform: translate(-50%, 100%); + z-index: 1000; + + display: flex; + align-items: flex-start; + background: transparent; + } + + &__actions-box { + &_main { + display: flex; + gap: 2px; + align-items: center; + padding: 1px 8px 4px; + background-color: var(--g-color-base-brand); + border-radius: 0px 0px 8px 8px; + } + + &_reorder { + padding: 3px 4px; + } + } + + &__reorder-icon { + color: var(--g-color-base-brand); + } +} diff --git a/src/editor-v2/containers/Overlay/Overlay.tsx b/src/editor-v2/containers/Overlay/Overlay.tsx new file mode 100644 index 0000000000..2cca695db4 --- /dev/null +++ b/src/editor-v2/containers/Overlay/Overlay.tsx @@ -0,0 +1,277 @@ +import * as React from 'react'; + +import {ChevronDown, ChevronUp, Copy, TrashBin} from '@gravity-ui/icons'; +import {Button, Icon} from '@gravity-ui/uikit'; +import _ from 'lodash'; + +import {usePostMessageAPIListener} from '../../../common/postMessage'; +import {SerializableRect} from '../../../common/types/rect'; +import {getCursorPositionOverElement} from '../../../utils/editor'; +import {useMainEditorStore} from '../../hooks/useMainEditorStore'; +import {editorCn} from '../../utils/cn'; + +import './Overlay.scss'; + +const b = editorCn('overlay'); + +type InsertPosition = 'top' | 'bottom' | 'left' | 'right'; + +interface OverlayProps { + className?: string; + canvasElement?: HTMLDivElement | null; +} + +const findRectByPath = ( + rectMap: Array<{path: number[]; rect: SerializableRect}>, + path: number[] | null, +): SerializableRect | null => { + if (!path) return null; + return rectMap.find((entry) => _.isEqual(entry.path, path))?.rect ?? null; +}; + +const Overlay = ({className, canvasElement}: OverlayProps) => { + const { + rectMap, + setRectMap, + selectedBlock, + setSelectedBlock, + deleteBlock, + duplicateBlock, + manipulateOverlayMode, + preInsertBlockType, + preReorderBlockPath, + insertBlock, + reorderBlock, + disableMode, + } = useMainEditorStore(); + + const [hoveredPath, setHoveredPath] = React.useState<number[] | null>(null); + const [insertPosition, setInsertPosition] = React.useState<InsertPosition | null>(null); + + usePostMessageAPIListener( + 'ON_UPDATE_RECT_MAP', + ({rects}) => { + setRectMap(rects); + }, + [setRectMap], + ); + + const selectedRect = React.useMemo( + () => findRectByPath(rectMap, selectedBlock), + [rectMap, selectedBlock], + ); + const hoveredRect = React.useMemo( + () => findRectByPath(rectMap, hoveredPath), + [rectMap, hoveredPath], + ); + + // Auto-scroll to selected block when it changes + React.useEffect(() => { + if (selectedRect && canvasElement) { + const canvasHeight = canvasElement.clientHeight; + const scrollPosition = selectedRect.top - canvasHeight / 2 + selectedRect.height / 2; + canvasElement.scrollTo({ + top: Math.max(0, scrollPosition), + behavior: 'smooth', + }); + } + // Intentionally depends on selectedBlock identity (path), not rect value, to avoid scroll jitter on rect updates. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedBlock, canvasElement]); + + const handleClick = React.useCallback( + (path: number[]) => { + if (selectedBlock && _.isEqual(selectedBlock, path)) { + setSelectedBlock(null); + } else { + setSelectedBlock(path); + } + }, + [selectedBlock, setSelectedBlock], + ); + + const handleMouseEnter = React.useCallback((path: number[]) => { + setHoveredPath(path); + }, []); + + const handleMouseLeave = React.useCallback((path: number[]) => { + setHoveredPath((current) => (_.isEqual(current, path) ? null : current)); + }, []); + + const handleMouseMove = React.useCallback( + (e: React.MouseEvent) => { + if (!manipulateOverlayMode) { + return; + } + + const domRect = e.currentTarget.getBoundingClientRect(); + const position = getCursorPositionOverElement(domRect, e); + setInsertPosition(position as InsertPosition); + }, + [manipulateOverlayMode], + ); + + const handleKeyDown = React.useCallback( + (e: React.KeyboardEvent, path: number[]) => { + if (e.key === 'Enter' || e.key === ' ') { + e.stopPropagation(); + handleClick(path); + } + }, + [handleClick], + ); + + const handleMouseUp = React.useCallback( + (path: number[]) => { + if (!manipulateOverlayMode) return; + + const position = insertPosition; + const prepend = position === 'left' || position === 'top'; + + if (manipulateOverlayMode === 'insert' && preInsertBlockType) { + insertBlock(path, preInsertBlockType, prepend ? 'prepend' : 'append'); + } else if (manipulateOverlayMode === 'reorder' && preReorderBlockPath) { + reorderBlock(preReorderBlockPath, path, prepend ? 'prepend' : 'append'); + } + + disableMode(); + setInsertPosition(null); + }, + [ + manipulateOverlayMode, + preInsertBlockType, + preReorderBlockPath, + insertBlock, + reorderBlock, + disableMode, + insertPosition, + ], + ); + + const handleMoveUp = () => { + if (!selectedBlock) return; + const destination = [...selectedBlock]; + const newLastDestination = destination[destination.length - 1] - 1; + + if (newLastDestination < 0) { + return; + } + + destination[destination.length - 1] = newLastDestination; + reorderBlock(selectedBlock, destination, 'prepend'); + }; + + const handleMoveDown = () => { + if (!selectedBlock) return; + const destination = [...selectedBlock]; + const newLastDestination = destination[destination.length - 1] + 1; + + destination[destination.length - 1] = newLastDestination; + reorderBlock(selectedBlock, destination, 'append'); + }; + + const showHoverBorder = + hoveredRect && (!selectedBlock || !_.isEqual(hoveredPath, selectedBlock)); + + return ( + <div className={b(null, className)}> + {rectMap.map(({path, rect, dropZone}) => { + const key = path.join('.'); + return ( + <div + key={key} + role={dropZone ? 'presentation' : 'button'} + tabIndex={dropZone ? -1 : 0} + className={b('hit')} + style={{ + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + zIndex: 10 * (path.length + (dropZone ? 0 : 1)), + }} + onClick={ + dropZone + ? undefined + : (e) => { + e.stopPropagation(); + handleClick(path); + } + } + onKeyDown={dropZone ? undefined : (e) => handleKeyDown(e, path)} + onMouseEnter={() => handleMouseEnter(path)} + onMouseLeave={() => handleMouseLeave(path)} + onMouseMove={(e) => handleMouseMove(e)} + onMouseUp={() => handleMouseUp(path)} + /> + ); + })} + {selectedRect ? ( + <div + className={b('border')} + style={{ + top: selectedRect.top, + left: selectedRect.left, + width: selectedRect.width, + height: selectedRect.height, + }} + > + <div className={b('actions')}> + <div className={b('actions-box', {reorder: true})}> + <Button view="flat" size={'m'} onClick={handleMoveUp}> + <Icon className={b('reorder-icon')} data={ChevronUp} size={18} /> + </Button> + </div> + <div className={b('actions-box', {main: true})}> + <Button + className={b('action-button')} + size={'m'} + view={'action'} + onClick={() => selectedBlock && duplicateBlock(selectedBlock)} + > + <Icon data={Copy} size={18} /> + </Button> + <Button + className={b('action-button')} + size={'m'} + view={'action'} + onClick={() => selectedBlock && deleteBlock(selectedBlock)} + > + <Icon data={TrashBin} size={18} /> + </Button> + </div> + <div className={b('actions-box', {reorder: true})}> + <Button view="flat" size={'m'} onClick={handleMoveDown}> + <Icon className={b('reorder-icon')} data={ChevronDown} size={18} /> + </Button> + </div> + </div> + </div> + ) : null} + {showHoverBorder ? ( + <div + className={b('border', {hover: true})} + style={{ + top: hoveredRect.top, + left: hoveredRect.left, + width: hoveredRect.width, + height: hoveredRect.height, + }} + /> + ) : null} + {manipulateOverlayMode && hoveredRect && insertPosition ? ( + <div + className={b('line', {position: insertPosition})} + style={{ + top: hoveredRect.top, + left: hoveredRect.left, + width: hoveredRect.width, + height: hoveredRect.height, + }} + /> + ) : null} + </div> + ); +}; + +export default Overlay; diff --git a/src/editor-v2/containers/Source/Source.scss b/src/editor-v2/containers/Source/Source.scss new file mode 100644 index 0000000000..14ed100698 --- /dev/null +++ b/src/editor-v2/containers/Source/Source.scss @@ -0,0 +1,22 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}source'; + +#{$block} { + width: 100%; + display: inline-flex; + box-sizing: border-box; + align-items: center; + flex-direction: row; + padding: 16px 12px; + + &__icon { + flex: 0 0 auto; + margin-right: var(--g-spacing-1); + } + + &__text { + flex: 1; + } +} diff --git a/src/editor-v2/containers/Source/Source.tsx b/src/editor-v2/containers/Source/Source.tsx new file mode 100644 index 0000000000..b09ec7040c --- /dev/null +++ b/src/editor-v2/containers/Source/Source.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; + +import {ArrowRotateRight} from '@gravity-ui/icons'; +import {Button, Icon, TextInput} from '@gravity-ui/uikit'; + +import {IframeContext} from '../../context/iframeContext'; +import {useMainEditorStore} from '../../hooks/useMainEditorStore'; +import {editorCn} from '../../utils/cn'; + +import './Source.scss'; + +const b = editorCn('source'); + +const Source = () => { + const {resetInitialize} = useMainEditorStore(); + const {disableUrlField, url, setUrl} = React.useContext(IframeContext); + + const onUpdateUrl = React.useCallback( + (value: string) => { + setUrl(value); + resetInitialize(); + }, + [resetInitialize, setUrl], + ); + + const reloadIframe = () => { + setUrl(''); + setTimeout(() => { + setUrl(url); + }, 0); + }; + + return ( + <div className={b()}> + <Button className={b('icon')} view="flat" size="m" onClick={reloadIframe}> + <Icon data={ArrowRotateRight} size={18} /> + </Button> + <TextInput + disabled={disableUrlField} + className={b('text')} + size="m" + value={url} + onUpdate={onUpdateUrl} + /> + </div> + ); +}; + +export default Source; diff --git a/src/editor-v2/containers/SourceCode/SourceCode.scss b/src/editor-v2/containers/SourceCode/SourceCode.scss new file mode 100644 index 0000000000..ce0d7aaffe --- /dev/null +++ b/src/editor-v2/containers/SourceCode/SourceCode.scss @@ -0,0 +1,58 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}source-code'; + +#{$block} { + height: 100%; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 12px; + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + } + + &__title { + font-weight: 500; + font-size: 13px; + line-height: 18px; + color: var(--g-color-text-secondary); + } + + &__code { + @include custom-scrollbar(); + white-space: pre; + @include text-code-inline-1; + padding: 6px 8px; + overflow: auto; + border: 1px solid var(--g-color-line-generic); + border-radius: 6px; + position: relative; + flex: 1; + } + + &__content { + height: 100%; + margin: 0; + padding: 0; + background: transparent; + border: none; + outline: none; + resize: none; + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: inherit; + } + + &__controls { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; + } +} diff --git a/src/editor-v2/containers/SourceCode/SourceCode.tsx b/src/editor-v2/containers/SourceCode/SourceCode.tsx new file mode 100644 index 0000000000..609bac60b2 --- /dev/null +++ b/src/editor-v2/containers/SourceCode/SourceCode.tsx @@ -0,0 +1,125 @@ +import * as React from 'react'; + +import {Pencil} from '@gravity-ui/icons'; +import {Button, ClipboardButton, Icon, SegmentedRadioGroup} from '@gravity-ui/uikit'; +import yaml from 'js-yaml'; +import _ from 'lodash'; + +import {PageContent} from '../../../models'; +import {MessageCard} from '../../components/MessageCard/MessageCard'; +import {MESSAGES} from '../../constants/messages'; +import {useMainEditorStore} from '../../hooks/useMainEditorStore'; +import {generateChildrenPathFromArray} from '../../utils'; +import {editorCn} from '../../utils/cn'; + +import {UpdateModal} from './UpdateModal/UpdateModal'; + +import './SourceCode.scss'; + +const b = editorCn('source-code'); + +interface SourceCodeProps { + className?: string; + showSelectedBlockOnly?: boolean; +} + +const formatOptions = [ + {value: 'yaml', content: 'YAML'}, + {value: 'json', content: 'JSON'}, +]; + +const SourceCode = ({className, showSelectedBlockOnly = false}: SourceCodeProps) => { + const {content, setContent, selectedBlock} = useMainEditorStore(); + const [isOpen, setIsOpen] = React.useState(false); + const [format, setFormat] = React.useState<'yaml' | 'json'>('yaml'); + + const handleUpdate = (tempConfig: string) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let object: any; + + try { + if (tempConfig.trim().startsWith('{') && tempConfig.trim().endsWith('}')) { + object = JSON.parse(tempConfig); + } else { + object = yaml.load(tempConfig); + } + } catch { + // eslint-disable-next-line no-console + console.error('JSON.parse failed'); + return; + } + + if (showSelectedBlockOnly && selectedBlock) { + // Update only the selected block + const currentBlockPath = generateChildrenPathFromArray(selectedBlock); + const newContent = _.cloneDeep(content); + _.set(newContent.blocks, currentBlockPath, object); + setContent(newContent); + } else if (object) { + // Update the entire content + setContent(object as PageContent); + } + + setIsOpen(false); + }; + + const textContent = React.useMemo(() => { + if (showSelectedBlockOnly && selectedBlock) { + const currentBlockPath = generateChildrenPathFromArray(selectedBlock); + const currentConfig = _.get(content.blocks, currentBlockPath || ''); + + if (currentConfig) { + return format === 'yaml' + ? yaml.dump(currentConfig) + : JSON.stringify(currentConfig, null, 2); + } + + return 'No block selected'; + } + + return format === 'yaml' ? yaml.dump(content) : JSON.stringify(content, null, 2); + }, [format, content, showSelectedBlockOnly, selectedBlock]); + + if (!selectedBlock && showSelectedBlockOnly) { + return ( + <div className={b(null, className)}> + <MessageCard + title={MESSAGES.NO_BLOCK_SELECTED.title} + description={MESSAGES.NO_BLOCK_SELECTED.description} + theme="info" + /> + </div> + ); + } + + return ( + <div className={b(null, className)}> + <div className={b('header')}> + <SegmentedRadioGroup + size="m" + value={format} + options={formatOptions} + onUpdate={(value: string) => setFormat(value as 'yaml' | 'json')} + /> + <div className={b('controls')}> + <Button view="outlined" size="m" onClick={() => setIsOpen(true)}> + <Icon size={14} data={Pencil} /> + Edit + </Button> + <ClipboardButton view="outlined" size="m" text={textContent} /> + </div> + </div> + <div className={b('code')}> + <div className={b('content')}>{textContent}</div> + </div> + <UpdateModal + onApply={handleUpdate} + onClose={() => setIsOpen(false)} + isOpen={isOpen} + initialConfig={textContent} + /> + </div> + ); +}; + +export default SourceCode; diff --git a/src/editor-v2/containers/SourceCode/UpdateModal/UpdateModal.scss b/src/editor-v2/containers/SourceCode/UpdateModal/UpdateModal.scss new file mode 100644 index 0000000000..995d31e7a7 --- /dev/null +++ b/src/editor-v2/containers/SourceCode/UpdateModal/UpdateModal.scss @@ -0,0 +1,14 @@ +@import '../../../styles/variables.scss'; +@import '../../../styles/mixins.scss'; + +$block: '.#{$ns}source-code-update-modal'; + +#{$block} { + &__alert { + margin-bottom: 8px; + } + + &__textarea textarea.g-text-area__control { + @include text-code-inline-1; + } +} diff --git a/src/editor-v2/containers/SourceCode/UpdateModal/UpdateModal.tsx b/src/editor-v2/containers/SourceCode/UpdateModal/UpdateModal.tsx new file mode 100644 index 0000000000..d5d42fdf06 --- /dev/null +++ b/src/editor-v2/containers/SourceCode/UpdateModal/UpdateModal.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; + +import {Alert, Dialog, TextArea} from '@gravity-ui/uikit'; + +import {editorCn} from '../../../utils/cn'; + +import './UpdateModal.scss'; + +const b = editorCn('source-code-update-modal'); + +interface UpdateModalProps { + initialConfig?: string; + onClose(): void; + onApply(tempConfig?: string): void; + isOpen: boolean; +} + +export const UpdateModal = ({onClose, onApply, isOpen, initialConfig}: UpdateModalProps) => { + const [tempConfig, setTempConfig] = React.useState(initialConfig || ''); + + React.useEffect(() => { + if (isOpen && initialConfig) { + setTempConfig(initialConfig); + } + }, [isOpen, initialConfig]); + + const handleApply = () => { + onApply(tempConfig); + }; + return ( + <Dialog onClose={onClose} open={isOpen} size={'l'} className={b()}> + <Dialog.Header caption="New configuration" /> + <Dialog.Body> + <Alert + theme={'info'} + title={'You can use YAML or JSON'} + message={'The editor will automatically understand which format is needed.'} + className={b('alert')} + ></Alert> + <TextArea + value={tempConfig} + onUpdate={setTempConfig} + rows={25} + className={b('textarea')} + /> + </Dialog.Body> + <Dialog.Footer + showError={false} + preset={'default'} + textButtonApply={'Apply'} + textButtonCancel={'Cancel'} + onClickButtonApply={handleApply} + onClickButtonCancel={onClose} + /> + </Dialog> + ); +}; diff --git a/src/editor-v2/containers/Tree/Tree.scss b/src/editor-v2/containers/Tree/Tree.scss new file mode 100644 index 0000000000..3db09571c7 --- /dev/null +++ b/src/editor-v2/containers/Tree/Tree.scss @@ -0,0 +1,27 @@ +@import '../../styles/variables.scss'; +@import '../../../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}tree'; + +#{$block} { + padding: $indentXXS; + position: relative; + + &__head { + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: space-between; + } + + &__list { + display: flex; + flex-direction: column; + gap: $indentXXXS; + } + + &__dnd-overlay { + opacity: 1; + } +} diff --git a/src/editor-v2/containers/Tree/Tree.tsx b/src/editor-v2/containers/Tree/Tree.tsx new file mode 100644 index 0000000000..2abb69ef5f --- /dev/null +++ b/src/editor-v2/containers/Tree/Tree.tsx @@ -0,0 +1,271 @@ +/* eslint-disable no-negated-condition */ +import * as React from 'react'; + +import type {DragDropEvents} from '@dnd-kit/abstract'; +import type {DragDropManager, Draggable, Droppable} from '@dnd-kit/dom'; +import {isKeyboardEvent} from '@dnd-kit/dom/utilities'; +import {move} from '@dnd-kit/helpers'; +import {DragDropProvider, DragOverlay} from '@dnd-kit/react'; +import {TrashBin} from '@gravity-ui/icons'; +import {Button, Icon} from '@gravity-ui/uikit'; +import _ from 'lodash'; + +import type {ClassNameProps} from '../../../models'; +import {useMainEditorStore} from '../../hooks'; +import {generateChildrenPathFromArray, getItemTitle} from '../../utils'; +import {editorCn} from '../../utils/cn'; + +import {TreeItem} from './TreeItem'; +import {TreeItemOverlay} from './TreeItemOverlay'; +import type {FlattenedTreeItem} from './types'; +import { + blocksToTreeItems, + getBlockTypeLabel, + idToBlockPath, + treeItemsToBlocks, +} from './utils/blocksBridge'; +import {buildTree, flattenTree, getDescendants, getDragDepth} from './utils/common'; +import {sanitizeProjectionForChildrenCapability} from './utils/treeDragGuards'; + +import './Tree.scss'; + +type TreeDnd = DragDropEvents<Draggable, Droppable, DragDropManager>; +type TreeDragStartEvent = Parameters<TreeDnd['dragstart']>[0]; +type TreeDragOverEvent = Parameters<TreeDnd['dragover']>[0]; +type TreeDragMoveEvent = Parameters<TreeDnd['dragmove']>[0]; +type TreeDragEndEvent = Parameters<TreeDnd['dragend']>[0]; + +const b = editorCn('tree'); + +const INDENTATION = 24; + +interface TreeProps extends ClassNameProps {} + +const Tree = ({className}: TreeProps) => { + const { + content, + setContent, + selectedBlock, + setSelectedBlock, + resetBlocks, + duplicateBlock, + deleteBlock, + } = useMainEditorStore(); + + const [flattenedItems, setFlattenedItems] = React.useState(() => + flattenTree(blocksToTreeItems(content.blocks)), + ); + + const initialDepth = React.useRef(0); + const sourceChildren = React.useRef<FlattenedTreeItem[]>([]); + + React.useEffect(() => { + setFlattenedItems(flattenTree(blocksToTreeItems(content.blocks))); + }, [content.blocks]); + + const selectedBlockPathStr = React.useMemo( + () => generateChildrenPathFromArray(selectedBlock || []), + [selectedBlock], + ); + + const handleDragStart = React.useCallback( + (event: TreeDragStartEvent) => { + const {source} = event.operation; + if (!source) { + return; + } + + const row = flattenedItems.find(({id}) => id === source.id); + if (!row) { + return; + } + + initialDepth.current = row.depth; + + setFlattenedItems((flatItems) => { + sourceChildren.current = []; + const descendants = getDescendants(flatItems, source.id); + return flatItems.filter((item) => { + if (descendants.has(item.id)) { + sourceChildren.current = [...sourceChildren.current, item]; + return false; + } + return true; + }); + }); + }, + [flattenedItems], + ); + + const handleDragOver = React.useCallback( + (event: TreeDragOverEvent, manager: DragDropManager) => { + event.preventDefault(); + const {source, target} = event.operation; + + if (source && target && source.id !== target.id) { + setFlattenedItems((flat) => { + const offsetLeft = manager.dragOperation.transform.x; + const dragDepth = getDragDepth(offsetLeft, INDENTATION); + const projectedDepth = initialDepth.current + dragDepth; + + const {depth, parentId} = sanitizeProjectionForChildrenCapability( + flat, + target.id, + projectedDepth, + ); + + const sortedItems = move(flat, event); + return sortedItems.map((item) => + item.id === source.id ? {...item, depth, parentId} : item, + ); + }); + } + }, + [], + ); + + const handleDragMove = React.useCallback( + (event: TreeDragMoveEvent, manager: DragDropManager) => { + if (event.defaultPrevented) { + return; + } + + const {source, target} = event.operation; + + if (source && target) { + const keyboard = isKeyboardEvent(event.operation.activatorEvent); + const currentDepth = source.data?.depth ?? 0; + let keyboardDepth: number | undefined; + + if (keyboard) { + const isHorizontal = event.by?.x !== 0 && event.by?.y === 0; + + if (isHorizontal) { + event.preventDefault(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + keyboardDepth = currentDepth + Math.sign(event.by!.x); + } + } + + const offsetLeft = manager.dragOperation.transform.x; + const dragDepth = getDragDepth(offsetLeft, INDENTATION); + + const projectedDepth = keyboardDepth ?? initialDepth.current + dragDepth; + + const {depth, parentId} = sanitizeProjectionForChildrenCapability( + flattenedItems, + source.id, + projectedDepth, + ); + + if (keyboard) { + if (currentDepth !== depth) { + const offset = INDENTATION * (depth - currentDepth); + + manager.actions.move({ + by: {x: offset, y: 0}, + propagate: false, + }); + } + } + + if (source.data?.depth !== depth || source.data?.parentId !== parentId) { + setFlattenedItems((flat) => + flat.map((item) => + item.id === source.id ? {...item, depth, parentId} : item, + ), + ); + } + } + }, + [flattenedItems], + ); + + const handleDragEnd = React.useCallback( + (event: TreeDragEndEvent) => { + if (event.canceled) { + setFlattenedItems(flattenTree(blocksToTreeItems(content.blocks))); + return; + } + + const merged = [...flattenedItems, ...sourceChildren.current]; + + const updatedTree = buildTree(merged); + const nextFlat = flattenTree(updatedTree); + const newBlocks = treeItemsToBlocks(updatedTree); + + setFlattenedItems(nextFlat); + setContent({...content, blocks: newBlocks}); + + const draggedId = event.operation.source?.id; + const pathToDragged = + draggedId !== null ? idToBlockPath(nextFlat, String(draggedId)) : []; + setSelectedBlock(pathToDragged.length ? pathToDragged : null); + }, + [setContent, setSelectedBlock, content, flattenedItems], + ); + + return ( + <div className={b(null, className)}> + <div className={b('head')}> + <Button view="outlined-danger" onClick={() => resetBlocks()}> + <Icon data={TrashBin} /> + Clear all + </Button> + </div> + <DragDropProvider + onDragStart={handleDragStart} + onDragOver={handleDragOver} + onDragMove={handleDragMove} + onDragEnd={handleDragEnd} + > + <div className={b('list')}> + {flattenedItems.map((row, index) => { + const path = idToBlockPath(flattenedItems, row.id); + const pathKey = generateChildrenPathFromArray(path); + const blockType = getBlockTypeLabel(row.block); + + return ( + <TreeItem + key={row.id} + item={row} + sortableIndex={index} + indentLeft={row.depth * INDENTATION} + path={path} + selected={selectedBlockPathStr === pathKey} + type={blockType} + treeTitle={getItemTitle(row.block)} + onCopy={duplicateBlock} + onDelete={deleteBlock} + onSelect={setSelectedBlock} + /> + ); + })} + </div> + <DragOverlay className={b('dnd-overlay')} dropAnimation={null}> + {(source) => { + if (!source) { + return null; + } + const sourceId = String(source.id); + const rowNow = flattenedItems.find(({id}) => String(id) === sourceId); + const overlayBlock = rowNow?.block; + if (!overlayBlock) { + return null; + } + const indentLeft = (rowNow?.depth ?? 0) * INDENTATION; + return ( + <TreeItemOverlay + block={overlayBlock} + count={sourceChildren.current.length} + indentLeft={indentLeft} + /> + ); + }} + </DragOverlay> + </DragDropProvider> + </div> + ); +}; + +export default Tree; diff --git a/src/editor-v2/containers/Tree/TreeItem.scss b/src/editor-v2/containers/Tree/TreeItem.scss new file mode 100644 index 0000000000..c4a1b73b00 --- /dev/null +++ b/src/editor-v2/containers/Tree/TreeItem.scss @@ -0,0 +1,92 @@ +@import '../../styles/variables.scss'; +@import '../../../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}tree-item'; + +#{$block} { + padding: $indentXXXS; + margin-bottom: 0; + cursor: pointer; + + &_selected.g-card:not(#{$block}_drag-over.g-card) { + border: 1.5px var(--g-color-line-brand) solid; + } + + &_dragging.g-card { + opacity: 0.85; + } + + &_drag-over.g-card { + outline: 1.5px dashed var(--g-color-line-brand); + outline-offset: -1px; + } + + &_overlay.g-card { + position: relative; + display: flex; + align-items: center; + min-width: 220px; + max-width: min(100vw - 32px, 420px); + cursor: grabbing; + background-color: var(--g-color-base-background); + opacity: 1; + box-shadow: 0 4px 12px var(--g-color-private-black-150); + } + + &__main { + display: flex; + flex-direction: row; + align-items: center; + flex: 1; + min-width: 0; + min-height: 20px; + + &:hover { + #{$block}__buttons { + display: flex; + } + } + } + + &__text { + flex: 1 1 auto; + min-width: 1px; + } + + &__buttons { + flex: 0 0 auto; + flex-direction: row; + gap: 4px; + display: none; + } + + &__type { + color: var(--g-color-text-secondary); + } + + &__title { + @include title-styles(); + + & * { + @include title-styles(); + } + } + + &__badge { + position: absolute; + top: -8px; + right: -8px; + display: flex; + align-items: center; + justify-content: center; + min-width: 22px; + height: 22px; + padding: 0 6px; + border-radius: 50%; + background-color: var(--g-color-base-brand); + font-size: 12px; + font-weight: 500; + color: var(--g-color-text-brand-contrast); + } +} diff --git a/src/editor-v2/containers/Tree/TreeItem.tsx b/src/editor-v2/containers/Tree/TreeItem.tsx new file mode 100644 index 0000000000..245da0cdc9 --- /dev/null +++ b/src/editor-v2/containers/Tree/TreeItem.tsx @@ -0,0 +1,130 @@ +import * as React from 'react'; + +import {useSortable} from '@dnd-kit/react/sortable'; +import {Copy, TrashBin} from '@gravity-ui/icons'; +import {Button, Card, Icon} from '@gravity-ui/uikit'; + +import {HTML} from '../../../components'; +import {editorCn} from '../../utils/cn'; + +import type {FlattenedTreeItem} from './types'; + +import './TreeItem.scss'; + +const b = editorCn('tree-item'); + +const sortableConfig = { + alignment: { + x: 'start' as const, + y: 'center' as const, + }, + transition: null, +} as const; + +export interface TreeItemProps { + item: FlattenedTreeItem; + sortableIndex: number; + indentLeft: number; + selected: boolean; + type: string; + treeTitle?: string; + path: number[]; + onCopy(path: number[]): void; + onDelete(path: number[]): void; + onSelect(path: number[]): void; +} + +export function TreeItem({ + item, + sortableIndex, + indentLeft, + selected, + type, + treeTitle, + path, + onCopy, + onDelete, + onSelect, +}: TreeItemProps) { + const {depth, parentId, id} = item; + + const {ref, isDragSource, isDropTarget} = useSortable({ + ...sortableConfig, + id, + index: sortableIndex, + data: { + depth, + parentId, + }, + }); + + const [mouseDownPos, setMouseDownPos] = React.useState<{x: number; y: number} | null>(null); + const itemRef = React.useRef<HTMLDivElement>(null); + + React.useLayoutEffect(() => { + if (selected && itemRef.current) { + itemRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }); + } + }, [selected]); + + const setNodeRef = React.useCallback( + (node: Element | null) => { + ref(node); + (itemRef as React.MutableRefObject<HTMLDivElement | null>).current = + node as HTMLDivElement | null; + }, + [ref], + ); + + const handleMouseDown = React.useCallback((e: React.MouseEvent<HTMLDivElement>) => { + setMouseDownPos({x: e.clientX, y: e.clientY}); + }, []); + + const handleMouseUp = React.useCallback( + (e: React.MouseEvent<HTMLDivElement>) => { + if (mouseDownPos) { + const dx = Math.abs(e.clientX - mouseDownPos.x); + const dy = Math.abs(e.clientY - mouseDownPos.y); + + if (dx < 5 && dy < 5) { + e.stopPropagation(); + onSelect(path); + } + } + setMouseDownPos(null); + }, + [mouseDownPos, onSelect, path], + ); + + return ( + <Card + ref={setNodeRef} + className={b({ + selected, + dragging: isDragSource, + 'drag-over': isDropTarget, + })} + style={{marginLeft: indentLeft}} + onMouseDown={handleMouseDown as unknown as React.MouseEventHandler<'div'>} + onMouseUp={handleMouseUp as unknown as React.MouseEventHandler<'div'>} + > + <div className={b('main')}> + <div className={b('text')}> + <div className={b('type')}>{type}</div> + <HTML className={b('title')}>{treeTitle}</HTML> + </div> + <div className={b('buttons')}> + <Button view="flat" size="xs" onClick={() => onCopy(path)}> + <Icon size={12} data={Copy} /> + </Button> + <Button view="flat" size="xs" onClick={() => onDelete(path)}> + <Icon size={12} data={TrashBin} /> + </Button> + </div> + </div> + </Card> + ); +} diff --git a/src/editor-v2/containers/Tree/TreeItemOverlay.tsx b/src/editor-v2/containers/Tree/TreeItemOverlay.tsx new file mode 100644 index 0000000000..080beb145a --- /dev/null +++ b/src/editor-v2/containers/Tree/TreeItemOverlay.tsx @@ -0,0 +1,35 @@ +import {Card} from '@gravity-ui/uikit'; + +import {HTML} from '../../../components'; +import type {ConstructorBlock} from '../../../models'; +import {getItemTitle} from '../../utils'; +import {editorCn} from '../../utils/cn'; + +import {getBlockTypeLabel} from './utils/blocksBridge'; + +import './TreeItem.scss'; + +const b = editorCn('tree-item'); + +export interface TreeItemOverlayProps { + block: ConstructorBlock; + count: number; + indentLeft: number; +} + +export function TreeItemOverlay({block, count, indentLeft}: TreeItemOverlayProps) { + const typeLabel = getBlockTypeLabel(block); + const treeTitle = getItemTitle(block); + + return ( + <Card className={b({overlay: true})} style={{marginLeft: indentLeft}} data-overlay> + <div className={b('main')}> + <div className={b('text')}> + <div className={b('type')}>{typeLabel}</div> + <HTML className={b('title')}>{treeTitle}</HTML> + </div> + </div> + {count > 0 ? <span className={b('badge')}>{count}</span> : null} + </Card> + ); +} diff --git a/src/editor-v2/containers/Tree/index.ts b/src/editor-v2/containers/Tree/index.ts new file mode 100644 index 0000000000..f53b46b1b0 --- /dev/null +++ b/src/editor-v2/containers/Tree/index.ts @@ -0,0 +1 @@ +export {default} from './Tree'; diff --git a/src/editor-v2/containers/Tree/types.ts b/src/editor-v2/containers/Tree/types.ts new file mode 100644 index 0000000000..8aa8f78f62 --- /dev/null +++ b/src/editor-v2/containers/Tree/types.ts @@ -0,0 +1,13 @@ +import type {ConstructorBlock} from '../../../models'; + +export interface TreeItemData { + id: string; + block: ConstructorBlock; + children: TreeItemData[]; +} + +export interface FlattenedTreeItem extends TreeItemData { + parentId: string | null; + depth: number; + index: number; +} diff --git a/src/editor-v2/containers/Tree/utils/blocksBridge.ts b/src/editor-v2/containers/Tree/utils/blocksBridge.ts new file mode 100644 index 0000000000..7e091608b7 --- /dev/null +++ b/src/editor-v2/containers/Tree/utils/blocksBridge.ts @@ -0,0 +1,77 @@ +/* eslint-disable no-negated-condition */ +import _ from 'lodash'; + +import type {ConstructorBlock} from '../../../../models'; +import type {FlattenedTreeItem, TreeItemData} from '../types'; + +export function blockHasChildrenProp(block: ConstructorBlock): boolean { + return Boolean(block && typeof block === 'object' && 'children' in block); +} + +export function getBlockTypeLabel(block: ConstructorBlock): string { + if (block && typeof block === 'object' && 'type' in block) { + return block.type; + } + return ''; +} + +export function getBlockChildren(block: ConstructorBlock): ConstructorBlock[] | undefined { + if (block && typeof block === 'object' && 'children' in block) { + if (Array.isArray(block.children)) { + return block.children as ConstructorBlock[]; + } + } + return undefined; +} + +export function blocksToTreeItems(blocks: ConstructorBlock[], pathPrefix = ''): TreeItemData[] { + return blocks.map((block, index) => { + const id = pathPrefix ? `${pathPrefix}/${index}` : String(index); + const children = getBlockChildren(block); + return { + id, + block, + children: children ? blocksToTreeItems(children, id) : [], + }; + }); +} + +export function treeItemsToBlocks(items: TreeItemData[]): ConstructorBlock[] { + return items.map((item) => { + const next = _.cloneDeep(item.block); + const nested = treeItemsToBlocks(item.children); + if (nested.length > 0) { + (next as {children?: ConstructorBlock[]}).children = nested as never; + } else if (getBlockChildren(item.block) !== undefined) { + (next as {children?: ConstructorBlock[]}).children = [] as never; + } + return next; + }); +} + +// Путь индексов от корня; при цикле или разрыве связи — пустой массив. +export function idToBlockPath(flat: FlattenedTreeItem[], id: string): number[] { + const byId = new Map(flat.map((x) => [String(x.id), x])); + const path: number[] = []; + const seen = new Set<string>(); + let currentId: string | null = String(id); + const maxSteps = flat.length + 1; + + for (let step = 0; step < maxSteps && currentId !== null; step++) { + if (seen.has(currentId)) { + return []; + } + seen.add(currentId); + + const row = byId.get(currentId); + if (!row) { + return []; + } + path.unshift(row.index); + + const p = row.parentId; + currentId = p === null ? null : String(p); + } + + return currentId !== null ? [] : path; +} diff --git a/src/editor-v2/containers/Tree/utils/common.ts b/src/editor-v2/containers/Tree/utils/common.ts new file mode 100644 index 0000000000..5766149719 --- /dev/null +++ b/src/editor-v2/containers/Tree/utils/common.ts @@ -0,0 +1,113 @@ +import type {UniqueIdentifier} from '@dnd-kit/abstract'; + +import type {ConstructorBlock} from '../../../../models'; +import type {FlattenedTreeItem, TreeItemData} from '../types'; + +export function flattenTree( + items: TreeItemData[], + parentId: string | null = null, + depth = 0, +): FlattenedTreeItem[] { + return items.reduce<FlattenedTreeItem[]>((acc, item, index) => { + return [ + ...acc, + {...item, parentId, depth, index}, + ...flattenTree(item.children, item.id, depth + 1), + ]; + }, []); +} + +export function buildTree(flattenedItems: FlattenedTreeItem[]): TreeItemData[] { + const root: TreeItemData = {id: 'root', block: {} as ConstructorBlock, children: []}; + const nodes: Record<string, TreeItemData> = {[root.id]: root}; + + const items = flattenedItems.map((item) => { + return {...item, children: [] as TreeItemData[]}; + }); + + for (const item of items) { + const {id} = item; + const parentId = item.parentId ?? root.id; + const parent = nodes[parentId] ?? items.find((x) => x.id === parentId); + + if (!parent) { + continue; + } + + const node: TreeItemData = {...item, children: []}; + nodes[id] = node; + parent.children.push(node); + } + + return root.children; +} + +export function getDragDepth(offset: number, indentationWidth: number) { + return Math.round(offset / indentationWidth); +} + +export function getProjection( + items: FlattenedTreeItem[], + targetId: UniqueIdentifier, + projectedDepth: number, +) { + const targetItemIndex = items.findIndex(({id}) => id === targetId); + const previousItem = items[targetItemIndex - 1]; + const targetItem = items[targetItemIndex]; + const nextItem = items[targetItemIndex + 1]; + const maxDepth = getMaxDepth(targetItem, previousItem); + const minDepth = getMinDepth(nextItem); + let depth = projectedDepth; + + if (projectedDepth >= maxDepth) { + depth = maxDepth; + } else if (projectedDepth < minDepth) { + depth = minDepth; + } + + return {depth, maxDepth, minDepth, parentId: getParentId()}; + + function getParentId(): string | null { + if (depth === 0 || !previousItem) { + return null; + } + + if (depth === previousItem.depth) { + return previousItem.parentId; + } + + if (depth > previousItem.depth) { + return previousItem.id; + } + + const newParent = items + .slice(0, targetItemIndex) + .reverse() + .find((item) => item.depth === depth)?.parentId; + + return newParent ?? null; + } +} + +function getMaxDepth(targetItem: FlattenedTreeItem, previousItem: FlattenedTreeItem | undefined) { + if (!previousItem) { + return 0; + } + + return Math.min(targetItem.depth + 1, previousItem.depth + 1); +} + +function getMinDepth(nextItem: FlattenedTreeItem | undefined) { + return nextItem ? nextItem.depth : 0; +} + +export function getDescendants( + items: FlattenedTreeItem[], + parentId: UniqueIdentifier, +): Set<string> { + const directChildren = items.filter((item) => item.parentId === parentId); + + return directChildren.reduce((descendants, child) => { + return new Set([...descendants, child.id, ...getDescendants(items, child.id)]); + }, new Set<string>()); +} diff --git a/src/editor-v2/containers/Tree/utils/treeDragGuards.ts b/src/editor-v2/containers/Tree/utils/treeDragGuards.ts new file mode 100644 index 0000000000..01fad87f54 --- /dev/null +++ b/src/editor-v2/containers/Tree/utils/treeDragGuards.ts @@ -0,0 +1,34 @@ +import type {UniqueIdentifier} from '@dnd-kit/abstract'; + +import type {FlattenedTreeItem} from '../types'; + +import {blockHasChildrenProp} from './blocksBridge'; +import {getProjection} from './common'; + +function parentRowSupportsChildren(flat: FlattenedTreeItem[], parentId: string | null): boolean { + if (parentId === null) { + return true; + } + const row = flat.find((x) => String(x.id) === String(parentId)); + return Boolean(row && blockHasChildrenProp(row.block)); +} + +// Не даём сделать элемент дочерним у блока без пропа `children` +// рекурсивно уменьшаем глубину, пока не найдём допустимого родителя. +export function sanitizeProjectionForChildrenCapability( + flat: FlattenedTreeItem[], + targetId: UniqueIdentifier, + projectedDepth: number, +): {depth: number; parentId: string | null} { + const {depth, parentId} = getProjection(flat, targetId, projectedDepth); + + if (parentRowSupportsChildren(flat, parentId)) { + return {depth, parentId}; + } + + if (projectedDepth <= 0) { + return getProjection(flat, targetId, 0); + } + + return sanitizeProjectionForChildrenCapability(flat, targetId, projectedDepth - 1); +} diff --git a/src/editor-v2/containers/ViewSwitches/ViewSwitches.scss b/src/editor-v2/containers/ViewSwitches/ViewSwitches.scss new file mode 100644 index 0000000000..69616b5b50 --- /dev/null +++ b/src/editor-v2/containers/ViewSwitches/ViewSwitches.scss @@ -0,0 +1,25 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns}view-switches'; + +#{$block} { + padding: 12px; + display: inline-flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + + &__zoom { + display: flex; + gap: 4px; + + &-select { + min-width: 80px; + } + } + + &__history-buttons { + width: max-content; + } +} diff --git a/src/editor-v2/containers/ViewSwitches/ViewSwitches.tsx b/src/editor-v2/containers/ViewSwitches/ViewSwitches.tsx new file mode 100644 index 0000000000..684644de71 --- /dev/null +++ b/src/editor-v2/containers/ViewSwitches/ViewSwitches.tsx @@ -0,0 +1,163 @@ +import * as React from 'react'; + +import { + ArrowUturnCcwLeft, + ArrowUturnCwRight, + Display, + Minus, + Plus, + Smartphone, + SquareDashed, +} from '@gravity-ui/icons'; +import {Button, Icon, SegmentedRadioGroup, Select} from '@gravity-ui/uikit'; + +import {ZOOM_STEPS} from '../../constants'; +import {useMainEditorStore} from '../../hooks/useMainEditorStore'; +import {editorCn} from '../../utils/cn'; + +import './ViewSwitches.scss'; + +const b = editorCn('view-switches'); + +/** + * Device option type definition + */ +interface DeviceOption { + /** React node to display as the option label */ + label: React.ReactNode; + /** Device width value (e.g., '100%', '768px') */ + value: string; + /** Descriptive name for accessibility */ + ariaLabel: string; +} + +/** + * Available device viewport options + * - Desktop: 100% width + * - Tablet: 768px width + * - Mobile: 375px width + */ +const DEVICE_OPTIONS: DeviceOption[] = [ + { + label: <Icon data={Display} />, + value: '100%', + ariaLabel: 'Desktop view', + }, + { + label: <Icon data={Smartphone} />, + value: '768px', + ariaLabel: 'Tablet view', + }, + { + label: <Icon width={14} data={Smartphone} />, + value: '375px', + ariaLabel: 'Mobile view', + }, +]; + +const ViewSwitches: React.FC = () => { + const { + zoom, + setZoom, + decreaseZoom, + increaseZoom, + deviceWidth, + setDeviceWidth, + togglePreviewMode, + } = useMainEditorStore(); + + const store = useMainEditorStore(); + const {undo, redo, historyFuture, historyPast} = store; + + const canUndo = historyPast.length > 0; + const canRedo = historyFuture.length > 0; + + // Memoize zoom options to prevent unnecessary recalculations + const zoomOptions = React.useMemo( + () => + ZOOM_STEPS.map((step) => ({ + value: String(step), + content: `${step}%`, + })), + [], + ); + + // Memoize current zoom value for Select component + const currentZoomValue = React.useMemo(() => [String(zoom)], [zoom]); + + // Create stable callback for zoom updates + const handleZoomUpdate = React.useCallback( + (value: string | string[]) => { + const newZoom = Number(Array.isArray(value) ? value[0] : value); + if (!isNaN(newZoom) && ZOOM_STEPS.includes(newZoom)) { + setZoom(newZoom); + } + }, + [setZoom], + ); + + return ( + <div className={b()} role="toolbar" aria-label="View controls"> + <SegmentedRadioGroup + value={deviceWidth} + onUpdate={setDeviceWidth} + aria-label="Device viewport selector" + > + {DEVICE_OPTIONS.map(({value, label, ariaLabel}) => ( + <SegmentedRadioGroup.Option key={value} value={value} aria-label={ariaLabel}> + {label} + </SegmentedRadioGroup.Option> + ))} + </SegmentedRadioGroup> + + <Button + view="flat" + onClick={togglePreviewMode} + aria-label="Switch to preview mode" + title="Switch to preview mode" + > + <Icon data={SquareDashed} /> + </Button> + + <div className={b('zoom')} role="group" aria-label="Zoom controls"> + <Button + view="flat" + onClick={decreaseZoom} + aria-label="Decrease zoom" + disabled={zoom <= Math.min(...ZOOM_STEPS)} + > + <Icon data={Minus} /> + </Button> + + <Select + className={b('zoom-select')} + multiple={false} + value={currentZoomValue} + options={zoomOptions} + onUpdate={handleZoomUpdate} + aria-label="Select zoom level" + width="max" + /> + + <Button + view="flat" + onClick={increaseZoom} + aria-label="Increase zoom" + disabled={zoom >= Math.max(...ZOOM_STEPS)} + > + <Icon data={Plus} /> + </Button> + </div> + <div className={b('history-buttons')}> + <Button view="flat" aria-label="Undo" onClick={undo} disabled={!canUndo}> + <Icon data={ArrowUturnCcwLeft} /> + </Button> + <Button view="flat" aria-label="Redo" onClick={redo} disabled={!canRedo}> + <Icon data={ArrowUturnCwRight} /> + </Button> + </div> + </div> + ); +}; + +export default React.memo(ViewSwitches); diff --git a/src/editor-v2/containers/__stories__/Editor.stories.tsx b/src/editor-v2/containers/__stories__/Editor.stories.tsx new file mode 100644 index 0000000000..f06c3746be --- /dev/null +++ b/src/editor-v2/containers/__stories__/Editor.stories.tsx @@ -0,0 +1,20 @@ +import {Meta, StoryFn} from '@storybook/react'; + +import {Editor} from '../Editor/Editor'; + +export default { + title: 'Editor/Main 2.0', + component: Editor, +} as Meta; + +const DefaultTemplate: StoryFn = (args) => { + return ( + <div style={{height: '100vh', width: '100vw'}}> + <Editor {...args} initialUrl={'http://localhost:3000'} /> + </div> + ); +}; + +export const Default = DefaultTemplate.bind({}); + +// Default.args = data.default; diff --git a/src/editor-v2/containers/__stories__/utils.ts b/src/editor-v2/containers/__stories__/utils.ts new file mode 100644 index 0000000000..7d9be48821 --- /dev/null +++ b/src/editor-v2/containers/__stories__/utils.ts @@ -0,0 +1,22 @@ +import isEqual from 'lodash/isEqual'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const memoizeLast = (fn: (...args: any[]) => any) => { + let cacheKey: Parameters<typeof fn>; + let cacheResult: ReturnType<typeof fn>; + + return (...params: Parameters<typeof fn>) => { + if (!isEqual(params, cacheKey)) { + try { + const result = fn(...params); + + cacheResult = result; + cacheKey = params; + } catch { + return cacheResult; + } + } + + return cacheResult; + }; +}; diff --git a/src/editor-v2/containers/index.ts b/src/editor-v2/containers/index.ts new file mode 100644 index 0000000000..a0a339ad85 --- /dev/null +++ b/src/editor-v2/containers/index.ts @@ -0,0 +1,8 @@ +export {Editor, EditorProvider, EditorView} from './Editor/Editor'; +export type {EditorProviderProps, EditorViewProps} from './Editor/Editor'; +export {default as BlockConfigForm} from './BlockConfigForm/BlockConfigForm'; +export {default as BlocksList} from './BlocksList/BlocksList'; +export {default as Source} from './Source/Source'; +export {default as SourceCode} from './SourceCode/SourceCode'; +export {default as Tree} from './Tree/Tree'; +export {default as ViewSwitches} from './ViewSwitches/ViewSwitches'; diff --git a/src/editor-v2/context/editorStore/MainEditorStoreContext.tsx b/src/editor-v2/context/editorStore/MainEditorStoreContext.tsx new file mode 100644 index 0000000000..0ab9b64870 --- /dev/null +++ b/src/editor-v2/context/editorStore/MainEditorStoreContext.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; + +import {StoreApi} from 'zustand'; + +import {EditorStore, createEditorStore} from '../../store'; + +export interface MainEditorStoreContextProps { + state: StoreApi<EditorStore>; +} + +export const MainEditorStoreContext = React.createContext<MainEditorStoreContextProps>({ + state: createEditorStore(), +}); diff --git a/src/editor-v2/context/editorStore/MainEditorStoreProvider.tsx b/src/editor-v2/context/editorStore/MainEditorStoreProvider.tsx new file mode 100644 index 0000000000..5047146b59 --- /dev/null +++ b/src/editor-v2/context/editorStore/MainEditorStoreProvider.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; + +import {StoreApi} from 'zustand'; + +import {POST_MESSAGE_SOURCE} from '../../../common/constants'; +import {EditorState} from '../../../common/store'; +import {StoreSyncMessage} from '../../../common/types'; +import {removeFn} from '../../../common/utils'; +import {EditorStore, createEditorStore} from '../../store'; +import {IframeContext} from '../iframeContext'; + +import {MainEditorStoreContext} from './MainEditorStoreContext'; + +interface MainEditorProviderProps extends React.PropsWithChildren {} + +export const MainEditorStoreProvider = ({children}: MainEditorProviderProps) => { + const {iframeElement} = React.useContext(IframeContext); + const storeRef = React.useRef<StoreApi<EditorStore>>(); + + const sendPostMessage = React.useCallback( + (data: EditorState) => { + const message: StoreSyncMessage = { + state: data, + source: POST_MESSAGE_SOURCE, + }; + + if (iframeElement && iframeElement.contentWindow) { + iframeElement.contentWindow.postMessage(message, '*'); + } + }, + [iframeElement], + ); + + if (!storeRef.current) { + storeRef.current = createEditorStore(); + } + + React.useEffect(() => { + storeRef.current?.subscribe((state) => { + const {historyPast: _historyPast, historyFuture: _historyFuture, ...syncable} = state; + sendPostMessage(removeFn({...syncable, historyPast: [], historyFuture: []})); + }); + }, [sendPostMessage]); + + return ( + <MainEditorStoreContext.Provider + value={{ + state: storeRef.current, + }} + > + {children} + </MainEditorStoreContext.Provider> + ); +}; diff --git a/src/editor-v2/context/editorStore/index.ts b/src/editor-v2/context/editorStore/index.ts new file mode 100644 index 0000000000..7804dc9a12 --- /dev/null +++ b/src/editor-v2/context/editorStore/index.ts @@ -0,0 +1,2 @@ +export * from './MainEditorStoreContext'; +export * from './MainEditorStoreProvider'; diff --git a/src/editor-v2/context/iframeContext/IframeContext.tsx b/src/editor-v2/context/iframeContext/IframeContext.tsx new file mode 100644 index 0000000000..c8962762f8 --- /dev/null +++ b/src/editor-v2/context/iframeContext/IframeContext.tsx @@ -0,0 +1,19 @@ +/** + * Context for iframe window + **/ + +import * as React from 'react'; + +export interface IframeContextProps { + iframeElement?: HTMLIFrameElement; + setIframeElement: (element: HTMLIFrameElement) => void; + url: string; + setUrl: (url: string) => void; + disableUrlField?: boolean; +} + +export const IframeContext = React.createContext<IframeContextProps>({ + setIframeElement: () => {}, + setUrl: () => {}, + url: '', +}); diff --git a/src/editor-v2/context/iframeContext/IframeProvider.tsx b/src/editor-v2/context/iframeContext/IframeProvider.tsx new file mode 100644 index 0000000000..499850d216 --- /dev/null +++ b/src/editor-v2/context/iframeContext/IframeProvider.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; + +import {IframeContext} from './IframeContext'; + +interface IframeProviderProps extends React.PropsWithChildren { + initialUrl?: string; + disableUrlField?: boolean; +} + +export const IframeProvider = ({ + children, + initialUrl = '', + disableUrlField, +}: IframeProviderProps) => { + const [iframeElement, setIframeElement] = React.useState<HTMLIFrameElement>(); + const [url, setUrl] = React.useState(initialUrl); + + const setIframeElementFunc = (element: HTMLIFrameElement) => setIframeElement(element); + + return ( + <IframeContext.Provider + value={{ + url, + setUrl, + iframeElement, + setIframeElement: setIframeElementFunc, + disableUrlField, + }} + > + {children} + </IframeContext.Provider> + ); +}; diff --git a/src/editor-v2/context/iframeContext/index.ts b/src/editor-v2/context/iframeContext/index.ts new file mode 100644 index 0000000000..dc2795f0f3 --- /dev/null +++ b/src/editor-v2/context/iframeContext/index.ts @@ -0,0 +1,2 @@ +export * from './IframeContext'; +export * from './IframeProvider'; diff --git a/src/editor-v2/hooks/index.ts b/src/editor-v2/hooks/index.ts new file mode 100644 index 0000000000..4a736c9ad9 --- /dev/null +++ b/src/editor-v2/hooks/index.ts @@ -0,0 +1,3 @@ +export {usePostMessageEvents} from './usePostMessageEvents'; +export {useMainEditorStore} from './useMainEditorStore'; +export {usePCEditorSettings} from './usePCEditorSettings'; diff --git a/src/editor-v2/hooks/useEditorTabs.tsx b/src/editor-v2/hooks/useEditorTabs.tsx new file mode 100644 index 0000000000..534e637639 --- /dev/null +++ b/src/editor-v2/hooks/useEditorTabs.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; + +import Tabs, {TabItemProps} from '../components/Tabs/Tabs'; +import BlockConfigForm from '../containers/BlockConfigForm/BlockConfigForm'; +import BlocksList from '../containers/BlocksList/BlocksList'; +import GlobalConfig from '../containers/GlobalConfig/GlobalConfig'; +import SourceCode from '../containers/SourceCode/SourceCode'; +import Tree from '../containers/Tree'; + +export const useEditorTabs = ({ + leftTabs, + rightTabs, +}: { + leftTabs?: TabItemProps[]; + rightTabs?: TabItemProps[]; +}) => { + const tabs = React.useMemo( + () => ({ + left: [ + { + id: 'page', + title: 'PAGE', + component: () => ( + <Tabs + items={[ + { + id: 'blocks-list', + title: 'BLOCKS', + component: BlocksList, + withPadding: true, + }, + { + id: 'layers', + title: 'LAYERS', + component: Tree, + withPadding: true, + }, + { + id: 'source-code', + title: 'RAW', + component: ({className}) => ( + <SourceCode className={className} /> + ), + withPadding: true, + }, + ]} + /> + ), + }, + { + id: 'global', + title: 'GLOBAL', + component: GlobalConfig, + }, + ...(leftTabs || []), + ], + right: [ + { + id: 'edit', + title: 'EDIT', + component: () => ( + <Tabs + items={[ + { + id: 'block-config', + title: 'INPUTS', + component: BlockConfigForm, + }, + { + id: 'source-code', + title: 'RAW', + component: ({className}) => ( + <SourceCode + className={className} + showSelectedBlockOnly={true} + /> + ), + withPadding: true, + }, + ]} + /> + ), + }, + ...(rightTabs || []), + ], + }), + [leftTabs, rightTabs], + ); + + return tabs; +}; diff --git a/src/editor-v2/hooks/useMainEditorInitialize.ts b/src/editor-v2/hooks/useMainEditorInitialize.ts new file mode 100644 index 0000000000..1796df45fc --- /dev/null +++ b/src/editor-v2/hooks/useMainEditorInitialize.ts @@ -0,0 +1,42 @@ +import * as React from 'react'; + +import {usePostMessageAPIListener} from '../../common/postMessage'; +import {PageContent} from '../../models'; + +import {useMainEditorStore} from './useMainEditorStore'; +import {usePostMessageEvents} from './usePostMessageEvents'; + +const useMainEditorInitialize = (content?: PageContent) => { + const {requestPostMessage} = usePostMessageEvents(); + const {initialize, setConfig, setContent} = useMainEditorStore(); + + usePostMessageAPIListener( + 'ON_INIT', + () => { + initialize(); + requestPostMessage('GET_SUPPORTED_BLOCKS', {}); + + if (!content) { + requestPostMessage('GET_INITIAL_CONTENT', {}); + } + }, + [requestPostMessage], + ); + + React.useEffect(() => { + if (content) { + setContent(content, true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [content]); + + usePostMessageAPIListener('ON_INITIAL_CONTENT', (data) => { + setContent(data, true); + }); + + usePostMessageAPIListener('ON_SUPPORTED_BLOCKS', (data) => { + setConfig(data); + }); +}; + +export default useMainEditorInitialize; diff --git a/src/editor-v2/hooks/useMainEditorStore.ts b/src/editor-v2/hooks/useMainEditorStore.ts new file mode 100644 index 0000000000..3d35bd2030 --- /dev/null +++ b/src/editor-v2/hooks/useMainEditorStore.ts @@ -0,0 +1,10 @@ +import * as React from 'react'; + +import {useStore} from 'zustand'; + +import {MainEditorStoreContext} from '../context/editorStore'; + +export const useMainEditorStore = () => { + const {state} = React.useContext(MainEditorStoreContext); + return useStore(state); +}; diff --git a/src/editor-v2/hooks/usePCEditorSettings.ts b/src/editor-v2/hooks/usePCEditorSettings.ts new file mode 100644 index 0000000000..bceaea87ff --- /dev/null +++ b/src/editor-v2/hooks/usePCEditorSettings.ts @@ -0,0 +1,8 @@ +import * as React from 'react'; + +import {IframeContext} from '../context/iframeContext'; + +export const usePCEditorSettings = () => { + const {url, setUrl} = React.useContext(IframeContext); + return {currentUrl: url, changeUrl: setUrl}; +}; diff --git a/src/editor-v2/hooks/usePostMessageEvents.ts b/src/editor-v2/hooks/usePostMessageEvents.ts new file mode 100644 index 0000000000..cc80008433 --- /dev/null +++ b/src/editor-v2/hooks/usePostMessageEvents.ts @@ -0,0 +1,26 @@ +import * as React from 'react'; + +import {requestActionPostMessage} from '../../common/postMessage'; +import {ActionMessageTypes} from '../../common/types'; +import {IframeContext} from '../context/iframeContext'; + +interface UsePostMessageRequestReturn { + requestPostMessage: <K extends keyof ActionMessageTypes>( + action: K, + data: ActionMessageTypes[K], + ) => void; +} + +export function usePostMessageEvents(): UsePostMessageRequestReturn { + const {iframeElement} = React.useContext(IframeContext); + + return { + requestPostMessage: (action, data) => { + if (iframeElement && iframeElement.contentWindow) { + return requestActionPostMessage(action, data, iframeElement.contentWindow); + } + + return undefined; + }, + }; +} diff --git a/src/editor-v2/index.ts b/src/editor-v2/index.ts new file mode 100644 index 0000000000..346f948557 --- /dev/null +++ b/src/editor-v2/index.ts @@ -0,0 +1,5 @@ +export * from '../common/types'; +export * from './containers'; +export * from './hooks'; +export * from './utils'; +export * from './constants'; diff --git a/src/editor-v2/store.ts b/src/editor-v2/store.ts new file mode 100644 index 0000000000..83ec95c97c --- /dev/null +++ b/src/editor-v2/store.ts @@ -0,0 +1,452 @@ +import _ from 'lodash'; + +import {EditorHistorySnapshot, EditorState, initialStore} from '../common/store'; +import {RectMapEntry} from '../common/types/rect'; +import {initializeStore} from '../common/utils'; +import {ConstructorBlock, PageContent} from '../models'; + +import {ZOOM_STEPS} from './constants'; +import { + duplicateArrayItem, + generateChildrenPathFromArray, + getDestinationShiftBeforeReorder, + insert, + isItemsNeighbours, + modifyObjectByPath, + removeFromArray, + reorderArrayItems, +} from './utils'; + +const MAX_EDITOR_HISTORY = 50; +const HISTORY_DEBOUNCE_MS = 500; + +/** After unsetting a leaf, remove empty `{}` parents (e.g. `seo` after `seo.slug` is removed). */ +function pruneEmptyObjectAncestors(root: object, unsetPath: string) { + const segments = _.toPath(unsetPath); + for (let depth = segments.length - 1; depth >= 1; depth--) { + const parentPath = segments.slice(0, depth); + const parentVal = _.get(root, parentPath); + if (_.isPlainObject(parentVal) && Object.keys(parentVal as object).length === 0) { + _.unset(root, parentPath); + } + } +} + +function snapshotEditorHistory(state: EditorState): EditorHistorySnapshot { + return { + content: _.cloneDeep(state.content), + selectedBlock: state.selectedBlock ? [...state.selectedBlock] : null, + }; +} + +function appendHistoryPast( + past: EditorHistorySnapshot[], + snapshot: EditorHistorySnapshot, +): EditorHistorySnapshot[] { + return [...past.slice(-(MAX_EDITOR_HISTORY - 1)), snapshot]; +} + +export interface EditorMethods { + initialize(): void; + setSelectedBlock(path: number[] | null): void; + setRectMap(rects: RectMapEntry[]): void; + setHeight(height: number): void; + setDeviceWidth(deviceWidth: string): void; + setZoom(zoom: number): void; + increaseZoom(): void; + decreaseZoom(): void; + togglePreviewMode(): void; + setConfig(data: Pick<EditorState, 'blocks' | 'subBlocks' | 'global'>): void; + setContent(data: PageContent, skipHistory?: boolean): void; + insertBlock(path: number[], blockType: string, position?: 'prepend' | 'append'): void; + enableInsertMode(blockType: string): void; + enableReorderMode(path: number[]): void; + disableMode(): void; + updateField(path: string, value: unknown): void; + deleteBlock(path: number[]): void; + duplicateBlock(path: number[]): void; + reorderBlock(path: number[], destination: number[], position?: 'prepend' | 'append'): void; + resetInitialize(): void; + resetBlocks(): void; + undo(): void; + redo(): void; +} + +export type EditorStore = EditorState & EditorMethods; + +export const createEditorStore = initializeStore<EditorState, EditorMethods>( + initialStore, + (set, get) => { + let pendingSnapshot: EditorHistorySnapshot | null = null; + let updateFieldTimer: ReturnType<typeof setTimeout> | null = null; + + function clearUpdateFieldTimer() { + if (updateFieldTimer !== null) { + clearTimeout(updateFieldTimer); + updateFieldTimer = null; + } + } + + function commitPending() { + clearUpdateFieldTimer(); + if (pendingSnapshot !== null) { + const snapshot = pendingSnapshot; + pendingSnapshot = null; + set((state) => ({ + ...state, + historyPast: appendHistoryPast(state.historyPast, snapshot), + historyFuture: [], + })); + } + } + + return { + setHeight(height: number) { + // We have to add 200-500px, because of bottom padding or margin of last element + // which is not taken into calculation of final height + const newHeight = height + 500; + set((state) => ({...state, height: newHeight})); + }, + setDeviceWidth(deviceWidth: string) { + set((state) => ({...state, deviceWidth})); + }, + setZoom(zoom) { + if (zoom > 0) { + set((state) => ({...state, zoom})); + } + }, + increaseZoom() { + const currentZoom = get().zoom; + + for (const step of ZOOM_STEPS) { + if (currentZoom < step) { + get().setZoom(step); + break; + } + } + }, + decreaseZoom() { + const currentZoom = get().zoom; + const reverseSteps = ZOOM_STEPS.slice().reverse(); + + for (const step of reverseSteps) { + if (currentZoom > step) { + get().setZoom(step); + break; + } + } + }, + togglePreviewMode() { + set((state) => ({...state, isPreviewMode: !state.isPreviewMode})); + }, + setConfig(data) { + set((state) => ({...state, ...data})); + }, + undo() { + if (pendingSnapshot !== null) { + const snapshot = pendingSnapshot; + clearUpdateFieldTimer(); + pendingSnapshot = null; + set((state) => { + const currentSnap = snapshotEditorHistory(state); + return { + ...state, + content: snapshot.content, + selectedBlock: snapshot.selectedBlock, + historyFuture: [currentSnap, ...state.historyFuture], + }; + }); + return; + } + set((state) => { + if (state.historyPast.length === 0) { + return state; + } + + const past = [...state.historyPast]; + const previous = past.pop(); + if (previous === undefined) { + return state; + } + + const currentSnap = snapshotEditorHistory(state); + + return { + ...state, + content: previous.content, + selectedBlock: previous.selectedBlock, + historyPast: past, + historyFuture: [currentSnap, ...state.historyFuture], + }; + }); + }, + redo() { + clearUpdateFieldTimer(); + pendingSnapshot = null; + set((state) => { + if (state.historyFuture.length === 0) { + return state; + } + + const future = [...state.historyFuture]; + const next = future.shift(); + if (next === undefined) { + return state; + } + + const currentSnap = snapshotEditorHistory(state); + + return { + ...state, + content: next.content, + selectedBlock: next.selectedBlock, + historyPast: appendHistoryPast(state.historyPast, currentSnap), + historyFuture: future, + }; + }); + }, + insertBlock: (arrayPath, blockType, position = 'append') => { + if (position === 'append') { + // TODO: fix + // eslint-disable-next-line no-not-accumulator-reassign/no-not-accumulator-reassign, no-param-reassign + arrayPath[arrayPath.length - 1] = arrayPath[arrayPath.length - 1] + 1; + } + + const blocksConfig = get().content.blocks; + const blocksData = get().blocks; + + const foundBlock = blocksData.find(({type}) => type === blockType); + const defaultValue = + foundBlock && foundBlock.schema.default + ? {...foundBlock.schema.default, type: blockType} + : {type: blockType}; + + const newBlocksConfig = modifyObjectByPath( + blocksConfig, + arrayPath, + (parentBlocks, index) => + insert(parentBlocks, index, defaultValue as ConstructorBlock), + ); + + set((state) => { + const before = snapshotEditorHistory(state); + + return { + ...state, + historyPast: appendHistoryPast(state.historyPast, before), + historyFuture: [], + content: {...state.content, blocks: newBlocksConfig}, + selectedBlock: arrayPath, + }; + }); + }, + enableInsertMode(blockType: string) { + set((state) => ({ + ...state, + manipulateOverlayMode: 'insert', + preInsertBlockType: blockType, + })); + }, + disableMode() { + set((state) => ({ + ...state, + manipulateOverlayMode: false, + preInsertBlockType: undefined, + preReorderBlockPath: undefined, + })); + }, + enableReorderMode(path) { + set((state) => ({ + ...state, + manipulateOverlayMode: 'reorder', + preReorderBlockPath: path, + })); + }, + setContent(content, skipHistory = false) { + set((state) => { + if (skipHistory) { + return { + ...state, + historyPast: state.historyPast, + historyFuture: [], + content, + }; + } + + const before = snapshotEditorHistory(state); + + return { + ...state, + historyPast: appendHistoryPast(state.historyPast, before), + historyFuture: [], + content, + }; + }); + }, + initialize() { + set((state) => ({ + ...state, + initialized: true, + })); + }, + setSelectedBlock(path) { + set((state) => ({ + ...state, + selectedBlock: path, + })); + }, + setRectMap(rects) { + set((state) => { + if (_.isEqual(state.rectMap, rects)) { + return state; + } + return {...state, rectMap: rects}; + }); + }, + updateField(path, value) { + if (pendingSnapshot === null) { + pendingSnapshot = snapshotEditorHistory(get()); + } + + clearUpdateFieldTimer(); + updateFieldTimer = setTimeout(() => { + updateFieldTimer = null; + commitPending(); + }, HISTORY_DEBOUNCE_MS); + + set((state) => { + const newConfig = _.cloneDeep(state.content); + // `_.set(..., undefined)` leaves empty objects; clearing a field should remove the key. + if (value === undefined) { + _.unset(newConfig, path); + pruneEmptyObjectAncestors(newConfig, path); + } else { + _.set(newConfig, path, value); + } + return {...state, historyFuture: [], content: newConfig}; + }); + }, + deleteBlock: (arrayPath) => { + const blocksConfig = get().content.blocks; + + const newBlocksConfig = modifyObjectByPath( + blocksConfig, + arrayPath, + removeFromArray, + ); + set((state) => { + const before = snapshotEditorHistory(state); + + return { + ...state, + historyPast: appendHistoryPast(state.historyPast, before), + historyFuture: [], + content: {...state.content, blocks: newBlocksConfig}, + selectedBlock: null, + }; + }); + }, + duplicateBlock: (arrayPath) => { + const blocksConfig = get().content.blocks; + + const newBlocksConfig = modifyObjectByPath( + blocksConfig, + arrayPath, + duplicateArrayItem, + ); + + set((state) => { + const before = snapshotEditorHistory(state); + + return { + ...state, + historyPast: appendHistoryPast(state.historyPast, before), + historyFuture: [], + content: {...state.content, blocks: newBlocksConfig}, + }; + }); + }, + reorderBlock: (arrayPath, destination, position = 'append') => { + const dest = _.cloneDeep(destination); + let finalDestinationPath: number[] = _.cloneDeep(destination); + + if (position === 'append') { + dest[dest.length - 1] = dest[dest.length - 1] + 1; + } + + let newBlocksConfig: ConstructorBlock[]; + const blocksConfig = get().content.blocks; + // Copy + const copiedBlock = _.get(blocksConfig, generateChildrenPathFromArray(arrayPath)); + + if (isItemsNeighbours(arrayPath, dest)) { + newBlocksConfig = modifyObjectByPath( + blocksConfig, + arrayPath, + (parentBlocks) => { + return reorderArrayItems( + parentBlocks, + arrayPath[arrayPath.length - 1], + dest[dest.length - 1], + ); + }, + ); + + if ( + position === 'append' && + dest[dest.length - 1] < arrayPath[arrayPath.length - 1] + ) { + finalDestinationPath[finalDestinationPath.length - 1] = + finalDestinationPath[finalDestinationPath.length - 1] + 1; + } + } else { + const arrayDest = getDestinationShiftBeforeReorder(arrayPath, dest); + finalDestinationPath = _.cloneDeep(arrayDest); + + // Delete + const blocksConfigWithoutBlock = modifyObjectByPath( + blocksConfig, + arrayPath, + removeFromArray, + ); + // Paste + newBlocksConfig = modifyObjectByPath( + blocksConfigWithoutBlock, + arrayDest, + (parentBlocks, index) => insert(parentBlocks, index, copiedBlock), + ); + } + + set((state) => { + const before = snapshotEditorHistory(state); + + return { + ...state, + historyPast: appendHistoryPast(state.historyPast, before), + historyFuture: [], + content: {...state.content, blocks: newBlocksConfig}, + selectedBlock: finalDestinationPath, + }; + }); + }, + resetInitialize: () => { + set((state) => ({ + ...state, + initialized: false, + })); + }, + resetBlocks: () => { + set((state) => { + const before = snapshotEditorHistory(state); + + return { + ...state, + historyPast: appendHistoryPast(state.historyPast, before), + historyFuture: [], + content: {...state.content, blocks: []}, + }; + }); + }, + }; + }, +); diff --git a/src/editor-v2/styles/mixins.scss b/src/editor-v2/styles/mixins.scss new file mode 100644 index 0000000000..db194a5836 --- /dev/null +++ b/src/editor-v2/styles/mixins.scss @@ -0,0 +1,66 @@ +@import '@gravity-ui/uikit/styles/mixins.scss'; +@import './variables.scss'; + +@mixin control($hoverScale: 1.05) { + display: flex; + justify-content: center; + align-items: center; + transition: transform $editorTransitionTime; + + &:hover { + transform: scale($hoverScale); + } +} + +@mixin custom-scrollbar($width: 8px, $track-radius: 4px, $thumb-radius: 4px) { + scrollbar-width: thin; + scrollbar-color: var(--g-color-scroll-handle) transparent; + overflow: auto; + + &::-webkit-scrollbar { + width: $width; + height: $width; + background-color: transparent; + } + + &::-webkit-scrollbar-track { + background: transparent; + border-radius: $track-radius; + } + + &::-webkit-scrollbar-thumb { + background: var(--g-color-scroll-handle); + border-radius: $thumb-radius; + transition: background-color $editorTransitionTime; + opacity: 0; + + &:hover { + background: var(--g-color-scroll-handle-hover); + opacity: 1; + } + + &:active { + background: var(--g-color-scroll-handle-hover); + opacity: 1; + } + } + + &::-webkit-scrollbar-corner { + background: transparent; + } + + &:hover::-webkit-scrollbar-thumb { + opacity: 0.7; + } + + &:not(:hover)::-webkit-scrollbar-thumb { + opacity: 0; + transition: opacity 0.5s ease-out; + } +} + +@mixin title-styles() { + @include overflow-ellipsis(); + margin: 0; + display: inherit; +} diff --git a/src/editor-v2/styles/root.scss b/src/editor-v2/styles/root.scss new file mode 100644 index 0000000000..c25a7298a9 --- /dev/null +++ b/src/editor-v2/styles/root.scss @@ -0,0 +1,9 @@ +.g-root { + --g-color-base-selection: var(--g-color-private-black-200); + --g-color-base-selection-hover: var(--g-color-private-black-300); + --g-color-base-brand: rgb(38, 38, 38); // --g-color-private-black-850-solid light theme only + --g-color-base-brand-hover: rgb(76, 76, 76); // --g-color-private-black-700-solid + --g-color-text-brand-contrast: var(--g-color-text-light-primary); + --g-color-text-brand-heavy: rgb(76, 76, 76); + --g-color-line-brand: var(--g-color-text-primary); +} diff --git a/src/editor-v2/styles/variables.scss b/src/editor-v2/styles/variables.scss new file mode 100644 index 0000000000..d44cdee401 --- /dev/null +++ b/src/editor-v2/styles/variables.scss @@ -0,0 +1,8 @@ +$ns: 'pceditor-'; +$editorTransitionTime: 0.2s; +$editorShadow: + 0px 2px 8px rgba(0, 0, 0, 0.06), + 0px 4px 24px rgba(0, 0, 0, 0.06); +$editorControlBorderRadius: 8px; + +$headerHeight: 0px; diff --git a/src/editor-v2/utils/cn.ts b/src/editor-v2/utils/cn.ts new file mode 100644 index 0000000000..a43775d67e --- /dev/null +++ b/src/editor-v2/utils/cn.ts @@ -0,0 +1,5 @@ +import {withNaming} from '@bem-react/classname'; + +export const EDITOR_NAMESPACE = 'pceditor-'; + +export const editorCn = withNaming({n: EDITOR_NAMESPACE, e: '__', m: '_'}); diff --git a/src/editor-v2/utils/code.ts b/src/editor-v2/utils/code.ts new file mode 100644 index 0000000000..3b46632677 --- /dev/null +++ b/src/editor-v2/utils/code.ts @@ -0,0 +1,12 @@ +import yaml from 'js-yaml'; + +import {PageContent} from '../../models'; + +export function parseCode(code: string) { + const pageContent = yaml.load(code) as PageContent; + + return { + ...pageContent, + blocks: pageContent.blocks?.filter(Boolean), + }; +} diff --git a/src/editor-v2/utils/index.ts b/src/editor-v2/utils/index.ts new file mode 100644 index 0000000000..8d98137145 --- /dev/null +++ b/src/editor-v2/utils/index.ts @@ -0,0 +1,179 @@ +import _ from 'lodash'; + +import {ConstructorBlock} from '../../models'; +export * from './code'; +export * from './cn'; + +export function insert<T>(arr: Array<T>, index: number, newItem: T) { + return [...arr.slice(0, index), newItem, ...arr.slice(index)]; +} + +export function removeFromArray<T>(array: Array<T>, index: number) { + return [...array.slice(0, index), ...array.slice(index + 1)]; +} + +export function swapArrayItems<T>(array: Array<T>, firstIndex: number, secondIndex: number) { + const results = array.slice(); + const firstItem = array[firstIndex]; + results[firstIndex] = array[secondIndex]; + results[secondIndex] = firstItem; + return results; +} + +export function reorderArrayItems<T>(array: Array<T>, index: number, destination: number) { + const min = Math.min(index, destination); + const max = Math.max(index, destination); + const firstOperationRemove = index < destination; + const result = []; + result.push(...array.slice(0, min)); + if (!firstOperationRemove) { + result.push(array[index]); + } + result.push(...array.slice(firstOperationRemove ? min + 1 : min, max)); + if (firstOperationRemove) { + result.push(array[index]); + } + result.push(...array.slice(firstOperationRemove ? max : max + 1, array.length)); + return result.filter((item) => item !== undefined); +} + +export function duplicateArrayItem<T>(array: Array<T>, index: number) { + const duplicatedItem = _.cloneDeep(array[index]); + return [...array.slice(0, index), duplicatedItem, ...array.slice(index)]; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function insertByPath<T extends object>(object: T, path: string, value: any) { + if (!path) { + return value as T; + } + + return _.setWith(_.clone(object), path, value, _.clone); +} + +/* + * path: string; + * Example: + * 1. blocks[0] => {path: blocks, index: 0} + * 2. blocks[2].children[10] => {path: blocks[2].children, index: 10} + **/ +export function splitPathAndIndex(path: string) { + // Match blocks[3], blocks[0].children[12], blocks[0], blocks[999999] + const bracketsRegExp = /(.*)\[(\d+)]$/g; + const regexpMatches = Array.from(path.matchAll(bracketsRegExp)); + if (regexpMatches.length) { + return { + // blocks, blocks[0].children + path: regexpMatches[0][1], + // 3, 12, 0, 9999 + index: Number(regexpMatches[0][2]), + }; + } + + // eslint-disable-next-line no-console + console.error('Non correct path for splitting'); + return undefined; +} + +/* + * [0, 4, 3] => [0].children[4].children[3] + * */ +export function generateChildrenPathFromArray(indexes: number[]) { + if (!indexes.length) { + return ''; + } + + let resultPath = `[${indexes[0]}]`; + + if (indexes.length > 1) { + for (let i = 1; i < indexes.length; i++) { + resultPath += `.children[${indexes[i]}]`; + } + } + + return resultPath; +} + +export function modifyObjectByPath( + blocks: ConstructorBlock[], + arrayPath: number[], + modifyCallback: (parentBlocks: ConstructorBlock[], index: number) => ConstructorBlock[], +) { + // [1] + // [4].children[3] + const insertPath = generateChildrenPathFromArray(arrayPath); + // path: '' index: 1 + // path: '[4].children' index: 3 + const splitPath = splitPathAndIndex(insertPath); + + if (splitPath) { + const {path: parentPath, index} = splitPath; + // Get Array that lies on path + const parentArray = parentPath ? _.get(blocks, parentPath) : blocks; + + const value = Array.isArray(parentArray) ? parentArray : []; + // Modify Array + const newModifiedArray = modifyCallback(value, index); + + // Return it back + return insertByPath(blocks, parentPath, newModifiedArray); + } + + return blocks; +} + +export function isItemsNeighbours(arrayA: number[], arrayB: number[]) { + if (arrayA.length !== arrayB.length) { + return false; + } + + for (let i = 0; i < arrayA.length - 1; i++) { + if (arrayA[i] !== arrayB[i]) { + return false; + } + } + + return true; +} + +export function getDestinationShiftBeforeReorder(arrayInit: number[], arrayDest: number[]) { + if (arrayInit.length === arrayDest.length || arrayInit.length > arrayDest.length) { + return arrayDest; + } + + for (let i = 0; i < arrayInit.length; i++) { + if (arrayInit[i] < arrayDest[i]) { + return prepareShift(arrayInit, arrayDest); + } + } + + return arrayDest; +} + +export function prepareShift(arrayInit: number[], arrayDest: number[]) { + if (arrayInit.length === arrayDest.length || arrayInit.length > arrayDest.length) { + return arrayDest; + } + + return arrayDest.map((pathIndex, index) => + index === arrayInit.length - 1 ? pathIndex - 1 : pathIndex, + ); +} + +export const getUrlOrigin = (url: string) => { + try { + const urlObject = new URL(url); + return urlObject.origin; + } catch { + return undefined; + } +}; + +export const getItemTitle = (item: object): string | undefined => { + return ( + _.get(item, 'title.text') || + _.get(item, 'title') || + _.get(item, 'textContent.title') || + _.get(item, 'content.title') + ); +}; diff --git a/src/editor/components/CodeEditor/CodeEditor.tsx b/src/editor/components/CodeEditor/CodeEditor.tsx index de0978a5c4..86de60f1c9 100644 --- a/src/editor/components/CodeEditor/CodeEditor.tsx +++ b/src/editor/components/CodeEditor/CodeEditor.tsx @@ -17,8 +17,6 @@ import './CodeEditor.scss'; const b = block('code-editor'); -const ON_CHANGE_DEBOUNCE_TIMEOUT = 300; - interface CodeEditorProps { code: string; fullscreenModeOn: boolean; @@ -28,60 +26,57 @@ interface CodeEditorProps { message?: CodeEditorMessageProps; } -export const CodeEditor = React.memo( - ({onChange, validator, fullscreenModeOn, onFullscreenModeOnUpdate, code}: CodeEditorProps) => { - const [message, setMessage] = React.useState(() => validator(code)); - const {theme = Theme.Light} = React.useContext(EditorContext); +export const CodeEditor = ({ + onChange, + validator, + fullscreenModeOn, + onFullscreenModeOnUpdate, + code, +}: CodeEditorProps) => { + const [message, setMessage] = React.useState(() => validator(code)); + const {theme = Theme.Light} = React.useContext(EditorContext); - // eslint-disable-next-line react-hooks/exhaustive-deps - const onChangeWithValidation = React.useCallback( - debounce((newCode: string) => { - const validationResult = validator(newCode); + // eslint-disable-next-line react-hooks/exhaustive-deps + const onChangeWithValidation = React.useCallback( + debounce((newCode: string) => { + const validationResult = validator(newCode); - setMessage(validationResult); - onChange(parseCode(newCode)); - }, ON_CHANGE_DEBOUNCE_TIMEOUT), - [onChange, validator], - ); + setMessage(validationResult); + onChange(parseCode(newCode)); + }, 200), + [onChange, validator], + ); - return ( - <div className={b({fullscreen: fullscreenModeOn})}> - <div className={b('header')}> - <Button - view="flat-secondary" - onClick={() => onFullscreenModeOnUpdate(!fullscreenModeOn)} - > - <Icon - data={ - fullscreenModeOn ? ChevronsCollapseUpRight : ChevronsExpandUpRight - } - size={16} - /> - </Button> - </div> - <div className={b('code')}> - <MonacoEditor - key={String(fullscreenModeOn)} - defaultValue={code} - value={code} - language="yaml" - options={options} - onChange={onChangeWithValidation} - theme={theme === Theme.Dark ? 'vs-dark' : 'vs'} + return ( + <div className={b({fullscreen: fullscreenModeOn})}> + <div className={b('header')}> + <Button + view="flat-secondary" + onClick={() => onFullscreenModeOnUpdate(!fullscreenModeOn)} + > + <Icon + data={fullscreenModeOn ? ChevronsCollapseUpRight : ChevronsExpandUpRight} + size={16} /> - </div> - <div className={b('footer')}> - {message && ( - <div className={b('message-container')}> - <div className={b('message', {status: message.status})}> - {message.text} - </div> - </div> - )} - </div> + </Button> </div> - ); - }, -); - -CodeEditor.displayName = 'CodeEditor'; + <div className={b('code')}> + <MonacoEditor + key={String(fullscreenModeOn)} + value={code} + language="yaml" + options={options} + onChange={onChangeWithValidation} + theme={theme === Theme.Dark ? 'vs-dark' : 'vs'} + /> + </div> + <div className={b('footer')}> + {message && ( + <div className={b('message-container')}> + <div className={b('message', {status: message.status})}>{message.text}</div> + </div> + )} + </div> + </div> + ); +}; diff --git a/src/editor/components/EditBlock/EditBlock.tsx b/src/editor/components/EditBlock/EditBlock.tsx index 100d392617..50ad707d55 100644 --- a/src/editor/components/EditBlock/EditBlock.tsx +++ b/src/editor/components/EditBlock/EditBlock.tsx @@ -35,14 +35,7 @@ export type EditBlockActions = { [key in EditBlockControls]?: () => void; }; -const EditBlock = ({ - actions, - isActive, - onSelect, - isHeader, - children, - resetPaddings, -}: EditBlockProps) => { +const EditBlock = ({actions, isActive, onSelect, isHeader, children}: EditBlockProps) => { const ref = React.useRef<HTMLDivElement>(null); const stopPropagationProps = React.useMemo( @@ -73,7 +66,6 @@ const EditBlock = ({ className={b('controls', { active: isActive, isHeader, - 'reset-paddings': resetPaddings, })} > {isActive && ( diff --git a/src/editor/components/ErrorBoundary/ErrorBoundary.tsx b/src/editor/components/ErrorBoundary/ErrorBoundary.tsx index c901863400..57a6eb3af2 100644 --- a/src/editor/components/ErrorBoundary/ErrorBoundary.tsx +++ b/src/editor/components/ErrorBoundary/ErrorBoundary.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {BlockDecorationProps} from '../../../models'; +import {BlockWrapperDataProps} from '../../../models'; import {block} from '../../../utils'; import {getBlockId} from '../../utils'; @@ -10,7 +10,7 @@ import './ErrorBoundary.scss'; const b = block('error-boundary'); -interface ErrorBoundaryProps extends React.PropsWithChildren, Partial<BlockDecorationProps> {} +interface ErrorBoundaryProps extends React.PropsWithChildren, Partial<BlockWrapperDataProps> {} interface ErrorBoundaryState { error?: string; } diff --git a/src/editor/components/NotFoundBlock/NotFoundBlock.tsx b/src/editor/components/NotFoundBlock/NotFoundBlock.tsx index f33adbccce..82744aaa73 100644 --- a/src/editor/components/NotFoundBlock/NotFoundBlock.tsx +++ b/src/editor/components/NotFoundBlock/NotFoundBlock.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import {BlockBase} from '../../../components'; -import {BlockDecorationProps} from '../../../models'; +import {BlockWrapperDataProps} from '../../../models'; import {block} from '../../../utils'; import {i18n} from './i18n'; @@ -10,7 +10,7 @@ import './NotFoundBlock.scss'; const b = block('not-found-block'); -export const NotFoundBlock = ({type, children}: BlockDecorationProps) => +export const NotFoundBlock = ({type, children}: BlockWrapperDataProps & React.PropsWithChildren) => children ? ( <React.Fragment>{children}</React.Fragment> ) : ( diff --git a/src/editor/containers/Editor/Editor.tsx b/src/editor/containers/Editor/Editor.tsx index b19e2d1dd1..4a84680c26 100644 --- a/src/editor/containers/Editor/Editor.tsx +++ b/src/editor/containers/Editor/Editor.tsx @@ -1,4 +1,5 @@ import {PageConstructor, PageConstructorProvider} from '../../../containers/PageConstructor'; +import {GravityBlocksProvider} from '../../../gravity-blocks/extensions/GravityBlocksExtension'; import {block} from '../../../utils'; import AddBlock from '../../components/AddBlock/AddBlock'; import {CodeEditor} from '../../components/CodeEditor/CodeEditor'; @@ -83,8 +84,10 @@ export const Editor = (props: EditorProps) => { {!isCodeOnlyMode && ( <Layout.Right> <ErrorBoundary key={errorBoundaryState}> - <PageConstructorProvider {...providerProps} theme={constructorTheme}> - <PageConstructor {...outgoingProps} /> + <PageConstructorProvider {...providerProps}> + <GravityBlocksProvider theme={constructorTheme}> + <PageConstructor {...outgoingProps} /> + </GravityBlocksProvider> </PageConstructorProvider> </ErrorBoundary> {isFormEditMode && <AddBlock onAdd={onAdd} />} diff --git a/src/editor/containers/Editor/hooks/useEditorState.tsx b/src/editor/containers/Editor/hooks/useEditorState.tsx index 3f2824b296..c6c52c8a15 100644 --- a/src/editor/containers/Editor/hooks/useEditorState.tsx +++ b/src/editor/containers/Editor/hooks/useEditorState.tsx @@ -1,15 +1,16 @@ import * as React from 'react'; -import {BlockDecorationProps} from '../../../../models'; -import {generateDefaultSchema} from '../../../../schema'; -import EditBlock from '../../../components/EditBlock/EditBlock'; -import {ErrorBoundary} from '../../../components/ErrorBoundary/ErrorBoundary'; -import {NotFoundBlock} from '../../../components/NotFoundBlock/NotFoundBlock'; +// import {PageConstructorExtension} from '../../../../containers/PageConstructor/PageConstructor'; +// import {BlockWrapperDataProps} from '../../../../models'; +import {generateDefaultSchema} from '../../../../gravity-blocks/schema'; +// import EditBlock from '../../../components/EditBlock/EditBlock'; +// import {ErrorBoundary} from '../../../components/ErrorBoundary/ErrorBoundary'; +// import {NotFoundBlock} from '../../../components/NotFoundBlock/NotFoundBlock'; import {useCodeValidator} from '../../../hooks/useCodeValidator'; import {useMainState} from '../../../store/main'; import {useSettingsState} from '../../../store/settings'; import {EditModeItem, EditorProps, ViewModeItem} from '../../../types'; -import {addCustomDecorator, checkIsMobile, getBlockId} from '../../../utils'; +import {checkIsMobile} from '../../../utils'; import {useCode} from './useCode'; @@ -23,15 +24,8 @@ export const useEditorState = ({ theme: editorTheme, ...rest }: EditorProps) => { - const { - content, - activeBlockIndex, - errorBoundaryState, - onContentUpdate, - onAdd, - onSelect, - injectEditBlockProps, - } = useMainState(rest); + const {content, activeBlockIndex, errorBoundaryState, onContentUpdate, onAdd, onSelect} = + useMainState(rest); const { viewMode, @@ -58,37 +52,66 @@ export const useEditorState = ({ const codeValidator = useCodeValidator(schema); const outgoingProps = React.useMemo(() => { - const custom = - isCodeEditMode || isViewEditMode - ? rest.custom - : addCustomDecorator( - [ - (props: BlockDecorationProps) => <NotFoundBlock {...props} />, - (props: BlockDecorationProps) => ( - <EditBlock {...injectEditBlockProps(props)} /> - ), - // need errorBoundaryState flag to reset error on content update - (props: BlockDecorationProps) => ( - <ErrorBoundary - {...props} - key={`${getBlockId(props)}-${errorBoundaryState}`} - /> - ), - ], - rest.custom, - ); + const userExtensions = rest.extensions ?? []; + + if (isCodeEditMode || isViewEditMode) { + return { + content: transformedContent, + custom: rest.custom, + extensions: userExtensions, + viewMode, + }; + } + + // const editorExtensions: PageConstructorExtension[] = [ + // { + // name: 'Editor Not Found Block', + // id: '@gravity-ui/page-constructor/editor-not-found', + // settings: { + // blockWrapper: ({ + // children, + // type, + // }: BlockWrapperDataProps & React.PropsWithChildren) => ( + // <NotFoundBlock type={type} content={{}}> + // {children} + // </NotFoundBlock> + // ), + // }, + // }, + // { + // name: 'Editor Edit Block', + // id: '@gravity-ui/page-constructor/editor-edit-block', + // settings: { + // blockWrapper: (props: BlockWrapperDataProps & React.PropsWithChildren) => ( + // <EditBlock {...injectEditBlockProps(props)} /> + // ), + // }, + // }, + // { + // name: 'Editor Error Boundary', + // id: '@gravity-ui/page-constructor/editor-error-boundary', + // settings: { + // blockWrapper: (props: BlockWrapperDataProps & React.PropsWithChildren) => ( + // <ErrorBoundary + // {...props} + // key={`${getBlockId(props)}-${errorBoundaryState}`} + // /> + // ), + // }, + // }, + // ]; return { content: transformedContent, - custom, + custom: rest.custom, + extensions: userExtensions, viewMode, }; }, [ - injectEditBlockProps, - errorBoundaryState, viewMode, transformedContent, rest.custom, + rest.extensions, isCodeEditMode, isViewEditMode, ]); diff --git a/src/editor/containers/__stories__/Editor.stories.tsx b/src/editor/containers/__stories__/Editor.stories.tsx index d4238fb78b..e58f1df594 100644 --- a/src/editor/containers/__stories__/Editor.stories.tsx +++ b/src/editor/containers/__stories__/Editor.stories.tsx @@ -3,11 +3,11 @@ import * as React from 'react'; import {Meta, StoryFn} from '@storybook/react'; import {scriptsSrc, ymapApiKeyForStorybook} from '../../../../.storybook/maps'; -import {LocaleContext} from '../../../context/localeContext'; -import {MapType} from '../../../context/mapsContext/mapsContext'; -import {MapProvider} from '../../../context/mapsContext/mapsProvider'; +import {LocaleContext} from '../../../gravity-blocks/context/localeContext'; +import {MapType} from '../../../gravity-blocks/context/mapsContext/mapsContext'; +import {MapProvider} from '../../../gravity-blocks/context/mapsContext/mapsProvider'; +import {contentTransformer} from '../../../gravity-blocks/text-transform'; import {PageContent} from '../../../models'; -import {contentTransformer} from '../../../text-transform'; import {EditorProps} from '../../types'; import {Editor} from '../Editor/Editor'; diff --git a/src/editor/data/index.ts b/src/editor/data/index.ts index 52d4808b72..4ec434f5a4 100644 --- a/src/editor/data/index.ts +++ b/src/editor/data/index.ts @@ -31,7 +31,6 @@ const getBlockPreview = (blockType: BlockType): PreviewComponent => { return DefaultPreview; } }; - type EditorBlocksData = Partial<Record<BlockType, EdiorBlockData>>; async function getEditorBlocksData(): Promise<EditorBlocksData> { diff --git a/src/editor/data/templates/test-editor-block.json b/src/editor/data/templates/test-editor-block.json new file mode 100644 index 0000000000..43c1534b1f --- /dev/null +++ b/src/editor/data/templates/test-editor-block.json @@ -0,0 +1,18 @@ +{ + "template": { + "type": "test-editor-block", + "title": "Lorem ipsum dolor sit amet", + "table": { + "content": [ + ["Lorem", "ipsum 1", "dolor 2", "sit 3"], + ["Lorem 1", "0", "0", "0"], + ["Lorem 2", "0", "0", "1"], + ["Lorem 3", "0", "0", "1"], + ["Lorem 4", "0", "1", "1"], + ["Lorem 5", "1", "1", "1"] + ], + "legend": ["ipsum 1", "ipsum 2"], + "justify": ["start", "center", "center", "center"] + } + } +} diff --git a/src/editor/dynamic-forms-custom/parser/detect.ts b/src/editor/dynamic-forms-custom/parser/detect.ts index 4318030475..b99ae0265e 100644 --- a/src/editor/dynamic-forms-custom/parser/detect.ts +++ b/src/editor/dynamic-forms-custom/parser/detect.ts @@ -1,6 +1,6 @@ import {SpecTypes} from '@gravity-ui/dynamic-forms'; -import {Schema} from '../../../schema'; +import {Schema} from '../../../gravity-blocks/schema'; export enum ParserType { Object = 'object', diff --git a/src/editor/dynamic-forms-custom/parser/index.ts b/src/editor/dynamic-forms-custom/parser/index.ts index 37619c4f83..16d7e24e64 100644 --- a/src/editor/dynamic-forms-custom/parser/index.ts +++ b/src/editor/dynamic-forms-custom/parser/index.ts @@ -2,7 +2,7 @@ /* eslint-disable no-not-accumulator-reassign/no-not-accumulator-reassign */ import {ArraySpec, ObjectSpec, SpecTypes} from '@gravity-ui/dynamic-forms'; -import {Schema, SchemaDefinitions} from '../../../schema'; +import {Schema, SchemaDefinitions} from '../../../gravity-blocks/schema'; import {ParserType, detectParserType} from './detect'; import { diff --git a/src/editor/dynamic-forms-custom/parser/types.ts b/src/editor/dynamic-forms-custom/parser/types.ts index 562c933978..89479218f9 100644 --- a/src/editor/dynamic-forms-custom/parser/types.ts +++ b/src/editor/dynamic-forms-custom/parser/types.ts @@ -1,6 +1,6 @@ import {Spec as DynamicFormSpec, ObjectSpec} from '@gravity-ui/dynamic-forms'; -import {Schema} from '../../../schema'; +import {Schema} from '../../../gravity-blocks/schema'; export type OneOfSpec = { oneOf: DynamicFormSpec[]; diff --git a/src/editor/dynamic-forms-custom/parser/views.ts b/src/editor/dynamic-forms-custom/parser/views.ts index 0f33cb244d..abef022abf 100644 --- a/src/editor/dynamic-forms-custom/parser/views.ts +++ b/src/editor/dynamic-forms-custom/parser/views.ts @@ -1,4 +1,4 @@ -import {Schema} from '../../../schema'; +import {Schema} from '../../../gravity-blocks/schema'; import {CustomObjectSpec, CustomSpec} from './types'; diff --git a/src/editor/store/main/index.ts b/src/editor/store/main/index.ts index ac2874cffd..637694847d 100644 --- a/src/editor/store/main/index.ts +++ b/src/editor/store/main/index.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import {DEFAULT_THEME} from '../../../components/constants'; -import {Block, BlockDecorationProps, HeaderBlockTypes, PageContent} from '../../../models'; +import {Block, BlockWrapperDataProps, HeaderBlockTypes, PageContent} from '../../../models'; import {getCustomTypes, getHeaderBlock} from '../../../utils'; import {EditBlockActions, EditBlockControls} from '../../components/EditBlock/EditBlock'; import {EditBlockProps, EditorProps, ViewModeItem} from '../../types'; @@ -58,7 +58,7 @@ export function useMainState({content: intialContent, custom}: Omit<EditorProps, index: relativeIndex = 0, children, ...rest - }: BlockDecorationProps) => { + }: BlockWrapperDataProps & React.PropsWithChildren) => { const orderedBlocksStartIndex = contentHasHeader ? 1 : 0; const isHeader = checkIsHeader(type); const index = isHeader ? 0 : relativeIndex + orderedBlocksStartIndex; diff --git a/src/editor/types/index.ts b/src/editor/types/index.ts index 3364582942..d595c63103 100644 --- a/src/editor/types/index.ts +++ b/src/editor/types/index.ts @@ -1,7 +1,9 @@ +import * as React from 'react'; + import {PageConstructorProps, PageConstructorProviderProps} from '../../containers/PageConstructor'; -import {BlockDecorationProps, PageContent} from '../../models'; +import {SchemaCustomConfig} from '../../gravity-blocks/schema'; +import {BlockWrapperDataProps, PageContent} from '../../models'; import {Theme} from '../../models/common'; -import {SchemaCustomConfig} from '../../schema'; import {EditBlockActions} from '../components/EditBlock/EditBlock'; export type EditorBlockId = number | string; @@ -38,7 +40,10 @@ export interface EditBlockEditorProps { actions: EditBlockActions; } -export interface EditBlockProps extends EditBlockEditorProps, BlockDecorationProps { +export interface EditBlockProps + extends EditBlockEditorProps, + BlockWrapperDataProps, + React.PropsWithChildren { isHeader?: boolean; } diff --git a/src/editor/utils/index.ts b/src/editor/utils/index.ts index 2ee77f76ff..96bf3cfe62 100644 --- a/src/editor/utils/index.ts +++ b/src/editor/utils/index.ts @@ -1,23 +1,11 @@ import capitalize from 'lodash/capitalize'; -import {BlockDecorationProps, BlockDecorator, CustomConfig} from '../../models'; +import {BlockWrapperDataProps} from '../../models'; import {ViewModeItem} from '../types'; export const formatBlockName = (name: string) => capitalize(name).replace(/(block|-)/g, ' '); -export const addCustomDecorator = (decorators: BlockDecorator[], custom = {} as CustomConfig) => { - const customDecorators = custom.decorators || {}; - - return { - ...custom, - decorators: { - ...customDecorators, - block: [...(customDecorators.block || []), ...decorators], - }, - }; -}; - -export const getBlockId = ({index, type}: BlockDecorationProps) => +export const getBlockId = ({index, type}: BlockWrapperDataProps) => `${type}${index === undefined ? '' : `-${index}`}`; export const checkIsMobile = (viewMode: ViewModeItem) => diff --git a/src/form-builder-v2/CanvasContentContext.tsx b/src/form-builder-v2/CanvasContentContext.tsx new file mode 100644 index 0000000000..5d0b31ca31 --- /dev/null +++ b/src/form-builder-v2/CanvasContentContext.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; + +import type {Content} from '../form-generator-v2/types'; + +interface CanvasContentContextType { + content: Content; + setContent: React.Dispatch<React.SetStateAction<Content>>; + templateContent: Content; + setTemplateContent: React.Dispatch<React.SetStateAction<Content>>; +} + +const CanvasContentContext = React.createContext<CanvasContentContextType | null>(null); + +interface CanvasContentProviderProps { + content: Content; + setContent: React.Dispatch<React.SetStateAction<Content>>; + templateContent: Content; + setTemplateContent: React.Dispatch<React.SetStateAction<Content>>; + children: React.ReactNode; +} + +export const CanvasContentProvider = ({ + content, + setContent, + templateContent, + setTemplateContent, + children, +}: CanvasContentProviderProps) => { + const value = React.useMemo<CanvasContentContextType>( + () => ({content, setContent, templateContent, setTemplateContent}), + [content, setContent, templateContent, setTemplateContent], + ); + return <CanvasContentContext.Provider value={value}>{children}</CanvasContentContext.Provider>; +}; + +export const useCanvasContent = (): CanvasContentContextType => { + const context = React.useContext(CanvasContentContext); + if (!context) { + throw new Error('useCanvasContent must be used within a CanvasContentProvider'); + } + return context; +}; diff --git a/src/form-builder-v2/FormBuilderV2.scss b/src/form-builder-v2/FormBuilderV2.scss new file mode 100644 index 0000000000..74cfd901a7 --- /dev/null +++ b/src/form-builder-v2/FormBuilderV2.scss @@ -0,0 +1,128 @@ +@import './styles/variables.scss'; + +$block: '.#{$ns-form-builder-v2}main'; +$palette-block: '.#{$ns-form-builder-v2}palette'; + +#{$block} { + width: 100%; + min-width: 640px; + display: flex; + flex-direction: column; + min-height: 0; + + &__toolbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--g-spacing-2) 0; + gap: var(--g-spacing-3); + } + + &__toolbar-option { + display: inline-flex; + align-items: center; + gap: 6px; + } + + &__schema-popup { + width: min(560px, 90vw); + padding: var(--g-spacing-4); + display: flex; + flex-direction: column; + gap: 10px; + } + + &__schema-popup-header { + display: flex; + flex-direction: column; + gap: var(--g-spacing-half); + } + + &__schema-popup-json { + margin: 0; + padding: var(--g-spacing-3); + background: var(--g-color-base-generic); + border-radius: var(--g-border-radius-m); + font-size: 12px; + line-height: 1.4; + max-height: min(480px, 60vh); + overflow: auto; + white-space: pre; + } + + &__schema-popup-actions { + display: flex; + gap: var(--g-spacing-2); + justify-content: flex-end; + } + + &__visual { + display: grid; + grid-template-columns: + var(--fb2-palette-w, 220px) + var(--g-spacing-2) + minmax(240px, 1fr) + var(--g-spacing-2) + var(--fb2-inspector-w, 320px); + padding: var(--g-spacing-4) 0; + align-items: stretch; + } + + &__palette, + &__canvas, + &__inspector { + min-width: 0; + } + + &__palette, + &__inspector { + position: sticky; + top: var(--g-spacing-4); + align-self: start; + max-height: calc(100vh - 80px); + overflow-y: auto; + } + + &_compact { + #{$block}__visual { + grid-template-columns: + 56px + minmax(240px, 1fr) + var(--g-spacing-2) + var(--fb2-inspector-w, 320px); + } + + #{$block}__canvas { + padding-left: var(--g-spacing-2); + } + + #{$palette-block} { + padding: 6px; + } + + #{$palette-block}__header { + display: none; + } + + #{$palette-block}__items { + grid-template-columns: 1fr; + } + + #{$palette-block}__tile { + padding: var(--g-spacing-1); + gap: 0; + } + + #{$palette-block}__tile-label { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + } +} diff --git a/src/form-builder-v2/FormBuilderV2.tsx b/src/form-builder-v2/FormBuilderV2.tsx new file mode 100644 index 0000000000..caa5720090 --- /dev/null +++ b/src/form-builder-v2/FormBuilderV2.tsx @@ -0,0 +1,262 @@ +'use client'; + +import * as React from 'react'; + +import type {DragDropEvents} from '@dnd-kit/abstract'; +import type {DragDropManager, Draggable, Droppable} from '@dnd-kit/dom'; +import {move} from '@dnd-kit/helpers'; +import {DragDropProvider, DragOverlay} from '@dnd-kit/react'; +import {Code, Eye, Pencil} from '@gravity-ui/icons'; +import {Button, Icon, SegmentedRadioGroup} from '@gravity-ui/uikit'; + +import type {Content} from '../form-generator-v2/types'; + +import {CanvasContentProvider} from './CanvasContentContext'; +import {Canvas} from './components/Canvas/Canvas'; +import {ContentTab} from './components/ContentTab/ContentTab'; +import {DragOverlayPreview} from './components/DragOverlayPreview/DragOverlayPreview'; +import {Inspector} from './components/Inspector/Inspector'; +import {PALETTE_DRAGGABLE_PREFIX, Palette} from './components/Palette/Palette'; +import {ResizeHandle} from './components/ResizeHandle/ResizeHandle'; +import {SchemaPopup} from './components/SchemaPopup/SchemaPopup'; +import {FormProvider, useFormContext} from './hooks/FormContext'; +import {FormField} from './types'; +import {formBuilderV2Cn} from './utils/cn'; +import {isDropAfter, isPaletteData, isSectionDropData} from './utils/dragData'; +import {applyGroupsMap, buildGroupsMap} from './utils/fieldGroups'; +import {findFieldById} from './utils/fieldTree'; +import {stripIds} from './utils/stripIds'; + +import './FormBuilderV2.scss'; + +const b = formBuilderV2Cn('main'); + +type DndEvents = DragDropEvents<Draggable, Droppable, DragDropManager>; +type DragEndEvent = Parameters<DndEvents['dragend']>[0]; + +type Mode = 'edit' | 'preview'; + +export type FormBuilderDensity = 'full' | 'compact'; + +const PALETTE_MIN = 160; +const PALETTE_MAX = 400; +const PALETTE_DEFAULT = 220; +const INSPECTOR_MIN = 270; +const INSPECTOR_MAX = 560; +const INSPECTOR_DEFAULT = 320; + +interface FormBuilderV2Props { + className?: string; + formFields: FormField[]; + onChange?: (fields: FormField[]) => void; + + density?: FormBuilderDensity; +} + +interface FormBuilderShellProps { + className?: string; + density: FormBuilderDensity; +} + +const FormBuilderShell = ({className, density}: FormBuilderShellProps) => { + const [mode, setMode] = React.useState<Mode>('edit'); + const [canvasContent, setCanvasContent] = React.useState<Content>({}); + const [templateContent, setTemplateContent] = React.useState<Content>({}); + const [paletteWidth, setPaletteWidth] = React.useState(PALETTE_DEFAULT); + const [inspectorWidth, setInspectorWidth] = React.useState(INSPECTOR_DEFAULT); + const [schemaPopupOpen, setSchemaPopupOpen] = React.useState(false); + const schemaButtonRef = React.useRef<HTMLButtonElement | null>(null); + + const isCompact = density === 'compact'; + + const { + formFields, + setAllFields, + addField, + addFieldToSection, + insertFieldBefore, + insertFieldAfter, + moveFieldToSection, + } = useFormContext(); + + const schema = React.useMemo(() => stripIds(formFields), [formFields]); + + const handleDragEnd = React.useCallback( + (event: DragEndEvent) => { + if (event.canceled) return; + const {source, target} = event.operation; + if (!source) return; + + const paletteData = isPaletteData(source.data) ? source.data : null; + const isPaletteDrag = + paletteData !== null || String(source.id).startsWith(PALETTE_DRAGGABLE_PREFIX); + + if (isPaletteDrag) { + const type = paletteData?.type; + if (!type) return; + if (!target) { + addField(type); + return; + } + + if (isSectionDropData(target.data)) { + addFieldToSection(target.data.sectionId, type); + return; + } + + if (String(target.id) === String(source.id)) { + addField(type); + return; + } + + const dropAfter = isDropAfter( + event.operation.position?.current?.y, + target.shape?.center?.y, + ); + if (dropAfter) { + insertFieldAfter(String(target.id), type); + } else { + insertFieldBefore(String(target.id), type); + } + return; + } + + if (target && isSectionDropData(target.data)) { + moveFieldToSection(String(source.id), target.data.sectionId); + return; + } + + const groups = buildGroupsMap(formFields); + const nextGroups = move(groups, event); + const nextFields = applyGroupsMap(nextGroups); + setAllFields(nextFields); + }, + [ + addField, + addFieldToSection, + formFields, + insertFieldBefore, + insertFieldAfter, + moveFieldToSection, + setAllFields, + ], + ); + + const gridStyle = { + '--fb2-palette-w': `${paletteWidth}px`, + '--fb2-inspector-w': `${inspectorWidth}px`, + } as React.CSSProperties; + + return ( + <div className={b({compact: density === 'compact'}, className)}> + <CanvasContentProvider + content={canvasContent} + setContent={setCanvasContent} + templateContent={templateContent} + setTemplateContent={setTemplateContent} + > + <div className={b('toolbar')}> + <SegmentedRadioGroup<Mode> + size="m" + value={mode} + onUpdate={setMode} + options={[ + { + value: 'edit', + content: ( + <span className={b('toolbar-option')}> + <Icon data={Pencil} size={14} /> + Edit + </span> + ), + }, + { + value: 'preview', + content: ( + <span className={b('toolbar-option')}> + <Icon data={Eye} size={14} /> + Preview + </span> + ), + }, + ]} + /> + <Button + ref={schemaButtonRef} + view="outlined" + size="m" + onClick={() => setSchemaPopupOpen((prev) => !prev)} + > + <Icon data={Code} size={14} /> + Schema + </Button> + <SchemaPopup + schema={schema} + onApply={setAllFields} + open={schemaPopupOpen} + onOpenChange={setSchemaPopupOpen} + anchorElement={schemaButtonRef.current} + /> + </div> + + {mode === 'edit' ? ( + <DragDropProvider onDragEnd={handleDragEnd}> + <div className={b('visual')} style={gridStyle}> + <aside className={b('palette')}> + <Palette /> + </aside> + {!isCompact && ( + <ResizeHandle + value={paletteWidth} + min={PALETTE_MIN} + max={PALETTE_MAX} + direction="left" + onChange={setPaletteWidth} + /> + )} + <main className={b('canvas')}> + <Canvas /> + </main> + <ResizeHandle + value={inspectorWidth} + min={INSPECTOR_MIN} + max={INSPECTOR_MAX} + direction="right" + onChange={setInspectorWidth} + /> + <aside className={b('inspector')}> + <Inspector /> + </aside> + </div> + <DragOverlay dropAnimation={null}> + {(source) => { + if (!source) return null; + if (isPaletteData(source.data)) { + return <DragOverlayPreview type={source.data.type} />; + } + const field = findFieldById(formFields, String(source.id)); + if (!field) return null; + return <DragOverlayPreview type={field.type} field={field} />; + }} + </DragOverlay> + </DragDropProvider> + ) : ( + <ContentTab /> + )} + </CanvasContentProvider> + </div> + ); +}; + +export const FormBuilderV2 = ({ + className, + formFields, + onChange, + density = 'full', +}: FormBuilderV2Props) => { + return ( + <FormProvider formFields={formFields} onChange={onChange}> + <FormBuilderShell className={className} density={density} /> + </FormProvider> + ); +}; diff --git a/src/form-builder-v2/__stories__/FormBuilderV2.stories.tsx b/src/form-builder-v2/__stories__/FormBuilderV2.stories.tsx new file mode 100644 index 0000000000..377b418427 --- /dev/null +++ b/src/form-builder-v2/__stories__/FormBuilderV2.stories.tsx @@ -0,0 +1,108 @@ +import * as React from 'react'; + +import {ThemeProvider} from '@gravity-ui/uikit'; +import {Meta, StoryFn} from '@storybook/react'; + +import {FormBuilderDensity, FormBuilderV2} from '../FormBuilderV2'; +import {FormField} from '../types'; + +export default { + title: 'FormBuilder/v2', + component: FormBuilderV2, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: + 'FormBuilderV2 — visual editor for FormGenerator v2 schemas. ' + + 'Two density modes: `full` (default — roomy 220px palette with ' + + 'labeled tiles, palette is user-resizable) and `compact` (narrow ' + + 'icon-only strip for embedding into limited-width host UI). ' + + 'Inspector and canvas behave identically in both — no drawer, ' + + 'no overlap.', + }, + }, + }, +} as Meta<typeof FormBuilderV2>; + +const SAMPLE_FIELDS: FormField[] = [ + {id: 'fb2_1', type: 'textInput', name: 'shipName', title: 'Ship name'}, + {id: 'fb2_2', type: 'textArea', name: 'missionLog', title: 'Mission log'}, + { + id: 'fb2_3', + type: 'segmentedRadioGroup', + name: 'shipClass', + title: 'Ship class', + options: [ + {value: 'ranger', content: 'Ranger'}, + {value: 'lander', content: 'Lander'}, + {value: 'station', content: 'Station'}, + ], + }, + {id: 'fb2_4', type: 'switch', name: 'armed', title: 'Armed'}, + {id: 'fb2_5', type: 'colorInput', name: 'hullColor', title: 'Hull color'}, +]; + +interface TemplateArgs { + initialFields: FormField[]; + density?: FormBuilderDensity; +} + +const Template: StoryFn<TemplateArgs> = ({initialFields, density}) => { + const [fields, setFields] = React.useState<FormField[]>(initialFields); + + return ( + <ThemeProvider theme="light"> + <div + style={{ + padding: 24, + minHeight: '100vh', + background: 'var(--g-color-base-background)', + boxSizing: 'border-box', + }} + > + <FormBuilderV2 formFields={fields} onChange={setFields} density={density} /> + </div> + </ThemeProvider> + ); +}; + +export const Full = Template.bind({}); +Full.args = { + initialFields: SAMPLE_FIELDS, + density: 'full', +}; +Full.parameters = { + docs: { + description: { + story: + 'Default mode. Palette is 220px wide with two columns of labeled ' + + 'square tiles, and is user-resizable via the bar between palette ' + + 'and canvas. Roomy variant for standalone usage where there is ' + + 'plenty of horizontal space.', + }, + }, +}; + +export const Compact = Template.bind({}); +Compact.args = { + initialFields: SAMPLE_FIELDS, + density: 'compact', +}; +Compact.parameters = { + docs: { + description: { + story: + 'Narrow palette mode (`density="compact"`). Palette shrinks to a ' + + '~56px icon-only strip; labels are visually hidden but kept for ' + + 'screen readers and shown as hover tooltips. Use when embedding ' + + 'the builder into a sidebar, modal, or settings tab on a host site.', + }, + }, +}; + +export const Empty = Template.bind({}); +Empty.args = { + initialFields: [], + density: 'full', +}; diff --git a/src/form-builder-v2/components/Canvas/Canvas.scss b/src/form-builder-v2/components/Canvas/Canvas.scss new file mode 100644 index 0000000000..47c64a97bd --- /dev/null +++ b/src/form-builder-v2/components/Canvas/Canvas.scss @@ -0,0 +1,21 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder-v2}canvas'; + +#{$block} { + padding: var(--g-spacing-5); + min-height: 400px; + + &__list { + display: flex; + flex-direction: column; + gap: var(--g-spacing-3); + } + + &__empty { + padding: var(--g-spacing-10) var(--g-spacing-5); + text-align: center; + border: 1px dashed var(--g-color-line-generic); + border-radius: var(--g-border-radius-l); + } +} diff --git a/src/form-builder-v2/components/Canvas/Canvas.tsx b/src/form-builder-v2/components/Canvas/Canvas.tsx new file mode 100644 index 0000000000..9fa844db90 --- /dev/null +++ b/src/form-builder-v2/components/Canvas/Canvas.tsx @@ -0,0 +1,43 @@ +import {Card, Text} from '@gravity-ui/uikit'; + +import {useFormContext} from '../../hooks/FormContext'; +import {FormField} from '../../types'; +import {formBuilderV2Cn} from '../../utils/cn'; +import {CanvasCard} from '../CanvasCard/CanvasCard'; + +import './Canvas.scss'; + +const b = formBuilderV2Cn('canvas'); + +interface CanvasListProps { + fields: FormField[]; + parentGroup: string; +} + +const CanvasList = ({fields, parentGroup}: CanvasListProps) => ( + <div className={b('list')}> + {fields.map((field, index) => ( + <CanvasCard key={field.id} field={field} index={index} group={parentGroup} /> + ))} + </div> +); + +export const Canvas = () => { + const {formFields, selectField} = useFormContext(); + + return ( + <Card className={b()} view="outlined" onClick={() => selectField(null)}> + {formFields.length === 0 ? ( + <div className={b('empty')}> + <Text variant="body-2" color="hint"> + Canvas is empty. Click a field type on the left to add it. + </Text> + </div> + ) : ( + <CanvasList fields={formFields} parentGroup="root" /> + )} + </Card> + ); +}; + +export {CanvasList}; diff --git a/src/form-builder-v2/components/CanvasCard/CanvasCard.scss b/src/form-builder-v2/components/CanvasCard/CanvasCard.scss new file mode 100644 index 0000000000..a7c424c040 --- /dev/null +++ b/src/form-builder-v2/components/CanvasCard/CanvasCard.scss @@ -0,0 +1,168 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder-v2}canvas-card'; + +#{$block} { + position: relative; + padding: var(--g-spacing-3) var(--g-spacing-3); + border: 1px solid transparent; + border-radius: var(--g-border-radius-l); + cursor: pointer; + transition: + background-color 0.15s ease, + border-color 0.15s ease, + opacity 0.15s ease; + + &:hover { + background-color: var(--g-color-base-simple-hover); + } + + &:hover > #{$block}__controls { + opacity: 1; + } + + &_selected { + border-color: var(--g-color-line-brand); + background-color: var(--g-color-base-selection); + + #{$block}__controls { + opacity: 1; + } + } + + &_dragging { + opacity: 0.4; + } + + &_drop-target, + &_drop-target-after { + position: relative; + + &::before { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 3px; + background: var(--g-color-line-brand); + border-radius: 2px; + opacity: 0.45; + pointer-events: none; + } + } + + &_drop-target::before { + top: calc((var(--g-spacing-3) + 3px) / -2 - 1px); + } + + &_drop-target-after::before { + bottom: calc((var(--g-spacing-3) + 3px) / -2 - 1px); + } + + &__visibility-badge { + margin-bottom: var(--g-spacing-1); + } + + &__controls { + position: absolute; + top: var(--g-spacing-1); + right: var(--g-spacing-2); + display: flex; + align-items: center; + gap: var(--g-spacing-half); + opacity: 0; + transition: opacity 0.15s ease; + } + + &__control { + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--g-spacing-5); + height: var(--g-spacing-5); + padding: 0; + border: none; + background: transparent; + color: var(--g-color-text-secondary); + cursor: grab; + border-radius: var(--g-border-radius-s); + + &:hover { + background-color: var(--g-color-base-simple-hover-solid); + color: var(--g-color-text-primary); + } + + &:active { + cursor: grabbing; + } + } + + &__labeled { + display: flex; + flex-direction: column; + gap: var(--g-spacing-1); + } + + &__labeled-title { + display: flex; + align-items: baseline; + gap: var(--g-spacing-2); + } + + &__section-header { + display: flex; + align-items: center; + gap: var(--g-spacing-2); + padding-bottom: var(--g-spacing-1); + border-bottom: 1px solid var(--g-color-line-generic); + } + + &__children { + margin-top: var(--g-spacing-2); + padding-left: var(--g-spacing-3); + border-left: 2px solid var(--g-color-line-generic); + display: flex; + flex-direction: column; + gap: var(--g-spacing-2); + border-radius: 0 var(--g-border-radius-m) var(--g-border-radius-m) 0; + transition: + border-color 0.12s ease, + background-color 0.12s ease; + + &_drop-target { + border-left-color: var(--g-color-line-brand); + background-color: var(--g-color-base-selection); + } + + #{$block}_drop-target::before { + top: calc((var(--g-spacing-2) + 3px) / -2 - 1px); + } + + #{$block}_drop-target-after::before { + bottom: calc((var(--g-spacing-2) + 3px) / -2 - 1px); + } + } + + &__static-text { + padding: var(--g-spacing-2) 10px; + border-radius: var(--g-border-radius-s); + } + + &__color-swatch { + display: flex; + align-items: center; + gap: var(--g-spacing-2); + } + + &__color-swatch-chip { + width: var(--g-spacing-6); + height: var(--g-spacing-6); + border: 1px solid var(--g-color-line-generic); + border-radius: var(--g-border-radius-s); + } + + &__color-swatch-value { + font-size: var(--g-text-body-1-font-size); + color: var(--g-color-text-secondary); + } +} diff --git a/src/form-builder-v2/components/CanvasCard/CanvasCard.tsx b/src/form-builder-v2/components/CanvasCard/CanvasCard.tsx new file mode 100644 index 0000000000..8ae6159851 --- /dev/null +++ b/src/form-builder-v2/components/CanvasCard/CanvasCard.tsx @@ -0,0 +1,144 @@ +import * as React from 'react'; + +import {CollisionPriority} from '@dnd-kit/abstract'; +import {SortableKeyboardPlugin, isSortable} from '@dnd-kit/dom/sortable'; +import {useSortable} from '@dnd-kit/react/sortable'; +import {Copy, Grip, TrashBin} from '@gravity-ui/icons'; +import {Button, Icon, Label} from '@gravity-ui/uikit'; + +import {useFormContext} from '../../hooks/FormContext'; +import {FormField} from '../../types'; +import {formBuilderV2Cn} from '../../utils/cn'; +import type {CardDragData} from '../../utils/dragData'; +import {isDropAfter, isPaletteData} from '../../utils/dragData'; + +import {FieldPreview} from './components/FieldPreview'; +import {SectionChildrenDropZone, SectionDropData} from './components/SectionChildrenDropZone'; + +import './CanvasCard.scss'; + +export type {SectionDropData}; + +const b = formBuilderV2Cn('canvas-card'); + +interface CanvasCardProps { + field: FormField; + index: number; + group: string; +} + +export const CanvasCard = ({field, index, group}: CanvasCardProps) => { + const {selectedFieldId, selectField, removeField, duplicateField} = useFormContext(); + + const isSelected = selectedFieldId === field.id; + const hasWhen = 'when' in field && Array.isArray(field.when) && field.when.length > 0; + + const cardData: CardDragData = React.useMemo(() => ({kind: 'card', group}), [group]); + + const {ref, handleRef, isDragSource, isDropTarget, sortable} = useSortable({ + id: field.id, + index, + group, + data: cardData, + transition: null, + plugins: [SortableKeyboardPlugin], + collisionPriority: field.type === 'section' ? CollisionPriority.Low : undefined, + alignment: { + x: 'start', + y: 'center', + }, + }); + + const dragOp = sortable.manager?.dragOperation; + const source = dragOp?.source; + const isPaletteSource = isPaletteData(source?.data); + const sourceIndex = source && isSortable(source) ? source.index : undefined; + const pointerY = dragOp?.position?.current?.y; + const targetCenterY = sortable.droppable?.shape?.center?.y; + + const dropAfter = + isDropTarget && + !isDragSource && + (isPaletteSource + ? isDropAfter(pointerY, targetCenterY) + : sourceIndex !== undefined && sourceIndex < index); + + const handleClick = (event: React.MouseEvent) => { + event.stopPropagation(); + selectField(field.id); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + event.stopPropagation(); + selectField(field.id); + } + }; + + return ( + <div + ref={ref} + className={b({ + selected: isSelected, + dragging: isDragSource, + 'drop-target': isDropTarget && !isDragSource && !dropAfter, + 'drop-target-after': dropAfter, + })} + onClick={handleClick} + onKeyDown={handleKeyDown} + role="button" + tabIndex={0} + aria-pressed={isSelected} + > + {hasWhen && ( + <div className={b('visibility-badge')}> + <Label theme="info" size="s"> + Visibility by condition + </Label> + </div> + )} + <FieldPreview field={field} /> + + <div className={b('controls')}> + <Button + ref={handleRef} + view="flat" + size="xs" + className={b('control')} + aria-label="Drag to reorder" + title="Drag to reorder" + onClick={(event) => event.stopPropagation()} + > + <Icon data={Grip} size={12} /> + </Button> + <Button + view="flat" + size="xs" + onClick={(event) => { + event.stopPropagation(); + duplicateField(field.id); + }} + title="Duplicate" + > + <Icon data={Copy} size={12} /> + </Button> + <Button + view="flat-danger" + size="xs" + onClick={(event) => { + event.stopPropagation(); + removeField(field.id); + }} + title="Remove" + > + <Icon data={TrashBin} size={12} /> + </Button> + </div> + + {field.type === 'section' && ( + <SectionChildrenDropZone sectionId={field.id} fields={field.fields} /> + )} + </div> + ); +}; diff --git a/src/form-builder-v2/components/CanvasCard/components/FieldPreview.tsx b/src/form-builder-v2/components/CanvasCard/components/FieldPreview.tsx new file mode 100644 index 0000000000..2df3021e4b --- /dev/null +++ b/src/form-builder-v2/components/CanvasCard/components/FieldPreview.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; + +import {Label, Text} from '@gravity-ui/uikit'; +import {cloneDeep, set, unset} from 'lodash'; + +import {componentMap} from '../../../../form-generator-v2/components/constants'; +import type {Content, OnUpdate} from '../../../../form-generator-v2/types'; +import {useCanvasContent} from '../../../CanvasContentContext'; +import {FormField} from '../../../types'; +import {formBuilderV2Cn} from '../../../utils/cn'; + +const b = formBuilderV2Cn('canvas-card'); + +const isTemplateField = (field: FormField): boolean => + 'name' in field && typeof field.name === 'string' && field.name.includes('{{index}}'); + +const makeOnUpdate = + (setContent: React.Dispatch<React.SetStateAction<Content>>): OnUpdate => + (key, value, options) => { + setContent((prev) => { + const next = cloneDeep(prev); + if (options?.unset) { + unset(next, key); + } else { + set(next, key, value); + } + return next; + }); + }; + +interface FieldPreviewProps { + field: FormField; +} + +export const FieldPreview = ({field}: FieldPreviewProps) => { + const {content, setContent, templateContent, setTemplateContent} = useCanvasContent(); + + const onUpdateForContent = React.useMemo<OnUpdate>( + () => makeOnUpdate(setContent), + [setContent], + ); + const onUpdateForTemplate = React.useMemo<OnUpdate>( + () => makeOnUpdate(setTemplateContent), + [setTemplateContent], + ); + + if (field.type === 'section') { + return ( + <div className={b('section-header')}> + <Text variant="subheader-2">{field.title || 'Section'}</Text> + {field.index ? ( + <Label theme="info" size="s"> + Array · {field.itemTitle ?? 'Item {{index}}'} + </Label> + ) : ( + <Label theme="unknown" size="s"> + Group + </Label> + )} + </div> + ); + } + + const Component = componentMap[field.type] as + | React.ComponentType<Record<string, unknown>> + | undefined; + if (!Component) { + return null; + } + + const {when: _when, id: _id, ...fieldProps} = field; + + const useTemplate = isTemplateField(field); + const widgetContent = useTemplate ? templateContent : content; + const onUpdate = useTemplate ? onUpdateForTemplate : onUpdateForContent; + + return ( + <Component + {...(fieldProps as Record<string, unknown>)} + content={widgetContent} + onUpdate={onUpdate} + /> + ); +}; diff --git a/src/form-builder-v2/components/CanvasCard/components/SectionChildrenDropZone.tsx b/src/form-builder-v2/components/CanvasCard/components/SectionChildrenDropZone.tsx new file mode 100644 index 0000000000..59916a73f4 --- /dev/null +++ b/src/form-builder-v2/components/CanvasCard/components/SectionChildrenDropZone.tsx @@ -0,0 +1,39 @@ +import {useDroppable} from '@dnd-kit/react'; +import {Text} from '@gravity-ui/uikit'; + +import {FormField} from '../../../types'; +import {formBuilderV2Cn} from '../../../utils/cn'; +import type {SectionDropData} from '../../../utils/dragData'; +import {CanvasList} from '../../Canvas/Canvas'; + +const b = formBuilderV2Cn('canvas-card'); + +export const SECTION_DROP_PREFIX = 'section-drop:'; + +export type {SectionDropData}; + +interface SectionChildrenDropZoneProps { + sectionId: string; + fields: FormField[]; +} + +export const SectionChildrenDropZone = ({sectionId, fields}: SectionChildrenDropZoneProps) => { + const {ref, isDropTarget} = useDroppable({ + id: `${SECTION_DROP_PREFIX}${sectionId}`, + data: {kind: 'section-drop', sectionId}, + }); + + const className = b('children'); + + return ( + <div ref={ref} className={`${className}${isDropTarget ? ` ${className}_drop-target` : ''}`}> + <CanvasList fields={fields} parentGroup={`section:${sectionId}`} /> + {fields.length === 0 && ( + <Text variant="body-2" color="hint"> + Empty section. Drag a tile from the palette into this box, or select the section + first and click a tile to add it inside. + </Text> + )} + </div> + ); +}; diff --git a/src/form-builder-v2/components/ContentTab/ContentTab.scss b/src/form-builder-v2/components/ContentTab/ContentTab.scss new file mode 100644 index 0000000000..d88b2f3808 --- /dev/null +++ b/src/form-builder-v2/components/ContentTab/ContentTab.scss @@ -0,0 +1,29 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder-v2}content-tab'; + +#{$block} { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: var(--g-spacing-4); + margin-top: var(--g-spacing-4); + + &__column { + padding: var(--g-spacing-4); + } + + &__title { + display: block; + margin-bottom: var(--g-spacing-3); + } + + &__json { + margin: 0; + padding: var(--g-spacing-3); + background: var(--g-color-base-generic); + border-radius: var(--g-border-radius-m); + font-size: 12px; + overflow: auto; + max-height: 70vh; + } +} diff --git a/src/form-builder-v2/components/ContentTab/ContentTab.tsx b/src/form-builder-v2/components/ContentTab/ContentTab.tsx new file mode 100644 index 0000000000..46a6381101 --- /dev/null +++ b/src/form-builder-v2/components/ContentTab/ContentTab.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; + +import {Card, Text} from '@gravity-ui/uikit'; + +import FormGenerator from '../../../form-generator-v2/FormGenerator'; +import {useCanvasContent} from '../../CanvasContentContext'; +import {useFormContext} from '../../hooks/FormContext'; +import {formBuilderV2Cn} from '../../utils/cn'; +import {stripIds} from '../../utils/stripIds'; + +import './ContentTab.scss'; + +const b = formBuilderV2Cn('content-tab'); + +export const ContentTab = () => { + const {formFields} = useFormContext(); + const {content, setContent} = useCanvasContent(); + + const schema = React.useMemo(() => stripIds(formFields), [formFields]); + + return ( + <div className={b()}> + <Card className={b('column')} view="outlined"> + <Text variant="subheader-2" className={b('title')}> + Live form + </Text> + {schema.length > 0 ? ( + <FormGenerator + blockConfig={schema} + contentConfig={content} + onUpdate={setContent} + /> + ) : ( + <Text variant="body-2" color="hint"> + Add fields on the Visual tab to see the form here. + </Text> + )} + </Card> + <Card className={b('column')} view="outlined"> + <Text variant="subheader-2" className={b('title')}> + Content JSON + </Text> + <pre className={b('json')}>{JSON.stringify(content, null, 2)}</pre> + </Card> + </div> + ); +}; diff --git a/src/form-builder-v2/components/DragOverlayPreview/DragOverlayPreview.scss b/src/form-builder-v2/components/DragOverlayPreview/DragOverlayPreview.scss new file mode 100644 index 0000000000..a80135a317 --- /dev/null +++ b/src/form-builder-v2/components/DragOverlayPreview/DragOverlayPreview.scss @@ -0,0 +1,18 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder-v2}drag-overlay'; + +#{$block} { + display: inline-flex; + align-items: center; + gap: var(--g-spacing-2); + padding: var(--g-spacing-2) var(--g-spacing-3); + color: var(--g-color-text-primary); + cursor: grabbing; + + &__text { + display: flex; + align-items: center; + gap: 6px; + } +} diff --git a/src/form-builder-v2/components/DragOverlayPreview/DragOverlayPreview.tsx b/src/form-builder-v2/components/DragOverlayPreview/DragOverlayPreview.tsx new file mode 100644 index 0000000000..b7a965c1a3 --- /dev/null +++ b/src/form-builder-v2/components/DragOverlayPreview/DragOverlayPreview.tsx @@ -0,0 +1,40 @@ +import {Card, Icon, Label, Text} from '@gravity-ui/uikit'; + +import type {BuilderFieldType, FormField} from '../../types'; +import {formBuilderV2Cn} from '../../utils/cn'; +import {TYPE_ICONS, TYPE_LABELS} from '../../utils/fieldMeta'; + +import './DragOverlayPreview.scss'; + +const b = formBuilderV2Cn('drag-overlay'); + +interface DragOverlayPreviewProps { + type: BuilderFieldType; + field?: FormField; +} + +const getFieldTitle = (f: FormField | undefined): string => { + if (!f) return ''; + if ('title' in f) return f.title ?? ''; + if ('text' in f) return f.text ?? ''; + return ''; +}; + +export const DragOverlayPreview = ({type, field}: DragOverlayPreviewProps) => { + const name = field && 'name' in field ? field.name : ''; + const title = getFieldTitle(field); + + return ( + <Card className={b()} view="raised"> + <Icon data={TYPE_ICONS[type]} size={14} /> + <div className={b('text')}> + <Text variant="body-2">{TYPE_LABELS[type]}</Text> + {(name || title) && ( + <Label theme="unknown" size="s"> + {name || title} + </Label> + )} + </div> + </Card> + ); +}; diff --git a/src/form-builder-v2/components/FieldSettings/FieldSettings.scss b/src/form-builder-v2/components/FieldSettings/FieldSettings.scss new file mode 100644 index 0000000000..6aa1116bb2 --- /dev/null +++ b/src/form-builder-v2/components/FieldSettings/FieldSettings.scss @@ -0,0 +1,40 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder-v2}field-settings'; + +#{$block} { + &__row { + display: grid; + grid-template-columns: 100px 1fr; + align-items: start; + gap: var(--g-spacing-2); + } + + &__row-label { + padding-top: 6px; + font-size: var(--g-text-body-1-font-size); + line-height: 1.4; + color: var(--g-color-text-secondary); + } + + &__options { + display: flex; + flex-direction: column; + gap: 6px; + padding: var(--g-spacing-2); + border: 1px solid var(--g-color-line-generic); + border-radius: var(--g-border-radius-m); + } + + &__option-row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; + gap: 6px; + align-items: center; + } + + &__add-option { + align-self: flex-start; + margin-top: var(--g-spacing-1); + } +} diff --git a/src/form-builder-v2/components/FieldSettings/FieldSettings.tsx b/src/form-builder-v2/components/FieldSettings/FieldSettings.tsx new file mode 100644 index 0000000000..894411d938 --- /dev/null +++ b/src/form-builder-v2/components/FieldSettings/FieldSettings.tsx @@ -0,0 +1,134 @@ +import * as React from 'react'; + +import {TextInput} from '@gravity-ui/uikit'; + +import type {When} from '../../../form-generator-v2/types'; +import {useFormContext} from '../../hooks/FormContext'; +import {FormField} from '../../types'; +import {collectNames} from '../../utils/fieldNames'; +import {WhenEditor} from '../WhenEditor/WhenEditor'; + +import {ColorInputSettings} from './fields/ColorInputSettings'; +import {OptionsSettings} from './fields/OptionsSettings'; +import {Row} from './fields/Row'; +import {SectionSettings} from './fields/SectionSettings'; +import {SwitchSettings} from './fields/SwitchSettings'; +import {TextFieldSettings} from './fields/TextFieldSettings'; +import {TextSettings} from './fields/TextSettings'; + +import './FieldSettings.scss'; + +interface FieldSettingsProps { + field: FormField; +} + +export const FieldSettings = ({field}: FieldSettingsProps) => { + const {updateField, formFields} = useFormContext(); + + const availableFields = React.useMemo(() => { + const all = Array.from(collectNames(formFields)); + if (field.type === 'section' || field.type === 'text' || field.type === 'divider') { + return all; + } + return all.filter((n) => n !== field.name); + }, [formFields, field]); + + const whenEditorSection = ( + <Row label="Visible when"> + <WhenEditor + when={field.when} + availableFields={availableFields} + onChange={(next: When | undefined) => updateField(field.id, {when: next})} + /> + </Row> + ); + + const commonRows = ( + <React.Fragment> + <Row label="Title"> + <TextInput + value={'title' in field ? (field.title ?? '') : ''} + onUpdate={(value) => updateField(field.id, {title: value})} + placeholder="Label shown above the field" + /> + </Row> + <Row label="Name"> + <TextInput + value={'name' in field ? (field.name ?? '') : ''} + onUpdate={(value) => updateField(field.id, {name: value})} + placeholder="Path in content object" + /> + </Row> + </React.Fragment> + ); + + if (field.type === 'section') { + return ( + <SectionSettings + field={field} + updateField={updateField} + whenEditorSection={whenEditorSection} + /> + ); + } + + if (field.type === 'text') { + return ( + <TextSettings + field={field} + updateField={updateField} + whenEditorSection={whenEditorSection} + /> + ); + } + + if (field.type === 'divider') { + return whenEditorSection; + } + + if (field.type === 'textInput' || field.type === 'textArea') { + return ( + <TextFieldSettings + field={field} + updateField={updateField} + commonRows={commonRows} + whenEditorSection={whenEditorSection} + /> + ); + } + + if (field.type === 'switch') { + return ( + <SwitchSettings + field={field} + updateField={updateField} + commonRows={commonRows} + whenEditorSection={whenEditorSection} + /> + ); + } + + if (field.type === 'colorInput') { + return ( + <ColorInputSettings + field={field} + updateField={updateField} + commonRows={commonRows} + whenEditorSection={whenEditorSection} + /> + ); + } + + if (field.type === 'select' || field.type === 'segmentedRadioGroup') { + return ( + <OptionsSettings + field={field} + updateField={updateField} + commonRows={commonRows} + whenEditorSection={whenEditorSection} + /> + ); + } + + return null; +}; diff --git a/src/form-builder-v2/components/FieldSettings/fields/ColorInputSettings.tsx b/src/form-builder-v2/components/FieldSettings/fields/ColorInputSettings.tsx new file mode 100644 index 0000000000..f3de186ca3 --- /dev/null +++ b/src/form-builder-v2/components/FieldSettings/fields/ColorInputSettings.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; + +import {TextInput} from '@gravity-ui/uikit'; + +import type {BuilderLeafField, FieldUpdate} from '../../../types'; + +import {Row} from './Row'; + +interface ColorInputSettingsProps { + field: BuilderLeafField & {type: 'colorInput'}; + updateField: (id: string, updates: FieldUpdate) => void; + commonRows: React.ReactNode; + whenEditorSection: React.ReactNode; +} + +export const ColorInputSettings = ({ + field, + updateField, + commonRows, + whenEditorSection, +}: ColorInputSettingsProps) => ( + <React.Fragment> + {commonRows} + <Row label="Default"> + <TextInput + value={field.defaultValue ?? ''} + onUpdate={(value) => updateField(field.id, {defaultValue: value || undefined})} + placeholder="#000000" + /> + </Row> + {whenEditorSection} + </React.Fragment> +); diff --git a/src/form-builder-v2/components/FieldSettings/fields/OptionsSettings.tsx b/src/form-builder-v2/components/FieldSettings/fields/OptionsSettings.tsx new file mode 100644 index 0000000000..f67414f82a --- /dev/null +++ b/src/form-builder-v2/components/FieldSettings/fields/OptionsSettings.tsx @@ -0,0 +1,109 @@ +import * as React from 'react'; + +import {TrashBin} from '@gravity-ui/icons'; +import {Button, Checkbox, Icon, Select, TextInput} from '@gravity-ui/uikit'; + +import type {BuilderLeafField, FieldUpdate} from '../../../types'; +import {formBuilderV2Cn} from '../../../utils/cn'; + +import {Row} from './Row'; + +const b = formBuilderV2Cn('field-settings'); + +interface OptionsSettingsProps { + field: BuilderLeafField & {type: 'select' | 'segmentedRadioGroup'}; + updateField: (id: string, updates: FieldUpdate) => void; + commonRows: React.ReactNode; + whenEditorSection: React.ReactNode; +} + +export const OptionsSettings = ({ + field, + updateField, + commonRows, + whenEditorSection, +}: OptionsSettingsProps) => { + const options = field.options ?? []; + + const updateOption = (index: number, key: 'value' | 'content', value: string) => { + const next = options.map((opt, i) => (i === index ? {...opt, [key]: value} : opt)); + updateField(field.id, {options: next}); + }; + + const removeOption = (index: number) => { + const next = options.filter((_, i) => i !== index); + updateField(field.id, {options: next}); + }; + + const addOption = () => { + const nextIndex = options.length + 1; + updateField(field.id, { + options: [...options, {value: `option${nextIndex}`, content: `Option ${nextIndex}`}], + }); + }; + + const defaultValueOptions = [ + {value: '', content: '— none —'}, + ...options.map((o) => ({value: o.value, content: o.content ?? o.value})), + ]; + + return ( + <React.Fragment> + {commonRows} + <Row label="Default"> + <Select + size="m" + value={[field.defaultValue ?? '']} + options={defaultValueOptions} + onUpdate={(next) => { + const value = next[0]; + updateField(field.id, {defaultValue: value || undefined}); + }} + /> + </Row> + {field.type === 'select' && ( + <Row label="Has clear"> + <Checkbox + checked={Boolean(field.hasClear)} + onUpdate={(value) => updateField(field.id, {hasClear: value})} + > + Allow clearing the selection + </Checkbox> + </Row> + )} + <Row label="Options"> + <div className={b('options')}> + {options.map((opt, index) => ( + <div key={index} className={b('option-row')}> + <TextInput + size="s" + value={opt.value} + onUpdate={(value) => updateOption(index, 'value', value)} + placeholder="value" + /> + <TextInput + size="s" + value={opt.content ?? ''} + onUpdate={(value) => updateOption(index, 'content', value)} + placeholder="label" + /> + <Button + size="s" + view="flat-danger" + disabled={options.length <= 1} + onClick={() => removeOption(index)} + title="Remove option" + > + <Icon data={TrashBin} size={12} /> + </Button> + </div> + ))} + <Button className={b('add-option')} size="s" view="normal" onClick={addOption}> + + Add option + </Button> + </div> + </Row> + {whenEditorSection} + </React.Fragment> + ); +}; diff --git a/src/form-builder-v2/components/FieldSettings/fields/Row.tsx b/src/form-builder-v2/components/FieldSettings/fields/Row.tsx new file mode 100644 index 0000000000..e184b166f3 --- /dev/null +++ b/src/form-builder-v2/components/FieldSettings/fields/Row.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; + +import {formBuilderV2Cn} from '../../../utils/cn'; + +const b = formBuilderV2Cn('field-settings'); + +interface RowProps { + label: string; + children: React.ReactNode; +} + +export const Row = ({label, children}: RowProps) => ( + <div className={b('row')}> + <span className={b('row-label')}>{label}</span> + <div>{children}</div> + </div> +); diff --git a/src/form-builder-v2/components/FieldSettings/fields/SectionSettings.tsx b/src/form-builder-v2/components/FieldSettings/fields/SectionSettings.tsx new file mode 100644 index 0000000000..d94e0e58dc --- /dev/null +++ b/src/form-builder-v2/components/FieldSettings/fields/SectionSettings.tsx @@ -0,0 +1,118 @@ +import * as React from 'react'; + +import {Checkbox, Select, Switch, Text, TextInput} from '@gravity-ui/uikit'; + +import type {BuilderSectionField, FieldUpdate} from '../../../types'; +import { + prefixNameForArrayMode, + stripArrayModePrefix, + transformChildNames, +} from '../../../utils/fieldNames'; + +import {Row} from './Row'; + +interface SectionSettingsProps { + field: BuilderSectionField; + updateField: (id: string, updates: FieldUpdate) => void; + whenEditorSection: React.ReactNode; +} + +export const SectionSettings = ({field, updateField, whenEditorSection}: SectionSettingsProps) => { + const isArray = Boolean(field.index); + + const handleArrayModeToggle = (value: boolean) => { + if (value) { + updateField(field.id, { + index: 'index', + withAddButton: true, + itemTitle: 'Item {{index}}', + itemView: 'card', + fields: transformChildNames(field.fields, prefixNameForArrayMode), + }); + } else { + updateField(field.id, { + index: undefined, + withAddButton: undefined, + itemTitle: undefined, + itemView: undefined, + fields: transformChildNames(field.fields, stripArrayModePrefix), + }); + } + }; + + return ( + <React.Fragment> + <Row label="Title"> + <TextInput + value={field.title ?? ''} + onUpdate={(value) => updateField(field.id, {title: value})} + placeholder="Section heading" + /> + </Row> + <Row label="Opened"> + <Switch + checked={Boolean(field.opened)} + onUpdate={(value) => updateField(field.id, {opened: value})} + > + Expanded by default + </Switch> + </Row> + <Row label="Array mode"> + <Switch checked={isArray} onUpdate={handleArrayModeToggle}> + Repeating group (array of items) + </Switch> + </Row> + {isArray && ( + <React.Fragment> + <Row label="Hint"> + <Text variant="caption-2" color="hint"> + Child field names must include <code>{'{{index}}'}</code> so each row + gets its own value. New fields get an <code>{'items[{{index}}].'}</code>{' '} + prefix automatically. + </Text> + </Row> + <Row label="Index name"> + <TextInput + value={field.index ?? ''} + onUpdate={(value) => updateField(field.id, {index: value || 'index'})} + placeholder="index" + /> + </Row> + <Row label="Item title"> + <TextInput + value={field.itemTitle ?? ''} + onUpdate={(value) => + updateField(field.id, {itemTitle: value || undefined}) + } + placeholder="Item {{index}}" + /> + </Row> + <Row label="Item view"> + <Select + size="m" + value={[field.itemView ?? 'clear']} + options={[ + {value: 'clear', content: 'Clear (flat)'}, + {value: 'card', content: 'Card (bordered)'}, + ]} + onUpdate={(next) => + updateField(field.id, { + itemView: next[0] as 'card' | 'clear', + }) + } + /> + </Row> + <Row label="Add button"> + <Checkbox + checked={Boolean(field.withAddButton)} + onUpdate={(value) => updateField(field.id, {withAddButton: value})} + > + Show “Add item” button + </Checkbox> + </Row> + </React.Fragment> + )} + {whenEditorSection} + </React.Fragment> + ); +}; diff --git a/src/form-builder-v2/components/FieldSettings/fields/SwitchSettings.tsx b/src/form-builder-v2/components/FieldSettings/fields/SwitchSettings.tsx new file mode 100644 index 0000000000..6f5d757d93 --- /dev/null +++ b/src/form-builder-v2/components/FieldSettings/fields/SwitchSettings.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; + +import {Switch} from '@gravity-ui/uikit'; + +import type {BuilderLeafField, FieldUpdate} from '../../../types'; + +import {Row} from './Row'; + +interface SwitchSettingsProps { + field: BuilderLeafField & {type: 'switch'}; + updateField: (id: string, updates: FieldUpdate) => void; + commonRows: React.ReactNode; + whenEditorSection: React.ReactNode; +} + +export const SwitchSettings = ({ + field, + updateField, + commonRows, + whenEditorSection, +}: SwitchSettingsProps) => ( + <React.Fragment> + {commonRows} + <Row label="Default"> + <Switch + checked={Boolean(field.defaultValue)} + onUpdate={(value) => updateField(field.id, {defaultValue: value})} + /> + </Row> + {whenEditorSection} + </React.Fragment> +); diff --git a/src/form-builder-v2/components/FieldSettings/fields/TextFieldSettings.tsx b/src/form-builder-v2/components/FieldSettings/fields/TextFieldSettings.tsx new file mode 100644 index 0000000000..32c30fe1a1 --- /dev/null +++ b/src/form-builder-v2/components/FieldSettings/fields/TextFieldSettings.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; + +import {TextInput} from '@gravity-ui/uikit'; + +import type {BuilderLeafField, FieldUpdate} from '../../../types'; + +import {Row} from './Row'; + +interface TextFieldSettingsProps { + field: BuilderLeafField & {type: 'textInput' | 'textArea'}; + updateField: (id: string, updates: FieldUpdate) => void; + commonRows: React.ReactNode; + whenEditorSection: React.ReactNode; +} + +export const TextFieldSettings = ({ + field, + updateField, + commonRows, + whenEditorSection, +}: TextFieldSettingsProps) => ( + <React.Fragment> + {commonRows} + <Row label="Default"> + <TextInput + value={field.defaultValue ?? ''} + onUpdate={(value) => updateField(field.id, {defaultValue: value || undefined})} + placeholder="Default value" + /> + </Row> + {whenEditorSection} + </React.Fragment> +); diff --git a/src/form-builder-v2/components/FieldSettings/fields/TextSettings.tsx b/src/form-builder-v2/components/FieldSettings/fields/TextSettings.tsx new file mode 100644 index 0000000000..5b966c2032 --- /dev/null +++ b/src/form-builder-v2/components/FieldSettings/fields/TextSettings.tsx @@ -0,0 +1,96 @@ +import * as React from 'react'; + +import {Select, TextArea} from '@gravity-ui/uikit'; + +import type {TextColor} from '../../../../form-generator-v2/types'; +import type {BuilderLeafField, FieldUpdate} from '../../../types'; + +import {Row} from './Row'; + +type TextLevel = 'info' | 'danger'; + +const TEXT_COLOR_OPTIONS = [ + {value: '', content: 'Default'}, + {value: 'primary', content: 'Primary'}, + {value: 'secondary', content: 'Secondary'}, + {value: 'hint', content: 'Hint'}, + {value: 'info', content: 'Info'}, + {value: 'positive', content: 'Positive'}, + {value: 'warning', content: 'Warning'}, + {value: 'danger', content: 'Danger'}, + {value: 'utility', content: 'Utility'}, + {value: 'misc', content: 'Misc'}, +]; + +const TEXT_LEVEL_OPTIONS = [ + {value: '', content: 'None'}, + {value: 'info', content: 'Info banner'}, + {value: 'danger', content: 'Danger banner'}, +]; + +const TEXT_COLORS: readonly TextColor[] = [ + 'primary', + 'secondary', + 'hint', + 'info', + 'positive', + 'warning', + 'danger', + 'utility', + 'misc', +]; + +const isTextColor = (value: string): value is TextColor => + (TEXT_COLORS as readonly string[]).includes(value); + +const isTextLevel = (value: string): value is TextLevel => value === 'info' || value === 'danger'; + +interface TextSettingsProps { + field: BuilderLeafField & {type: 'text'}; + updateField: (id: string, updates: FieldUpdate) => void; + whenEditorSection: React.ReactNode; +} + +export const TextSettings = ({field, updateField, whenEditorSection}: TextSettingsProps) => { + const handleLevelUpdate = ([value]: string[]) => { + updateField(field.id, { + level: isTextLevel(value) ? value : undefined, + }); + }; + + const handleColorUpdate = ([value]: string[]) => { + updateField(field.id, { + color: isTextColor(value) ? value : undefined, + }); + }; + + return ( + <React.Fragment> + <Row label="Text"> + <TextArea + value={field.text ?? ''} + onUpdate={(value) => updateField(field.id, {text: value})} + minRows={2} + placeholder="Static hint text" + /> + </Row> + <Row label="Banner"> + <Select + size="m" + value={[field.level ?? '']} + options={TEXT_LEVEL_OPTIONS} + onUpdate={handleLevelUpdate} + /> + </Row> + <Row label="Color"> + <Select + size="m" + value={[field.color ?? '']} + options={TEXT_COLOR_OPTIONS} + onUpdate={handleColorUpdate} + /> + </Row> + {whenEditorSection} + </React.Fragment> + ); +}; diff --git a/src/form-builder-v2/components/Inspector/Inspector.scss b/src/form-builder-v2/components/Inspector/Inspector.scss new file mode 100644 index 0000000000..44501d4e27 --- /dev/null +++ b/src/form-builder-v2/components/Inspector/Inspector.scss @@ -0,0 +1,27 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder-v2}inspector'; + +#{$block} { + padding: 14px; + + &__header { + display: flex; + flex-direction: column; + gap: var(--g-spacing-half); + margin-bottom: var(--g-spacing-3); + padding-bottom: 10px; + border-bottom: 1px solid var(--g-color-line-generic); + } + + &__body { + display: flex; + flex-direction: column; + gap: var(--g-spacing-2); + } + + &__empty { + padding: var(--g-spacing-4) var(--g-spacing-2); + text-align: center; + } +} diff --git a/src/form-builder-v2/components/Inspector/Inspector.tsx b/src/form-builder-v2/components/Inspector/Inspector.tsx new file mode 100644 index 0000000000..86f20c2bdb --- /dev/null +++ b/src/form-builder-v2/components/Inspector/Inspector.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; + +import {Card, Text} from '@gravity-ui/uikit'; + +import {FieldSettings} from '../../components/FieldSettings/FieldSettings'; +import {useFormContext} from '../../hooks/FormContext'; +import {formBuilderV2Cn} from '../../utils/cn'; +import {TYPE_LABELS} from '../../utils/fieldMeta'; +import {findFieldById} from '../../utils/fieldTree'; + +import './Inspector.scss'; + +const b = formBuilderV2Cn('inspector'); + +export const Inspector = () => { + const {formFields, selectedFieldId} = useFormContext(); + + const selected = React.useMemo( + () => (selectedFieldId ? findFieldById(formFields, selectedFieldId) : null), + [formFields, selectedFieldId], + ); + + return ( + <Card className={b()} view="outlined"> + {selected ? ( + <React.Fragment> + <div className={b('header')}> + <Text variant="subheader-2">{TYPE_LABELS[selected.type]}</Text> + {'name' in selected && selected.name ? ( + <Text variant="caption-1" color="hint"> + {selected.name} + </Text> + ) : null} + </div> + <div className={b('body')}> + <FieldSettings field={selected} /> + </div> + </React.Fragment> + ) : ( + <div className={b('empty')}> + <Text variant="body-2" color="hint"> + Select a field on the canvas to edit its settings. + </Text> + </div> + )} + </Card> + ); +}; diff --git a/src/form-builder-v2/components/Palette/Palette.scss b/src/form-builder-v2/components/Palette/Palette.scss new file mode 100644 index 0000000000..35b518e3c5 --- /dev/null +++ b/src/form-builder-v2/components/Palette/Palette.scss @@ -0,0 +1,71 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder-v2}palette'; + +#{$block} { + padding: var(--g-spacing-3); + + &__header { + display: flex; + flex-direction: column; + gap: var(--g-spacing-half); + margin-bottom: 10px; + } + + &__items { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px; + } + + &__tile { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + aspect-ratio: 1; + width: 100%; + height: auto; + padding: var(--g-spacing-2); + border: 1px solid var(--g-color-line-generic); + border-radius: var(--g-border-radius-m); + background: transparent; + color: var(--g-color-text-primary); + cursor: grab; + transition: + background-color 0.15s ease, + border-color 0.15s ease, + opacity 0.15s ease; + + .g-button__text { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + width: 100%; + } + + &:hover { + background-color: var(--g-color-base-simple-hover); + border-color: var(--g-color-line-generic-hover); + } + + &:active { + cursor: grabbing; + background-color: var(--g-color-base-simple-hover-solid); + } + + &_dragging { + opacity: 0.5; + } + } + + &__tile-label { + font-size: var(--g-text-caption-2-font-size); + line-height: 1.2; + text-align: center; + color: var(--g-color-text-secondary); + } +} diff --git a/src/form-builder-v2/components/Palette/Palette.tsx b/src/form-builder-v2/components/Palette/Palette.tsx new file mode 100644 index 0000000000..41fe53be50 --- /dev/null +++ b/src/form-builder-v2/components/Palette/Palette.tsx @@ -0,0 +1,123 @@ +import * as React from 'react'; + +import {PointerActivationConstraints, PointerSensor} from '@dnd-kit/dom'; +import {useDraggable} from '@dnd-kit/react'; +import {Button, Card, Icon, Text, Tooltip} from '@gravity-ui/uikit'; +import type {IconData} from '@gravity-ui/uikit'; + +import {useFormContext} from '../../hooks/FormContext'; +import {BuilderFieldType, FormField} from '../../types'; +import {formBuilderV2Cn} from '../../utils/cn'; +import type {PaletteDragData} from '../../utils/dragData'; +import {FIELD_TYPES, PALETTE_LABELS, TYPE_ICONS} from '../../utils/fieldMeta'; + +import './Palette.scss'; + +export type {PaletteDragData}; + +const b = formBuilderV2Cn('palette'); + +export const PALETTE_DRAGGABLE_PREFIX = 'palette:'; + +const findContainerId = ( + fields: FormField[], + selectedId: string | null, + parentSectionId: string | null = null, +): string | null | undefined => { + if (!selectedId) return null; + for (const field of fields) { + if (field.id === selectedId) { + return field.type === 'section' ? field.id : parentSectionId; + } + if (field.type === 'section') { + const nested = findContainerId(field.fields, selectedId, field.id); + if (nested !== undefined) return nested; + } + } + return undefined; +}; + +interface PaletteTileProps { + type: BuilderFieldType; + label: string; + icon: IconData; + onClick: () => void; +} + +const DRAG_DISTANCE = 4; + +const paletteSensors = [ + PointerSensor.configure({ + activationConstraints: [new PointerActivationConstraints.Distance({value: DRAG_DISTANCE})], + }), +]; + +const PaletteTile = ({type, label, icon, onClick}: PaletteTileProps) => { + const {ref, handleRef, isDragging} = useDraggable({ + id: `${PALETTE_DRAGGABLE_PREFIX}${type}`, + data: {kind: 'palette', type}, + sensors: paletteSensors, + }); + + const setRefs = React.useCallback( + (element: HTMLButtonElement | null) => { + ref(element); + handleRef(element); + }, + [ref, handleRef], + ); + + return ( + <Tooltip content={label} placement="right" openDelay={500}> + <Button + ref={setRefs} + view="flat" + size="m" + className={b('tile', {dragging: isDragging})} + onClick={onClick} + > + <Icon data={icon} size={18} /> + <span className={b('tile-label')}>{label}</span> + </Button> + </Tooltip> + ); +}; + +export const Palette = () => { + const {addField, addFieldToSection, formFields, selectedFieldId} = useFormContext(); + + const containerId = React.useMemo( + () => findContainerId(formFields, selectedFieldId), + [formFields, selectedFieldId], + ); + + const handleAdd = (type: BuilderFieldType) => { + if (containerId) { + addFieldToSection(containerId, type); + } else { + addField(type); + } + }; + + return ( + <Card className={b()} view="outlined"> + <div className={b('header')}> + <Text variant="subheader-2">Add field</Text> + <Text variant="caption-1" color="hint"> + Click to add, or drag onto the canvas to drop at a specific position. + </Text> + </div> + <div className={b('items')}> + {FIELD_TYPES.map((type) => ( + <PaletteTile + key={type} + type={type} + label={PALETTE_LABELS[type]} + icon={TYPE_ICONS[type]} + onClick={() => handleAdd(type)} + /> + ))} + </div> + </Card> + ); +}; diff --git a/src/form-builder-v2/components/ResizeHandle/ResizeHandle.scss b/src/form-builder-v2/components/ResizeHandle/ResizeHandle.scss new file mode 100644 index 0000000000..5c04a4425e --- /dev/null +++ b/src/form-builder-v2/components/ResizeHandle/ResizeHandle.scss @@ -0,0 +1,39 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder-v2}resize-handle'; + +#{$block} { + width: var(--g-spacing-2); + align-self: stretch; + cursor: col-resize; + position: relative; + user-select: none; + + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 40%; + background: var(--g-color-line-generic); + opacity: 0; + border-radius: 2px; + transition: + opacity 0.12s ease, + background-color 0.12s ease, + height 0.12s ease; + } + + &:hover::after { + opacity: 1; + height: 60%; + } + + &_dragging::after { + opacity: 1; + height: 100%; + background: var(--g-color-line-brand); + } +} diff --git a/src/form-builder-v2/components/ResizeHandle/ResizeHandle.tsx b/src/form-builder-v2/components/ResizeHandle/ResizeHandle.tsx new file mode 100644 index 0000000000..bf1ef606d8 --- /dev/null +++ b/src/form-builder-v2/components/ResizeHandle/ResizeHandle.tsx @@ -0,0 +1,135 @@ +import * as React from 'react'; + +import {formBuilderV2Cn} from '../../utils/cn'; + +import './ResizeHandle.scss'; + +const b = formBuilderV2Cn('resize-handle'); + +interface ResizeHandleProps { + value: number; + min: number; + max: number; + direction: 'left' | 'right'; + onChange: (next: number) => void; +} + +export const ResizeHandle = ({value, min, max, direction, onChange}: ResizeHandleProps) => { + const [dragging, setDragging] = React.useState(false); + const onChangeRef = React.useRef(onChange); + onChangeRef.current = onChange; + + const cleanupRef = React.useRef<(() => void) | null>(null); + const mountedRef = React.useRef(true); + + React.useEffect(() => { + mountedRef.current = true; + return () => { + mountedRef.current = false; + cleanupRef.current?.(); + }; + }, []); + + const onMouseDown = React.useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + const startX = event.clientX; + const startValue = value; + setDragging(true); + + let rafId: number | null = null; + let pendingValue = startValue; + const controller = new AbortController(); + const {signal} = controller; + + const flush = () => { + rafId = null; + onChangeRef.current(pendingValue); + }; + + const cleanup = () => { + if (signal.aborted) return; + controller.abort(); + if (rafId !== null) { + cancelAnimationFrame(rafId); + rafId = null; + } + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + cleanupRef.current = null; + }; + + const handleMove = (moveEvent: MouseEvent) => { + const deltaRaw = moveEvent.clientX - startX; + const delta = direction === 'left' ? deltaRaw : -deltaRaw; + pendingValue = Math.max(min, Math.min(max, startValue + delta)); + if (rafId === null) { + rafId = requestAnimationFrame(flush); + } + }; + + const handleUp = () => { + if (rafId !== null) { + cancelAnimationFrame(rafId); + rafId = null; + onChangeRef.current(pendingValue); + } + if (mountedRef.current) { + setDragging(false); + } + cleanup(); + }; + + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + + document.addEventListener('mousemove', handleMove, {signal}); + document.addEventListener('mouseup', handleUp, {signal}); + + cleanupRef.current = cleanup; + }, + [direction, max, min, value], + ); + + const onKeyDown = React.useCallback( + (event: React.KeyboardEvent<HTMLDivElement>) => { + const STEP = event.shiftKey ? 32 : 8; + const sign = direction === 'left' ? 1 : -1; + let next: number | null = null; + switch (event.key) { + case 'ArrowLeft': + next = value + sign * -STEP; + break; + case 'ArrowRight': + next = value + sign * STEP; + break; + case 'Home': + next = direction === 'left' ? min : max; + break; + case 'End': + next = direction === 'left' ? max : min; + break; + default: + return; + } + event.preventDefault(); + onChangeRef.current(Math.max(min, Math.min(max, next))); + }, + [direction, max, min, value], + ); + + return ( + <div + className={b({dragging})} + onMouseDown={onMouseDown} + onKeyDown={onKeyDown} + role="slider" + aria-orientation="vertical" + aria-valuemin={min} + aria-valuemax={max} + aria-valuenow={value} + aria-label={direction === 'left' ? 'Resize palette' : 'Resize inspector'} + tabIndex={0} + /> + ); +}; diff --git a/src/form-builder-v2/components/SchemaPopup/SchemaPopup.scss b/src/form-builder-v2/components/SchemaPopup/SchemaPopup.scss new file mode 100644 index 0000000000..a5cb90925e --- /dev/null +++ b/src/form-builder-v2/components/SchemaPopup/SchemaPopup.scss @@ -0,0 +1,59 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder-v2}schema-popup'; + +#{$block} { + width: min(560px, 90vw); + padding: var(--g-spacing-4); + display: flex; + flex-direction: column; + gap: 10px; + + &__header { + display: flex; + flex-direction: column; + gap: var(--g-spacing-half); + } + + &__textarea { + box-sizing: border-box; + width: 100%; + max-width: 100%; + min-height: 400px; + max-height: min(70vh, 700px); + padding: var(--g-spacing-3); + background: var(--g-color-base-generic); + border: 1px solid var(--g-color-line-generic); + border-radius: var(--g-border-radius-m); + font-family: monospace; + font-size: 12px; + line-height: 1.4; + color: var(--g-color-text-primary); + white-space: pre; + overflow: auto; + resize: vertical; + outline: none; + transition: border-color 0.15s ease; + tab-size: 2; + + &:focus { + border-color: var(--g-color-line-brand); + } + + &_error { + border-color: var(--g-color-line-danger); + } + } + + &__error-message { + padding: var(--g-spacing-2) var(--g-spacing-3); + background: var(--g-color-base-danger-light); + border-radius: var(--g-border-radius-m); + } + + &__actions { + display: flex; + gap: var(--g-spacing-2); + justify-content: flex-end; + } +} diff --git a/src/form-builder-v2/components/SchemaPopup/SchemaPopup.tsx b/src/form-builder-v2/components/SchemaPopup/SchemaPopup.tsx new file mode 100644 index 0000000000..4ebaf2228b --- /dev/null +++ b/src/form-builder-v2/components/SchemaPopup/SchemaPopup.tsx @@ -0,0 +1,146 @@ +import * as React from 'react'; + +import {Button, Card, Popup, Text} from '@gravity-ui/uikit'; + +import type {Fields} from '../../../form-generator-v2/types'; +import type {FormField} from '../../types'; +import {formBuilderV2Cn} from '../../utils/cn'; +import {parseSchema} from '../../utils/parseSchema'; + +import './SchemaPopup.scss'; + +const b = formBuilderV2Cn('schema-popup'); + +const stringify = (schema: Fields): string => JSON.stringify(schema, null, 2); + +interface SchemaPopupProps { + schema: Fields; + onApply: (fields: FormField[]) => void; + open: boolean; + onOpenChange: (open: boolean) => void; + anchorElement: HTMLElement | null; +} + +export const SchemaPopup = ({ + schema, + onApply, + open, + onOpenChange, + anchorElement, +}: SchemaPopupProps) => { + const textareaRef = React.useRef<HTMLTextAreaElement>(null); + const copyTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null); + const [draft, setDraft] = React.useState<string>(() => stringify(schema)); + const [error, setError] = React.useState<string | null>(null); + const [copied, setCopied] = React.useState(false); + const [isFocused, setIsFocused] = React.useState(false); + + React.useEffect(() => { + if (!isFocused) { + setDraft(stringify(schema)); + setError(null); + } + }, [schema, isFocused]); + + React.useEffect( + () => () => { + if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current); + }, + [], + ); + + const applyDraft = (value: string) => { + setDraft(value); + const result = parseSchema(value); + if (result.ok) { + setError(null); + onApply(result.fields); + } else { + setError(result.error); + } + }; + + const replaceSelection = (insert: string, caretOffset: number) => { + const t = textareaRef.current; + if (!t) return; + const {selectionStart, selectionEnd} = t; + const next = draft.slice(0, selectionStart) + insert + draft.slice(selectionEnd); + const nextCaret = selectionStart + caretOffset; + applyDraft(next); + requestAnimationFrame(() => { + t.selectionStart = nextCaret; + t.selectionEnd = nextCaret; + }); + }; + + const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => { + if (event.key === 'Tab') { + event.preventDefault(); + replaceSelection(' ', 2); + return; + } + if (event.key === 'Enter') { + event.preventDefault(); + const t = event.currentTarget; + const before = draft.slice(0, t.selectionStart); + const lineStart = before.lastIndexOf('\n') + 1; + const indent = before.slice(lineStart).match(/^[ \t]*/)?.[0] ?? ''; + const insert = `\n${indent}`; + replaceSelection(insert, insert.length); + } + }; + + const handleCopy = React.useCallback(async () => { + try { + await navigator.clipboard.writeText(draft); + setCopied(true); + if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current); + copyTimeoutRef.current = setTimeout(() => setCopied(false), 1500); + } catch {} + }, [draft]); + + return ( + <Popup + anchorElement={anchorElement} + open={open} + onOpenChange={onOpenChange} + placement="bottom-end" + > + <Card className={b()} view="outlined"> + <div className={b('header')}> + <Text variant="subheader-2">Form schema</Text> + <Text variant="caption-1" color="hint"> + Edit JSON directly — the canvas updates live. Paste your own schema to load + it. Selection on canvas resets after each edit. + </Text> + </div> + <textarea + ref={textareaRef} + className={b('textarea', {error: Boolean(error)})} + value={draft} + onChange={(event) => applyDraft(event.target.value)} + onKeyDown={handleKeyDown} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + spellCheck={false} + wrap="off" + /> + {error && ( + <div className={b('error-message')}> + <Text variant="caption-1" color="danger"> + {error} + </Text> + </div> + )} + <div className={b('actions')}> + <Button view="action" size="m" onClick={handleCopy}> + {copied ? '✓ Copied' : 'Copy'} + </Button> + <Button view="flat" size="m" onClick={() => onOpenChange(false)}> + Close + </Button> + </div> + </Card> + </Popup> + ); +}; diff --git a/src/form-builder-v2/components/WhenEditor/WhenEditor.scss b/src/form-builder-v2/components/WhenEditor/WhenEditor.scss new file mode 100644 index 0000000000..a0ab948467 --- /dev/null +++ b/src/form-builder-v2/components/WhenEditor/WhenEditor.scss @@ -0,0 +1,31 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder-v2}when-editor'; + +#{$block} { + display: flex; + flex-direction: column; + gap: 6px; + padding: var(--g-spacing-2); + border: 1px solid var(--g-color-line-generic); + border-radius: var(--g-border-radius-m); + + &__condition { + display: grid; + grid-template-columns: minmax(0, 1fr) 80px minmax(0, 1fr) auto; + gap: 6px; + align-items: center; + } + + &__combinator { + display: flex; + align-items: center; + } + + &__actions { + display: flex; + gap: var(--g-spacing-2); + align-items: center; + margin-top: var(--g-spacing-1); + } +} diff --git a/src/form-builder-v2/components/WhenEditor/WhenEditor.tsx b/src/form-builder-v2/components/WhenEditor/WhenEditor.tsx new file mode 100644 index 0000000000..b1cabbfcd7 --- /dev/null +++ b/src/form-builder-v2/components/WhenEditor/WhenEditor.tsx @@ -0,0 +1,146 @@ +import * as React from 'react'; + +import {TrashBin} from '@gravity-ui/icons'; +import {Button, Icon, Select, TextInput} from '@gravity-ui/uikit'; + +import type {When} from '../../../form-generator-v2/types'; +import {formBuilderV2Cn} from '../../utils/cn'; + +import { + COMBINATOR_OPTIONS, + Combinator, + Condition, + OPERATOR_OPTIONS, + coerceValue, + displayValue, + parse, + serialize, +} from './utils'; + +import './WhenEditor.scss'; + +const b = formBuilderV2Cn('when-editor'); + +interface WhenEditorProps { + when: When | undefined; + availableFields: string[]; + onChange: (next: When | undefined) => void; +} + +export const WhenEditor = ({when, availableFields, onChange}: WhenEditorProps) => { + const {conditions, combinators} = parse(when); + + const fieldOptions = React.useMemo( + () => availableFields.map((name) => ({value: name, content: name})), + [availableFields], + ); + + const commit = (nextConditions: Condition[], nextCombinators: Combinator[]) => { + onChange(serialize(nextConditions, nextCombinators)); + }; + + const addCondition = () => { + const nextConditions: Condition[] = [ + ...conditions, + {field: availableFields[0] ?? '', operator: '===', value: ''}, + ]; + const nextCombinators: Combinator[] = + conditions.length === 0 ? combinators : [...combinators, {operator: '&&'}]; + commit(nextConditions, nextCombinators); + }; + + const removeCondition = (index: number) => { + const nextConditions = conditions.filter((_, i) => i !== index); + const nextCombinators = + index === 0 + ? combinators.slice(1) + : [...combinators.slice(0, index - 1), ...combinators.slice(index)]; + commit(nextConditions, nextCombinators); + }; + + const updateCondition = (index: number, patch: Partial<Condition>) => { + const nextConditions = conditions.map((c, i) => (i === index ? {...c, ...patch} : c)); + commit(nextConditions, combinators); + }; + + const updateCombinator = (index: number, op: '&&' | '||') => { + const nextCombinators = combinators.map((c, i) => (i === index ? {operator: op} : c)); + commit(conditions, nextCombinators); + }; + + const clearAll = () => { + onChange(undefined); + }; + + if (conditions.length === 0) { + return ( + <div className={b()}> + <Button view="normal" size="s" onClick={addCondition}> + + Add condition + </Button> + </div> + ); + } + + return ( + <div className={b()}> + {conditions.map((cond, i) => ( + <React.Fragment key={i}> + {i > 0 && ( + <div className={b('combinator')}> + <Select + size="s" + value={[combinators[i - 1]?.operator ?? '&&']} + options={COMBINATOR_OPTIONS} + onUpdate={(next) => updateCombinator(i - 1, next[0] as '&&' | '||')} + width={80} + /> + </div> + )} + <div className={b('condition')}> + <Select + size="s" + value={[cond.field]} + options={fieldOptions} + onUpdate={(next) => updateCondition(i, {field: next[0] ?? ''})} + placeholder="field" + filterable + width="max" + /> + <Select + size="s" + value={[cond.operator]} + options={OPERATOR_OPTIONS} + onUpdate={(next) => + updateCondition(i, {operator: next[0] as '===' | '!=='}) + } + width={80} + /> + <TextInput + size="s" + value={displayValue(cond.value)} + onUpdate={(value) => updateCondition(i, {value: coerceValue(value)})} + placeholder='value ("true"/"false" → bool)' + /> + <Button + view="flat-danger" + size="s" + onClick={() => removeCondition(i)} + title="Remove condition" + > + <Icon data={TrashBin} size={12} /> + </Button> + </div> + </React.Fragment> + ))} + <div className={b('actions')}> + <Button view="normal" size="s" onClick={addCondition}> + + Add condition + </Button> + <Button view="flat" size="s" onClick={clearAll}> + Clear + </Button> + </div> + </div> + ); +}; diff --git a/src/form-builder-v2/components/WhenEditor/utils.ts b/src/form-builder-v2/components/WhenEditor/utils.ts new file mode 100644 index 0000000000..c6aa400e3c --- /dev/null +++ b/src/form-builder-v2/components/WhenEditor/utils.ts @@ -0,0 +1,89 @@ +import type {When} from '../../../form-generator-v2/types'; + +export type Condition = {field: string; operator: '===' | '!=='; value: string | boolean}; +export type Combinator = {operator: '&&' | '||'}; + +export const OPERATOR_OPTIONS = [ + {value: '===', content: 'is'}, + {value: '!==', content: 'is not'}, +]; + +export const COMBINATOR_OPTIONS = [ + {value: '&&', content: 'AND'}, + {value: '||', content: 'OR'}, +]; + +export const parse = ( + when: When | undefined, +): {conditions: Condition[]; combinators: Combinator[]} => { + const conditions: Condition[] = []; + const combinators: Combinator[] = []; + if (!when) return {conditions, combinators}; + + let lastWasCombinator = false; + + for (const entry of when) { + const isCondition = + Boolean(entry.field) && (entry.operator === '===' || entry.operator === '!=='); + const isCombinator = !entry.field && (entry.operator === '&&' || entry.operator === '||'); + + if (isCondition) { + conditions.push({ + field: entry.field as string, + operator: entry.operator as '===' | '!==', + value: entry.value ?? '', + }); + lastWasCombinator = false; + continue; + } + + if (isCombinator) { + if (conditions.length === 0 || lastWasCombinator) continue; + combinators.push({operator: entry.operator as '&&' | '||'}); + lastWasCombinator = true; + } + } + + if (lastWasCombinator) { + combinators.pop(); + } + + while (combinators.length < conditions.length - 1) { + combinators.push({operator: '&&'}); + } + + if (combinators.length > Math.max(0, conditions.length - 1)) { + combinators.length = Math.max(0, conditions.length - 1); + } + + return {conditions, combinators}; +}; + +export const serialize = (conditions: Condition[], combinators: Combinator[]): When | undefined => { + if (conditions.length === 0) return undefined; + const out: When = []; + conditions.forEach((cond, i) => { + if (i > 0) { + const combinator = combinators[i - 1] ?? {operator: '&&'}; + out.push({operator: combinator.operator}); + } + out.push({ + field: cond.field, + operator: cond.operator, + value: cond.value, + }); + }); + return out; +}; + +export const coerceValue = (raw: string): string | boolean => { + if (raw === 'true') return true; + if (raw === 'false') return false; + return raw; +}; + +export const displayValue = (value: string | boolean | undefined): string => { + if (value === true) return 'true'; + if (value === false) return 'false'; + return value ?? ''; +}; diff --git a/src/form-builder-v2/hooks/FormContext.tsx b/src/form-builder-v2/hooks/FormContext.tsx new file mode 100644 index 0000000000..b65fb0a9bc --- /dev/null +++ b/src/form-builder-v2/hooks/FormContext.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; + +import {FormContextType, FormField} from '../types'; + +import {useFormFields} from './useFormFields'; + +export const FormContext = React.createContext<FormContextType | null>(null); + +interface FormProviderProps { + children: React.ReactNode; + formFields: FormField[]; + onChange?: (fields: FormField[]) => void; +} + +export const FormProvider = ({children, formFields, onChange}: FormProviderProps) => { + const value = useFormFields({initialFields: formFields, onChange}); + return <FormContext.Provider value={value}>{children}</FormContext.Provider>; +}; + +export const useFormContext = (): FormContextType => { + const context = React.useContext(FormContext); + if (!context) { + throw new Error('useFormContext must be used within a FormProvider'); + } + return context; +}; diff --git a/src/form-builder-v2/hooks/useFormFields.ts b/src/form-builder-v2/hooks/useFormFields.ts new file mode 100644 index 0000000000..5ffbf3a9be --- /dev/null +++ b/src/form-builder-v2/hooks/useFormFields.ts @@ -0,0 +1,224 @@ +import * as React from 'react'; + +import {BuilderFieldType, FieldUpdate, FormField} from '../types'; +import {createDefaultField} from '../utils/fieldDefaults'; +import { + collectNames, + maxIdNumericSuffix, + prefixNameForArrayMode, + stripArrayModePrefix, +} from '../utils/fieldNames'; +import {findFieldById} from '../utils/fieldTree'; +import { + addChildToSectionDeep, + containsId, + duplicateFieldDeep, + insertAtTargetDeep, + isArrayModeSection, + removeFieldDeep, + swapInListDeep, + updateFieldDeep, +} from '../utils/fieldTreeOps'; + +interface UseFormFieldsProps { + initialFields: FormField[]; + onChange?: (fields: FormField[]) => void; +} + +export const useFormFields = ({initialFields, onChange}: UseFormFieldsProps) => { + const [formFields, setFormFields] = React.useState<FormField[]>(initialFields); + const [selectedFieldId, setSelectedFieldId] = React.useState<string | null>(null); + const idCounter = React.useRef<number>(maxIdNumericSuffix(initialFields) + 1); + + const generateId = React.useCallback(() => { + const id = `fb2_${idCounter.current}`; + idCounter.current += 1; + return id; + }, []); + + const generateName = React.useCallback(() => `field_${idCounter.current}`, []); + + const commit = React.useCallback( + (next: FormField[]) => { + setFormFields(next); + onChange?.(next); + }, + [onChange], + ); + + const addField = React.useCallback( + (type: BuilderFieldType) => { + const id = generateId(); + const name = generateName(); + const newField = createDefaultField(type, name, id); + commit([...formFields, newField]); + }, + [commit, formFields, generateId, generateName], + ); + + const addFieldToSection = React.useCallback( + (sectionId: string, type: BuilderFieldType) => { + const id = generateId(); + const baseName = generateName(); + const arrayMode = isArrayModeSection(formFields, sectionId); + const name = arrayMode ? prefixNameForArrayMode(baseName) : baseName; + const newChild = createDefaultField(type, name, id); + commit(addChildToSectionDeep(formFields, sectionId, newChild)); + }, + [commit, formFields, generateId, generateName], + ); + + const insertFieldRelative = React.useCallback( + (targetId: string, type: BuilderFieldType, offset: 0 | 1) => { + const makeField = (arrayMode: boolean): FormField => { + const id = generateId(); + const baseName = generateName(); + const name = arrayMode ? prefixNameForArrayMode(baseName) : baseName; + return createDefaultField(type, name, id); + }; + const result = insertAtTargetDeep(formFields, targetId, offset, makeField); + if (result === null) { + const fallback = makeField(false); + commit([...formFields, fallback]); + return; + } + commit(result); + }, + [commit, formFields, generateId, generateName], + ); + + const insertFieldBefore = React.useCallback( + (targetId: string, type: BuilderFieldType) => insertFieldRelative(targetId, type, 0), + [insertFieldRelative], + ); + + const insertFieldAfter = React.useCallback( + (targetId: string, type: BuilderFieldType) => insertFieldRelative(targetId, type, 1), + [insertFieldRelative], + ); + + const removeField = React.useCallback( + (fieldId: string) => { + commit(removeFieldDeep(formFields, fieldId)); + }, + [commit, formFields], + ); + + const moveFieldToSection = React.useCallback( + (fieldId: string, targetSectionId: string) => { + if (fieldId === targetSectionId) return; + + const sourceField = findFieldById(formFields, fieldId); + if (!sourceField) return; + + const targetField = findFieldById(formFields, targetSectionId); + if (!targetField || targetField.type !== 'section') { + return; + } + + if (sourceField.type === 'section' && containsId(sourceField, targetSectionId)) { + return; + } + + const withoutField = removeFieldDeep(formFields, fieldId); + const targetIsArray = isArrayModeSection(withoutField, targetSectionId); + + let movedField = sourceField; + if ( + sourceField.type !== 'section' && + sourceField.type !== 'text' && + sourceField.type !== 'divider' + ) { + const currentHasIndex = sourceField.name.includes('{{index}}'); + if (targetIsArray && !currentHasIndex) { + movedField = { + ...sourceField, + name: prefixNameForArrayMode(sourceField.name), + }; + } else if (!targetIsArray && currentHasIndex) { + movedField = { + ...sourceField, + name: stripArrayModePrefix(sourceField.name), + }; + } + } + + const result = addChildToSectionDeep(withoutField, targetSectionId, movedField); + commit(result); + }, + [commit, formFields], + ); + + const duplicateField = React.useCallback( + (fieldId: string) => { + const existingNames = collectNames(formFields); + const result = duplicateFieldDeep(formFields, fieldId, generateId, existingNames); + if (result !== null) { + commit(result); + } + }, + [commit, formFields, generateId], + ); + + const updateField = React.useCallback( + (fieldId: string, updates: FieldUpdate) => { + commit(updateFieldDeep(formFields, fieldId, updates)); + }, + [commit, formFields], + ); + + const moveFieldUp = React.useCallback( + (fieldId: string) => { + const result = swapInListDeep(formFields, fieldId, -1); + if (result.handled) { + commit(result.fields); + } + }, + [commit, formFields], + ); + + const moveFieldDown = React.useCallback( + (fieldId: string) => { + const result = swapInListDeep(formFields, fieldId, 1); + if (result.handled) { + commit(result.fields); + } + }, + [commit, formFields], + ); + + const setAllFields = React.useCallback( + (next: FormField[]) => { + idCounter.current = Math.max(idCounter.current, maxIdNumericSuffix(next) + 1); + commit(next); + }, + [commit], + ); + + const resetForm = React.useCallback(() => { + commit([]); + setSelectedFieldId(null); + }, [commit]); + + const selectField = React.useCallback((fieldId: string | null) => { + setSelectedFieldId(fieldId); + }, []); + + return { + formFields, + selectedFieldId, + addField, + addFieldToSection, + insertFieldBefore, + insertFieldAfter, + moveFieldToSection, + removeField, + duplicateField, + updateField, + moveFieldUp, + moveFieldDown, + setAllFields, + resetForm, + selectField, + }; +}; diff --git a/src/form-builder-v2/index.ts b/src/form-builder-v2/index.ts new file mode 100644 index 0000000000..049e07e3c3 --- /dev/null +++ b/src/form-builder-v2/index.ts @@ -0,0 +1,2 @@ +export * from './FormBuilderV2'; +export * from './types'; diff --git a/src/form-builder-v2/styles/variables.scss b/src/form-builder-v2/styles/variables.scss new file mode 100644 index 0000000000..496364ae51 --- /dev/null +++ b/src/form-builder-v2/styles/variables.scss @@ -0,0 +1 @@ +$ns-form-builder-v2: 'pcformbuilderv2-'; diff --git a/src/form-builder-v2/types.ts b/src/form-builder-v2/types.ts new file mode 100644 index 0000000000..ac75c814f4 --- /dev/null +++ b/src/form-builder-v2/types.ts @@ -0,0 +1,52 @@ +import type { + ColorField, + DividerField, + SectionField, + SegmentedRadioGroupField, + SelectField, + SwitchField, + Text, + TextField, +} from '../form-generator-v2/types'; + +type LeafField = + | TextField + | SelectField + | SegmentedRadioGroupField + | Text + | DividerField + | SwitchField + | ColorField; + +export type BuilderLeafField = LeafField & {id: string}; +export type BuilderSectionField = Omit<SectionField, 'fields'> & { + id: string; + fields: FormField[]; +}; + +export type FormField = BuilderLeafField | BuilderSectionField; + +export type BuilderFieldType = FormField['type']; + +export type FieldUpdate = Partial<LeafField> | Partial<Omit<BuilderSectionField, 'id'>>; + +export interface FormFieldsActions { + addField: (type: BuilderFieldType) => void; + addFieldToSection: (sectionId: string, type: BuilderFieldType) => void; + insertFieldBefore: (targetId: string, type: BuilderFieldType) => void; + insertFieldAfter: (targetId: string, type: BuilderFieldType) => void; + moveFieldToSection: (fieldId: string, sectionId: string) => void; + removeField: (fieldId: string) => void; + duplicateField: (fieldId: string) => void; + updateField: (fieldId: string, updates: FieldUpdate) => void; + moveFieldUp: (fieldId: string) => void; + moveFieldDown: (fieldId: string) => void; + setAllFields: (fields: FormField[]) => void; + resetForm: () => void; + selectField: (fieldId: string | null) => void; +} + +export interface FormContextType extends FormFieldsActions { + formFields: FormField[]; + selectedFieldId: string | null; +} diff --git a/src/form-builder-v2/utils/cn.ts b/src/form-builder-v2/utils/cn.ts new file mode 100644 index 0000000000..8b01edb6f5 --- /dev/null +++ b/src/form-builder-v2/utils/cn.ts @@ -0,0 +1,5 @@ +import {withNaming} from '@bem-react/classname'; + +export const FORM_BUILDER_V2_NAMESPACE = 'pcformbuilderv2-'; + +export const formBuilderV2Cn = withNaming({n: FORM_BUILDER_V2_NAMESPACE, e: '__', m: '_'}); diff --git a/src/form-builder-v2/utils/dragData.ts b/src/form-builder-v2/utils/dragData.ts new file mode 100644 index 0000000000..038de32400 --- /dev/null +++ b/src/form-builder-v2/utils/dragData.ts @@ -0,0 +1,35 @@ +import type {Data} from '@dnd-kit/abstract'; + +import type {BuilderFieldType} from '../types'; + +export interface PaletteDragData extends Data { + kind: 'palette'; + type: BuilderFieldType; +} + +export interface CardDragData extends Data { + kind: 'card'; + group: string; +} + +export interface SectionDropData extends Data { + kind: 'section-drop'; + sectionId: string; +} + +export function isPaletteData(data: Data | null | undefined): data is PaletteDragData { + return data !== null && data !== undefined && data.kind === 'palette'; +} + +export function isCardData(data: Data | null | undefined): data is CardDragData { + return data !== null && data !== undefined && data.kind === 'card'; +} + +export function isSectionDropData(data: Data | null | undefined): data is SectionDropData { + return data !== null && data !== undefined && data.kind === 'section-drop'; +} + +export const isDropAfter = ( + pointerY: number | undefined, + targetCenterY: number | undefined, +): boolean => pointerY !== undefined && targetCenterY !== undefined && pointerY > targetCenterY; diff --git a/src/form-builder-v2/utils/fieldDefaults.ts b/src/form-builder-v2/utils/fieldDefaults.ts new file mode 100644 index 0000000000..068500b1ce --- /dev/null +++ b/src/form-builder-v2/utils/fieldDefaults.ts @@ -0,0 +1,50 @@ +import {BuilderFieldType, FormField} from '../types'; + +export const createDefaultField = (type: BuilderFieldType, name: string, id: string): FormField => { + switch (type) { + case 'textInput': + return {type, name, title: 'Text input', id}; + case 'textArea': + return {type, name, title: 'Text area', id}; + case 'switch': + return {type, name, title: 'Switch', id}; + case 'colorInput': + return {type, name, title: 'Color', defaultValue: '#000000', id}; + case 'select': + return { + type, + name, + title: 'Select', + options: [ + {value: 'option1', content: 'Option 1'}, + {value: 'option2', content: 'Option 2'}, + ], + id, + }; + case 'segmentedRadioGroup': + return { + type, + name, + title: 'Segmented radio', + options: [ + {value: 'option1', content: 'Option 1'}, + {value: 'option2', content: 'Option 2'}, + ], + id, + }; + case 'text': + return {type, text: 'Static text', id}; + case 'divider': + return {type, id}; + case 'section': + return { + type, + title: 'Section', + opened: true, + fields: [], + id, + }; + default: + return undefined as never; + } +}; diff --git a/src/form-builder-v2/utils/fieldGroups.ts b/src/form-builder-v2/utils/fieldGroups.ts new file mode 100644 index 0000000000..0ff5e225f0 --- /dev/null +++ b/src/form-builder-v2/utils/fieldGroups.ts @@ -0,0 +1,25 @@ +import {FormField} from '../types'; + +import {walkFields} from './treeWalk'; + +export const buildGroupsMap = (fields: FormField[]): Record<string, FormField[]> => { + const groups: Record<string, FormField[]> = {root: fields}; + walkFields(fields, (field) => { + if (field.type === 'section') { + groups[`section:${field.id}`] = field.fields; + } + }); + return groups; +}; + +export const applyGroupsMap = (groups: Record<string, FormField[]>): FormField[] => { + const transform = (fields: FormField[]): FormField[] => + fields.map((field) => { + if (field.type === 'section') { + const childGroup = groups[`section:${field.id}`] ?? field.fields; + return {...field, fields: transform(childGroup)}; + } + return field; + }); + return transform(groups.root ?? []); +}; diff --git a/src/form-builder-v2/utils/fieldMeta.ts b/src/form-builder-v2/utils/fieldMeta.ts new file mode 100644 index 0000000000..d09dbfb541 --- /dev/null +++ b/src/form-builder-v2/utils/fieldMeta.ts @@ -0,0 +1,62 @@ +import { + ChevronsExpandVertical, + CircleInfo, + Cubes3, + Droplet, + FontCursor, + ListCheck, + Minus, + TextAlignLeft, + ToggleOn, +} from '@gravity-ui/icons'; +import type {IconData} from '@gravity-ui/uikit'; + +import type {BuilderFieldType} from '../types'; + +export const TYPE_LABELS: Record<BuilderFieldType, string> = { + textInput: 'Text input', + textArea: 'Text area', + select: 'Select', + segmentedRadioGroup: 'Segmented radio', + switch: 'Switch', + colorInput: 'Color input', + text: 'Static text', + divider: 'Divider', + section: 'Section', +}; + +export const PALETTE_LABELS: Record<BuilderFieldType, string> = { + textInput: 'Text', + textArea: 'Text area', + select: 'Select', + segmentedRadioGroup: 'Radio', + switch: 'Switch', + colorInput: 'Color', + text: 'Hint', + divider: 'Divider', + section: 'Section', +}; + +export const TYPE_ICONS: Record<BuilderFieldType, IconData> = { + textInput: FontCursor, + textArea: TextAlignLeft, + select: ChevronsExpandVertical, + segmentedRadioGroup: ListCheck, + switch: ToggleOn, + colorInput: Droplet, + text: CircleInfo, + divider: Minus, + section: Cubes3, +}; + +export const FIELD_TYPES: BuilderFieldType[] = [ + 'textInput', + 'textArea', + 'select', + 'segmentedRadioGroup', + 'switch', + 'colorInput', + 'text', + 'divider', + 'section', +]; diff --git a/src/form-builder-v2/utils/fieldNames.ts b/src/form-builder-v2/utils/fieldNames.ts new file mode 100644 index 0000000000..38ed351c9b --- /dev/null +++ b/src/form-builder-v2/utils/fieldNames.ts @@ -0,0 +1,87 @@ +import {FormField} from '../types'; + +import {walkFields} from './treeWalk'; + +export const ARRAY_ITEM_PREFIX = 'items[{{index}}].'; + +export const prefixNameForArrayMode = (name: string): string => + name.includes('{{index}}') ? name : `${ARRAY_ITEM_PREFIX}${name}`; + +export const stripArrayModePrefix = (name: string): string => + name.startsWith(ARRAY_ITEM_PREFIX) ? name.slice(ARRAY_ITEM_PREFIX.length) : name; + +export const collectNames = (fields: FormField[]): Set<string> => { + const names = new Set<string>(); + walkFields(fields, (field) => { + if (field.type !== 'section' && field.type !== 'text' && field.type !== 'divider') { + names.add(field.name); + } + }); + return names; +}; + +const extractBaseName = (name: string): string => { + const m = name.match(/^(.+?)_copy(?:_\d+)?$/); + return m ? m[1] : name; +}; + +const findUniqueCopyName = (originalName: string, existing: Set<string>): string => { + const stem = extractBaseName(originalName); + let candidate = `${stem}_copy`; + let n = 2; + while (existing.has(candidate)) { + candidate = `${stem}_copy_${n}`; + n += 1; + } + return candidate; +}; + +export const cloneFieldWithNewIds = ( + field: FormField, + generateId: () => string, + existingNames: Set<string>, +): FormField => { + const newId = generateId(); + if (field.type === 'section') { + return { + ...field, + id: newId, + title: field.title ? `${field.title} (copy)` : 'Section (copy)', + fields: field.fields.map((child) => + cloneFieldWithNewIds(child, generateId, existingNames), + ), + }; + } + if (field.type === 'text' || field.type === 'divider') { + return {...field, id: newId}; + } + const newName = findUniqueCopyName(field.name, existingNames); + existingNames.add(newName); + return {...field, id: newId, name: newName}; +}; + +export const transformChildNames = ( + fields: FormField[], + transform: (name: string) => string, +): FormField[] => + fields.map((field) => { + if (field.type === 'section') { + return {...field, fields: transformChildNames(field.fields, transform)}; + } + if (field.type === 'text' || field.type === 'divider') { + return field; + } + return {...field, name: transform(field.name)}; + }); + +export const maxIdNumericSuffix = (fields: FormField[]): number => { + let max = 0; + walkFields(fields, (field) => { + const match = /^fb2_(\d+)$/.exec(field.id); + if (match) { + const n = parseInt(match[1] ?? '0', 10); + if (n > max) max = n; + } + }); + return max; +}; diff --git a/src/form-builder-v2/utils/fieldTree.ts b/src/form-builder-v2/utils/fieldTree.ts new file mode 100644 index 0000000000..b3543a50ea --- /dev/null +++ b/src/form-builder-v2/utils/fieldTree.ts @@ -0,0 +1,6 @@ +import type {FormField} from '../types'; + +import {findField} from './treeWalk'; + +export const findFieldById = (fields: FormField[], id: string): FormField | null => + findField(fields, (field) => field.id === id); diff --git a/src/form-builder-v2/utils/fieldTreeOps.ts b/src/form-builder-v2/utils/fieldTreeOps.ts new file mode 100644 index 0000000000..55575f49da --- /dev/null +++ b/src/form-builder-v2/utils/fieldTreeOps.ts @@ -0,0 +1,106 @@ +import {BuilderSectionField, FieldUpdate, FormField} from '../types'; + +import {cloneFieldWithNewIds} from './fieldNames'; +import {findField, transformAtId} from './treeWalk'; + +export const updateFieldDeep = ( + fields: FormField[], + fieldId: string, + updates: FieldUpdate, +): FormField[] => { + return fields.map((field) => { + if (field.id === fieldId) { + return { + ...field, + ...(updates as Partial<FormField>), + id: field.id, + } as FormField; + } + if (field.type === 'section') { + return { + ...field, + fields: updateFieldDeep(field.fields, fieldId, updates), + } as BuilderSectionField; + } + return field; + }); +}; + +export const removeFieldDeep = (fields: FormField[], fieldId: string): FormField[] => { + const filtered = fields.filter((field) => field.id !== fieldId); + return filtered.map((field) => + field.type === 'section' + ? ({...field, fields: removeFieldDeep(field.fields, fieldId)} as BuilderSectionField) + : field, + ); +}; + +export const addChildToSectionDeep = ( + fields: FormField[], + sectionId: string, + newChild: FormField, +): FormField[] => { + return fields.map((field) => { + if (field.id === sectionId && field.type === 'section') { + return {...field, fields: [...field.fields, newChild]}; + } + if (field.type === 'section') { + return { + ...field, + fields: addChildToSectionDeep(field.fields, sectionId, newChild), + }; + } + return field; + }); +}; + +export const containsId = (field: FormField, id: string): boolean => + findField([field], (f) => f.id === id) !== null; + +export const isArrayModeSection = (fields: FormField[], sectionId: string): boolean => { + const section = findField(fields, (f) => f.id === sectionId); + return section?.type === 'section' && Boolean(section.index); +}; + +export const insertAtTargetDeep = ( + fields: FormField[], + targetId: string, + offset: 0 | 1, + makeField: (arrayMode: boolean) => FormField, +): FormField[] | null => + transformAtId(fields, targetId, (siblings, index, parent) => { + const arrayMode = Boolean(parent?.index); + const newField = makeField(arrayMode); + return [...siblings.slice(0, index + offset), newField, ...siblings.slice(index + offset)]; + }); + +export const duplicateFieldDeep = ( + fields: FormField[], + fieldId: string, + generateId: () => string, + existingNames: Set<string>, +): FormField[] | null => + transformAtId(fields, fieldId, (siblings, index) => { + const clone = cloneFieldWithNewIds(siblings[index], generateId, existingNames); + return [...siblings.slice(0, index + 1), clone, ...siblings.slice(index + 1)]; + }); + +export interface SwapResult { + fields: FormField[]; + handled: boolean; +} + +export const swapInListDeep = (fields: FormField[], fieldId: string, delta: -1 | 1): SwapResult => { + let didSwap = false; + const result = transformAtId(fields, fieldId, (siblings, index) => { + const target = index + delta; + if (target < 0 || target >= siblings.length) { + return siblings; + } + const next = [...siblings]; + [next[index], next[target]] = [next[target], next[index]]; + didSwap = true; + return next; + }); + return result === null || !didSwap ? {fields, handled: false} : {fields: result, handled: true}; +}; diff --git a/src/form-builder-v2/utils/parseSchema.ts b/src/form-builder-v2/utils/parseSchema.ts new file mode 100644 index 0000000000..3940bc8294 --- /dev/null +++ b/src/form-builder-v2/utils/parseSchema.ts @@ -0,0 +1,108 @@ +import type {FormField} from '../types'; + +const VALID_TYPES = new Set([ + 'textInput', + 'textArea', + 'select', + 'segmentedRadioGroup', + 'switch', + 'colorInput', + 'text', + 'divider', + 'section', +]); + +export type ParseSchemaResult = {ok: true; fields: FormField[]} | {ok: false; error: string}; + +const TYPES_REQUIRING_NAME = new Set([ + 'textInput', + 'textArea', + 'select', + 'segmentedRadioGroup', + 'switch', + 'colorInput', +]); + +const TYPES_WITH_OPTIONS = new Set(['select', 'segmentedRadioGroup']); + +const validateField = (obj: Record<string, unknown>, here: string): void => { + const type = obj.type as string; + + if (TYPES_REQUIRING_NAME.has(type) && typeof obj.name !== 'string') { + throw new Error(`${here}: field of type "${type}" requires "name"`); + } + + if (TYPES_WITH_OPTIONS.has(type)) { + if (!Array.isArray(obj.options) || obj.options.length === 0) { + throw new Error(`${here}: field of type "${type}" requires non-empty "options" array`); + } + obj.options.forEach((opt: unknown, i) => { + if (!opt || typeof opt !== 'object') { + throw new Error(`${here}.options[${i}]: expected object`); + } + const o = opt as Record<string, unknown>; + if (typeof o.value !== 'string' || typeof o.content !== 'string') { + throw new Error(`${here}.options[${i}]: "value" and "content" must be strings`); + } + }); + } + + if (type === 'text' && typeof obj.text !== 'string') { + throw new Error(`${here}: field of type "text" requires "text" string`); + } +}; + +const validateAndAssignIds = (raw: unknown[], path: string, nextId: () => string): FormField[] => { + const seenNames = new Set<string>(); + return raw.map((item, i) => { + const here = `${path}[${i}]`; + if (!item || typeof item !== 'object') { + throw new Error(`${here}: expected object, got ${typeof item}`); + } + const obj = item as Record<string, unknown>; + if (typeof obj.type !== 'string' || !VALID_TYPES.has(obj.type)) { + throw new Error(`${here}: unknown field type "${String(obj.type)}"`); + } + validateField(obj, here); + if (typeof obj.name === 'string') { + if (seenNames.has(obj.name)) { + throw new Error(`${here}: duplicate name "${obj.name}"`); + } + seenNames.add(obj.name); + } + if (obj.type === 'section') { + const nested = Array.isArray(obj.fields) + ? validateAndAssignIds(obj.fields, `${here}.fields`, nextId) + : []; + return {...obj, id: nextId(), fields: nested} as FormField; + } + return {...obj, id: nextId()} as FormField; + }); +}; + +export const parseSchema = (input: string): ParseSchemaResult => { + const trimmed = input.trim(); + if (!trimmed) { + return {ok: false, error: 'Schema is empty'}; + } + let parsed: unknown; + try { + parsed = JSON.parse(trimmed); + } catch (e) { + return {ok: false, error: `JSON parse error: ${(e as Error).message}`}; + } + if (!Array.isArray(parsed)) { + return {ok: false, error: 'Schema must be a JSON array of fields'}; + } + let counter = 0; + const nextId = () => { + counter += 1; + return `imp_${counter}`; + }; + try { + const fields = validateAndAssignIds(parsed, '', nextId); + return {ok: true, fields}; + } catch (e) { + return {ok: false, error: (e as Error).message}; + } +}; diff --git a/src/form-builder-v2/utils/stripIds.ts b/src/form-builder-v2/utils/stripIds.ts new file mode 100644 index 0000000000..daaa8a339b --- /dev/null +++ b/src/form-builder-v2/utils/stripIds.ts @@ -0,0 +1,11 @@ +import type {Fields} from '../../form-generator-v2/types'; +import type {FormField} from '../types'; + +export const stripIds = (fields: FormField[]): Fields => + fields.map((field) => { + const {id: _id, ...rest} = field; + if (rest.type === 'section') { + return {...rest, fields: stripIds(rest.fields)}; + } + return rest; + }) as Fields; diff --git a/src/form-builder-v2/utils/treeWalk.ts b/src/form-builder-v2/utils/treeWalk.ts new file mode 100644 index 0000000000..69b9baedd2 --- /dev/null +++ b/src/form-builder-v2/utils/treeWalk.ts @@ -0,0 +1,52 @@ +import {BuilderSectionField, FormField} from '../types'; + +export const walkFields = (fields: FormField[], visit: (field: FormField) => void): void => { + for (const field of fields) { + visit(field); + if (field.type === 'section') { + walkFields(field.fields, visit); + } + } +}; + +export const findField = ( + fields: FormField[], + predicate: (field: FormField) => boolean, +): FormField | null => { + for (const field of fields) { + if (predicate(field)) return field; + if (field.type === 'section') { + const nested = findField(field.fields, predicate); + if (nested) return nested; + } + } + return null; +}; + +export const transformAtId = ( + fields: FormField[], + id: string, + handler: ( + siblings: FormField[], + index: number, + parent: BuilderSectionField | null, + ) => FormField[], + parent: BuilderSectionField | null = null, +): FormField[] | null => { + const index = fields.findIndex((field) => field.id === id); + if (index !== -1) { + return handler(fields, index, parent); + } + for (let i = 0; i < fields.length; i += 1) { + const field = fields[i]; + if (field.type === 'section') { + const result = transformAtId(field.fields, id, handler, field); + if (result !== null) { + const next = [...fields]; + next[i] = {...field, fields: result}; + return next; + } + } + } + return null; +}; diff --git a/src/form-builder/FormBuilder.tsx b/src/form-builder/FormBuilder.tsx new file mode 100644 index 0000000000..1db6b2b2d0 --- /dev/null +++ b/src/form-builder/FormBuilder.tsx @@ -0,0 +1,15 @@ +'use client'; + +import * as React from 'react'; + +import {FormBuilderBody} from './components/FormBuilderBody/FormBuilderBody'; +import {FormProvider} from './hooks/FormContext'; +import {FormBuilderProps} from './types'; + +export const FormBuilder: React.FC<FormBuilderProps> = ({formFields, className, onChange}) => { + return ( + <FormProvider formFields={formFields} onChange={onChange}> + <FormBuilderBody className={className} /> + </FormProvider> + ); +}; diff --git a/src/form-builder/README.md b/src/form-builder/README.md new file mode 100644 index 0000000000..7ccd21f4ac --- /dev/null +++ b/src/form-builder/README.md @@ -0,0 +1,30 @@ +# FormBuilder + +Компонент для визуального создания и редактирования форм. + +## Как подключить + +```bash +import {FormBuilder} from '@gravity-ui/page-constructor/form-builder'; +``` + +## Пропсы + +| Пропс | Тип | Описание | +| ------------ | ------------------------------- | ---------------------------------------------------- | +| `formFields` | `FormField[]` | Массив полей формы, которые отображаются в редакторе | +| `className` | `string` | Дополнительный CSS-класс для стилизации компонента | +| `onChange` | `(fields: FormField[]) => void` | Колбэк, вызываемый при изменении структуры формы | + +## Примеры использования + +```tsx +import React from 'react'; +import {FormBuilder, FormField} from '@gravity-ui/page-constructor/form-builder'; + +const MyFormBuilder = () => { + const [formFields, setFormFields] = React.useState<FormField[]>([]); + + return <FormBuilder formFields={formFields} onChange={setFormFields} />; +}; +``` diff --git a/src/form-builder/components/AddPropertyButton/AddPropertyButton.scss b/src/form-builder/components/AddPropertyButton/AddPropertyButton.scss new file mode 100644 index 0000000000..20c62ba3eb --- /dev/null +++ b/src/form-builder/components/AddPropertyButton/AddPropertyButton.scss @@ -0,0 +1,7 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder}add-property-button'; + +#{$block} { + margin-top: 8px; +} diff --git a/src/form-builder/components/AddPropertyButton/AddPropertyButton.tsx b/src/form-builder/components/AddPropertyButton/AddPropertyButton.tsx new file mode 100644 index 0000000000..aac4d42e4a --- /dev/null +++ b/src/form-builder/components/AddPropertyButton/AddPropertyButton.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; + +import {Button, DropdownMenu} from '@gravity-ui/uikit'; + +import {ConfigInput} from '../../../form-generator'; +import {formBuilderCn} from '../../utils/cn'; + +import './AddPropertyButton.scss'; + +const b = formBuilderCn('add-property-button'); + +interface AddPropertyButtonProps { + inputTypeMenuItems: Array<{action: () => void; text: string; type: string}>; + onAdd: (type: ConfigInput['type']) => void; + buttonText?: string; +} + +export const AddPropertyButton: React.FC<AddPropertyButtonProps> = ({ + inputTypeMenuItems, + onAdd, + buttonText = '+ Add Property', +}) => ( + <div className={b()}> + <DropdownMenu + items={inputTypeMenuItems.map((item) => ({ + ...item, + action: () => { + onAdd(item.type as ConfigInput['type']); + }, + }))} + renderSwitcher={(props) => ( + <Button {...props} view="normal" size="s"> + {buttonText} + </Button> + )} + /> + </div> +); diff --git a/src/form-builder/components/ArrayFieldRenderer/ArrayFieldRenderer.scss b/src/form-builder/components/ArrayFieldRenderer/ArrayFieldRenderer.scss new file mode 100644 index 0000000000..fc789b4311 --- /dev/null +++ b/src/form-builder/components/ArrayFieldRenderer/ArrayFieldRenderer.scss @@ -0,0 +1,33 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder}array-field-renderer'; + +#{$block} { + &__config { + display: flex; + flex-direction: column; + gap: 8px; + } + + &__config-row { + display: flex; + align-items: center; + gap: 8px; + + & > span { + min-width: 90px; + font-weight: 500; + } + + & > div { + flex: 1; + } + } + + &__nested-fields { + margin-top: 12px; + margin-left: 16px; + padding-left: 12px; + border-left: 2px solid var(--g-color-line-generic); + } +} diff --git a/src/form-builder/components/ArrayFieldRenderer/ArrayFieldRenderer.tsx b/src/form-builder/components/ArrayFieldRenderer/ArrayFieldRenderer.tsx new file mode 100644 index 0000000000..27245193e0 --- /dev/null +++ b/src/form-builder/components/ArrayFieldRenderer/ArrayFieldRenderer.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; + +import {Button, DropdownMenu, Text} from '@gravity-ui/uikit'; + +import {ArrayObjectInput, ConfigInput} from '../../../form-generator'; +import {useFormContext} from '../../hooks/FormContext'; +import {FormArrayField, InputTypeMenuItem} from '../../types'; +import {formBuilderCn} from '../../utils/cn'; +import {AddPropertyButton} from '../AddPropertyButton/AddPropertyButton'; +import {ConfigRow} from '../ConfigRow/ConfigRow'; +import {SectionHeader} from '../SectionHeader/SectionHeader'; + +import './ArrayFieldRenderer.scss'; + +const b = formBuilderCn('array-field-renderer'); + +interface ArrayFieldRendererProps { + field: FormArrayField; + fieldName: string; + inputTypeMenuItems: InputTypeMenuItem[]; + renderNestedField: ( + field: ConfigInput, + index: number, + parentId: string, + optionIndex?: number, + ) => React.ReactNode; +} + +export const ArrayFieldRenderer: React.FC<ArrayFieldRendererProps> = ({ + field, + fieldName: _fieldName, + inputTypeMenuItems, + renderNestedField, +}) => { + const {addObjectProperty, updateField} = useFormContext(); + + return ( + <div className={b('config')}> + <div className={b('config-row')}> + <Text variant="body-2">Array Type:</Text> + <DropdownMenu + items={[ + { + action: () => updateField(field.id, {arrayType: 'text'}), + text: 'Text', + }, + { + action: () => updateField(field.id, {arrayType: 'object'}), + text: 'Object', + }, + ]} + renderSwitcher={(props) => ( + <Button {...props} view="normal" size="s"> + {field.arrayType === 'object' ? 'Object' : 'Text'} + </Button> + )} + /> + </div> + + <ConfigRow + label="Button Text" + value={field.buttonText} + onUpdate={(value) => updateField(field.id, {buttonText: value})} + /> + + {field.arrayType === 'object' && ( + <div className={b('nested-fields')}> + <SectionHeader title="Properties:" /> + + {(field as ArrayObjectInput)?.properties?.map((property, index) => + renderNestedField(property, index, field.id), + )} + + <AddPropertyButton + inputTypeMenuItems={inputTypeMenuItems} + onAdd={(type) => addObjectProperty(field.id, type)} + /> + </div> + )} + </div> + ); +}; diff --git a/src/form-builder/components/ConfigRow/ConfigRow.scss b/src/form-builder/components/ConfigRow/ConfigRow.scss new file mode 100644 index 0000000000..ae1fa635c1 --- /dev/null +++ b/src/form-builder/components/ConfigRow/ConfigRow.scss @@ -0,0 +1,18 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder}config-row'; + +#{$block} { + display: flex; + align-items: center; + gap: 8px; + + & > span { + min-width: 90px; + font-weight: 500; + } + + & > div { + flex: 1; + } +} diff --git a/src/form-builder/components/ConfigRow/ConfigRow.tsx b/src/form-builder/components/ConfigRow/ConfigRow.tsx new file mode 100644 index 0000000000..74261db859 --- /dev/null +++ b/src/form-builder/components/ConfigRow/ConfigRow.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; + +import {Text, TextInput} from '@gravity-ui/uikit'; + +import {formBuilderCn} from '../../utils/cn'; + +import './ConfigRow.scss'; + +const b = formBuilderCn('config-row'); + +interface ConfigRowProps { + label: string; + value: string; + onUpdate: (value: string) => void; +} + +const ConfigRowComponent: React.FC<ConfigRowProps> = ({label, value, onUpdate}) => { + const handleUpdate = React.useCallback( + (newValue: string) => { + onUpdate(newValue); + }, + [onUpdate], + ); + + return ( + <div className={b()}> + <Text variant="body-2">{label}:</Text> + <TextInput value={value} onUpdate={handleUpdate} size="s" /> + </div> + ); +}; + +export const ConfigRow = React.memo(ConfigRowComponent); diff --git a/src/form-builder/components/FieldCard/FieldCard.scss b/src/form-builder/components/FieldCard/FieldCard.scss new file mode 100644 index 0000000000..be609fe050 --- /dev/null +++ b/src/form-builder/components/FieldCard/FieldCard.scss @@ -0,0 +1,21 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder}field-card'; + +#{$block} { + padding: 16px; + + &__config { + display: flex; + flex-direction: column; + gap: 8px; + } + + &__nested-field { + margin-top: 8px; + padding: 12px; + background-color: var(--g-color-base-generic); + border-radius: 6px; + border: 1px solid var(--g-color-line-generic); + } +} diff --git a/src/form-builder/components/FieldCard/FieldCard.tsx b/src/form-builder/components/FieldCard/FieldCard.tsx new file mode 100644 index 0000000000..6a221ddee9 --- /dev/null +++ b/src/form-builder/components/FieldCard/FieldCard.tsx @@ -0,0 +1,163 @@ +import * as React from 'react'; + +import {Card} from '@gravity-ui/uikit'; + +import {ConfigInput} from '../../../form-generator'; +import {useFormContext} from '../../hooks/FormContext'; +import { + FormAnyOfField, + FormArrayField, + FormField, + FormObjectField, + FormOneOfField, + InputTypeMenuItem, +} from '../../types'; +import {formBuilderCn} from '../../utils/cn'; +import {ArrayFieldRenderer} from '../ArrayFieldRenderer/ArrayFieldRenderer'; +import {ConfigRow} from '../ConfigRow/ConfigRow'; +import {FieldHeader} from '../FieldHeader/FieldHeader'; +import {ObjectFieldRenderer} from '../ObjectFieldRenderer/ObjectFieldRenderer'; +import {OptionsRenderer} from '../OptionsRenderer/OptionsRenderer'; +import {SelectFieldRenderer} from '../SelectFieldRenderer/SelectFieldRenderer'; + +import './FieldCard.scss'; + +const b = formBuilderCn('field-card'); + +interface FieldCardProps { + field: FormField; + inputTypeMenuItems: InputTypeMenuItem[]; +} + +export const FieldCard: React.FC<FieldCardProps> = ({field, inputTypeMenuItems}) => { + const { + removeField, + updateField, + removeObjectProperty, + removeOptionProperty, + updateObjectProperty, + updateOptionProperty, + } = useFormContext(); + + const renderNestedField = ( + nestedField: ConfigInput, + index: number, + parentId: string, + optionIndex?: number, + ) => { + const isInOption = optionIndex !== undefined; + + const handleRemove = () => { + if (isInOption) { + removeOptionProperty(parentId, optionIndex, index); + } else { + removeObjectProperty(parentId, index); + } + }; + + const handleUpdate = (updates: Partial<ConfigInput>) => { + if (isInOption) { + updateOptionProperty(parentId, optionIndex, index, updates); + } else { + updateObjectProperty(parentId, index, updates); + } + }; + + return ( + <div key={`${parentId}_${index}`} className={b('nested-field')}> + <FieldHeader + title={nestedField.type.toUpperCase()} + onRemove={handleRemove} + variant="subheader-3" + buttonSize="xs" + /> + + <div className={b('config')}> + <ConfigRow + label="Name" + value={nestedField.name} + onUpdate={(value) => handleUpdate({name: value})} + /> + <ConfigRow + label="Title" + value={nestedField.title} + onUpdate={(value) => handleUpdate({title: value})} + /> + + {/* eslint-disable-next-line @typescript-eslint/no-use-before-define */} + {renderFieldTypeSpecificContent(nestedField, parentId)} + </div> + </div> + ); + }; + + const renderFieldTypeSpecificContent = (fieldConfig: ConfigInput, parentId: string) => { + switch (fieldConfig.type) { + case 'object': + return ( + <ObjectFieldRenderer + field={fieldConfig as FormObjectField} + fieldName={parentId} + inputTypeMenuItems={inputTypeMenuItems} + renderNestedField={renderNestedField} + /> + ); + + case 'oneOf': + return ( + <OptionsRenderer + field={fieldConfig as FormOneOfField} + fieldName={parentId} + inputTypeMenuItems={inputTypeMenuItems} + renderNestedField={renderNestedField} + /> + ); + + case 'anyOf': + return ( + <OptionsRenderer + field={fieldConfig as FormAnyOfField} + fieldName={parentId} + inputTypeMenuItems={inputTypeMenuItems} + renderNestedField={renderNestedField} + /> + ); + + case 'select': + return <SelectFieldRenderer field={fieldConfig as FormField} />; + + case 'array': + return ( + <ArrayFieldRenderer + field={fieldConfig as FormArrayField} + fieldName={parentId} + inputTypeMenuItems={inputTypeMenuItems} + renderNestedField={renderNestedField} + /> + ); + + default: + return null; + } + }; + + return ( + <Card key={field.id} className={b()}> + <FieldHeader title={field.type.toUpperCase()} onRemove={() => removeField(field.id)} /> + + <div className={b('config')}> + <ConfigRow + label="Name" + value={field.name} + onUpdate={(value) => updateField(field.id, {name: value})} + /> + <ConfigRow + label="Title" + value={field.title} + onUpdate={(value) => updateField(field.id, {title: value})} + /> + {renderFieldTypeSpecificContent(field, field.id)} + </div> + </Card> + ); +}; diff --git a/src/form-builder/components/FieldHeader/FieldHeader.scss b/src/form-builder/components/FieldHeader/FieldHeader.scss new file mode 100644 index 0000000000..1885573999 --- /dev/null +++ b/src/form-builder/components/FieldHeader/FieldHeader.scss @@ -0,0 +1,10 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder}field-header'; + +#{$block} { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} diff --git a/src/form-builder/components/FieldHeader/FieldHeader.tsx b/src/form-builder/components/FieldHeader/FieldHeader.tsx new file mode 100644 index 0000000000..c55f52ef34 --- /dev/null +++ b/src/form-builder/components/FieldHeader/FieldHeader.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; + +import {Button, Text} from '@gravity-ui/uikit'; + +import {formBuilderCn} from '../../utils/cn'; + +import './FieldHeader.scss'; + +const b = formBuilderCn('field-header'); + +interface FieldHeaderProps { + title: string; + onRemove: () => void; + variant?: 'subheader-2' | 'subheader-3'; + buttonSize?: 's' | 'xs'; +} + +export const FieldHeader: React.FC<FieldHeaderProps> = ({ + title, + onRemove, + variant = 'subheader-2', + buttonSize = 's', +}) => ( + <div className={b()}> + <Text variant={variant}>{title}</Text> + <Button view="flat-danger" size={buttonSize} onClick={onRemove}> + Remove + </Button> + </div> +); diff --git a/src/form-builder/components/FormBuilderBody/FormBuilderBody.scss b/src/form-builder/components/FormBuilderBody/FormBuilderBody.scss new file mode 100644 index 0000000000..20f635a6b5 --- /dev/null +++ b/src/form-builder/components/FormBuilderBody/FormBuilderBody.scss @@ -0,0 +1,114 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder}form-builder-body'; + +#{$block} { + width: 100%; + + &__field { + margin-top: 16px; + } + + &__fields-list { + margin-top: 20px; + display: flex; + flex-direction: column; + gap: 12px; + } + + &__field-card { + padding: 16px; + border: 1px solid #d1d5db; + border-radius: 8px; + background-color: #ffffff; + } + + &__field-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + } + + &__field-config { + display: flex; + flex-direction: column; + gap: 8px; + } + + &__config-row { + display: flex; + align-items: center; + gap: 8px; + + & > span { + min-width: 90px; + font-weight: 500; + } + + & > div { + flex: 1; + } + } + + &__nested-fields { + margin-top: 12px; + margin-left: 16px; + padding-left: 12px; + border-left: 2px solid #e5e7eb; + } + + &__nested-field { + margin-top: 8px; + padding: 12px; + background-color: #f9fafb; + border-radius: 6px; + border: 1px solid #e5e7eb; + } + + &__nested-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } + + &__option { + margin-top: 12px; + padding: 12px; + background-color: #f9fafb; + border-radius: 6px; + border: 1px solid #e5e7eb; + } + + &__option-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } + + &__add-field-button { + margin-top: 8px; + } + + &__enum-option { + margin-top: 8px; + padding: 12px; + background-color: #f3f4f6; + border-radius: 6px; + border: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + gap: 8px; + + .forms__config-row { + margin-bottom: 4px; + } + + button { + align-self: flex-end; + margin-top: 4px; + } + } +} diff --git a/src/form-builder/components/FormBuilderBody/FormBuilderBody.tsx b/src/form-builder/components/FormBuilderBody/FormBuilderBody.tsx new file mode 100644 index 0000000000..d9d5e735d7 --- /dev/null +++ b/src/form-builder/components/FormBuilderBody/FormBuilderBody.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; + +import {Button, DropdownMenu} from '@gravity-ui/uikit'; + +import {useFormContext} from '../../hooks/FormContext'; +import {InputTypeMenuItem} from '../../types'; +import {formBuilderCn} from '../../utils/cn'; +import {FieldCard} from '../FieldCard/FieldCard'; + +import './FormBuilderBody.scss'; + +const b = formBuilderCn('form-builder-body'); + +interface FormBuilderBodyProps { + className?: string; +} + +export const FormBuilderBody: React.FC<FormBuilderBodyProps> = ({className}) => { + const {formFields, addField} = useFormContext(); + + const inputTypeMenuItems: InputTypeMenuItem[] = [ + { + type: 'text', + action: () => addField('text'), + text: 'Text Input', + }, + { + type: 'number', + action: () => addField('number'), + text: 'Number Input', + }, + { + type: 'boolean', + action: () => addField('boolean'), + text: 'Boolean Input', + }, + { + type: 'textarea', + action: () => addField('textarea'), + text: 'Textarea Input', + }, + { + type: 'select', + action: () => addField('select'), + text: 'Select Input', + }, + { + type: 'object', + action: () => addField('object'), + text: 'Object Input', + }, + { + type: 'array', + action: () => addField('array'), + text: 'Array Input', + }, + { + type: 'oneOf', + action: () => addField('oneOf'), + text: 'OneOf Input', + }, + { + type: 'anyOf', + action: () => addField('anyOf'), + text: 'AnyOf Input', + }, + ]; + + return ( + <div className={b(null, className)}> + <div className={b('fields-list')}> + {formFields.map((field) => ( + <FieldCard + key={field.id} + field={field} + inputTypeMenuItems={inputTypeMenuItems} + /> + ))} + </div> + + <div className={b('field')}> + <DropdownMenu + items={inputTypeMenuItems} + renderSwitcher={(props) => ( + <Button {...props} view="action" size="l"> + + Add Field + </Button> + )} + /> + </div> + </div> + ); +}; diff --git a/src/form-builder/components/ObjectFieldRenderer/ObjectFieldRenderer.scss b/src/form-builder/components/ObjectFieldRenderer/ObjectFieldRenderer.scss new file mode 100644 index 0000000000..a602dada4a --- /dev/null +++ b/src/form-builder/components/ObjectFieldRenderer/ObjectFieldRenderer.scss @@ -0,0 +1,12 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder}object-field-renderer'; + +#{$block} { + &__nested-fields { + margin-top: 12px; + margin-left: 16px; + padding-left: 12px; + border-left: 2px solid var(--g-color-line-generic); + } +} diff --git a/src/form-builder/components/ObjectFieldRenderer/ObjectFieldRenderer.tsx b/src/form-builder/components/ObjectFieldRenderer/ObjectFieldRenderer.tsx new file mode 100644 index 0000000000..73fe52c33a --- /dev/null +++ b/src/form-builder/components/ObjectFieldRenderer/ObjectFieldRenderer.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; + +import {ConfigInput} from '../../../form-generator'; +import {useFormContext} from '../../hooks/FormContext'; +import {FormObjectField, InputTypeMenuItem} from '../../types'; +import {formBuilderCn} from '../../utils/cn'; +import {AddPropertyButton} from '../AddPropertyButton/AddPropertyButton'; +import {SectionHeader} from '../SectionHeader/SectionHeader'; + +import './ObjectFieldRenderer.scss'; + +const b = formBuilderCn('object-field-renderer'); + +interface ObjectFieldRendererProps { + field: FormObjectField; + fieldName: string; + inputTypeMenuItems: InputTypeMenuItem[]; + renderNestedField: ( + field: ConfigInput, + index: number, + parentId: string, + optionIndex?: number, + ) => React.ReactNode; +} + +export const ObjectFieldRenderer: React.FC<ObjectFieldRendererProps> = ({ + field, + fieldName: _fieldName, + inputTypeMenuItems, + renderNestedField, +}) => { + const {addObjectProperty} = useFormContext(); + + return ( + <div className={b('nested-fields')}> + <SectionHeader title="Properties:" /> + + {field.properties.map((property, index) => + renderNestedField(property, index, field.id), + )} + + <AddPropertyButton + inputTypeMenuItems={inputTypeMenuItems} + onAdd={(type) => addObjectProperty(field.id, type)} + /> + </div> + ); +}; diff --git a/src/form-builder/components/OptionHeader/OptionHeader.scss b/src/form-builder/components/OptionHeader/OptionHeader.scss new file mode 100644 index 0000000000..4b625bcc49 --- /dev/null +++ b/src/form-builder/components/OptionHeader/OptionHeader.scss @@ -0,0 +1,10 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder}option-header'; + +#{$block} { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} diff --git a/src/form-builder/components/OptionHeader/OptionHeader.tsx b/src/form-builder/components/OptionHeader/OptionHeader.tsx new file mode 100644 index 0000000000..2fb691c33c --- /dev/null +++ b/src/form-builder/components/OptionHeader/OptionHeader.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; + +import {Text} from '@gravity-ui/uikit'; + +import {formBuilderCn} from '../../utils/cn'; + +import './OptionHeader.scss'; + +const b = formBuilderCn('option-header'); + +interface OptionHeaderProps { + title: string; +} + +export const OptionHeader: React.FC<OptionHeaderProps> = ({title}) => ( + <div className={b()}> + <Text variant="subheader-3">{title}</Text> + </div> +); diff --git a/src/form-builder/components/OptionsRenderer/OptionsRenderer.scss b/src/form-builder/components/OptionsRenderer/OptionsRenderer.scss new file mode 100644 index 0000000000..55e60bba3e --- /dev/null +++ b/src/form-builder/components/OptionsRenderer/OptionsRenderer.scss @@ -0,0 +1,42 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder}options-renderer'; + +#{$block} { + &__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } + + &__option-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } + + &__add-option-button { + margin-left: 8px; + } + + &__remove-option-button { + margin-left: 8px; + } + + &__nested-fields { + margin-top: 12px; + margin-left: 16px; + padding-left: 12px; + border-left: 2px solid var(--g-color-line-generic); + } + + &__option { + margin-top: 12px; + padding: 12px; + background-color: var(--g-color-base-background); + border-radius: 6px; + border: 1px solid var(--g-color-line-generic); + } +} diff --git a/src/form-builder/components/OptionsRenderer/OptionsRenderer.tsx b/src/form-builder/components/OptionsRenderer/OptionsRenderer.tsx new file mode 100644 index 0000000000..426be9b49a --- /dev/null +++ b/src/form-builder/components/OptionsRenderer/OptionsRenderer.tsx @@ -0,0 +1,89 @@ +import * as React from 'react'; + +import {Button} from '@gravity-ui/uikit'; + +import {ConfigInput} from '../../../form-generator'; +import {useFormContext} from '../../hooks/FormContext'; +import {FormOptionsField, InputTypeMenuItem} from '../../types'; +import {formBuilderCn} from '../../utils/cn'; +import {AddPropertyButton} from '../AddPropertyButton/AddPropertyButton'; +import {OptionHeader} from '../OptionHeader/OptionHeader'; +import {SectionHeader} from '../SectionHeader/SectionHeader'; + +import './OptionsRenderer.scss'; + +const b = formBuilderCn('options-renderer'); + +interface OptionsRendererProps { + field: FormOptionsField; + fieldName: string; + inputTypeMenuItems: InputTypeMenuItem[]; + renderNestedField: ( + field: ConfigInput, + index: number, + parentId: string, + optionIndex?: number, + ) => React.ReactNode; +} + +export const OptionsRenderer: React.FC<OptionsRendererProps> = ({ + field, + fieldName: _fieldName, + inputTypeMenuItems, + renderNestedField, +}) => { + const {addOptionProperty, addOption, removeOption} = useFormContext(); + + return ( + <div className={b('nested-fields')}> + <div className={b('header')}> + <SectionHeader title="Options:" /> + <Button + view="normal" + size="s" + onClick={() => { + addOption(field.id); + }} + className={b('add-option-button')} + > + + Add Option + </Button> + </div> + + {field.options.map((option, optionIndex) => ( + <div key={`option_${optionIndex}`} className={b('option')}> + <div className={b('option-header')}> + <OptionHeader title={option.title} /> + {field.options.length > 1 && ( + <Button + view="normal" + size="xs" + onClick={() => { + removeOption(field.id, optionIndex); + }} + className={b('remove-option-button')} + > + Remove + </Button> + )} + </div> + + <div className={b('nested-fields')}> + <SectionHeader title="Properties:" variant="body-2" /> + + {option.properties.map((property, propIndex) => + renderNestedField(property, propIndex, field.id, optionIndex), + )} + + <AddPropertyButton + inputTypeMenuItems={inputTypeMenuItems} + onAdd={(type) => { + addOptionProperty(field.id, optionIndex, type); + }} + /> + </div> + </div> + ))} + </div> + ); +}; diff --git a/src/form-builder/components/SectionHeader/SectionHeader.scss b/src/form-builder/components/SectionHeader/SectionHeader.scss new file mode 100644 index 0000000000..9550ae4349 --- /dev/null +++ b/src/form-builder/components/SectionHeader/SectionHeader.scss @@ -0,0 +1,7 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder}section-header'; + +#{$block} { + margin-bottom: 8px; +} diff --git a/src/form-builder/components/SectionHeader/SectionHeader.tsx b/src/form-builder/components/SectionHeader/SectionHeader.tsx new file mode 100644 index 0000000000..635a65eb2b --- /dev/null +++ b/src/form-builder/components/SectionHeader/SectionHeader.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; + +import {Text} from '@gravity-ui/uikit'; + +import {formBuilderCn} from '../../utils/cn'; + +import './SectionHeader.scss'; + +const b = formBuilderCn('section-header'); + +interface SectionHeaderProps { + title: string; + variant?: 'body-1' | 'body-2'; +} + +export const SectionHeader: React.FC<SectionHeaderProps> = ({title, variant = 'body-1'}) => ( + <div className={b()}> + <Text variant={variant}>{title}</Text> + </div> +); diff --git a/src/form-builder/components/SelectFieldRenderer/SelectFieldRenderer.scss b/src/form-builder/components/SelectFieldRenderer/SelectFieldRenderer.scss new file mode 100644 index 0000000000..851f4a5e11 --- /dev/null +++ b/src/form-builder/components/SelectFieldRenderer/SelectFieldRenderer.scss @@ -0,0 +1,49 @@ +@import '../../styles/variables.scss'; + +$block: '.#{$ns-form-builder}select-field-renderer'; + +#{$block} { + &__config-row { + display: flex; + align-items: center; + gap: 8px; + + & > span { + min-width: 90px; + font-weight: 500; + } + + & > div { + flex: 1; + } + + &_vertical { + flex-direction: column; + align-items: flex-start; + } + } + + &__options-title { + margin-bottom: 8px; + } + + &__enum-option { + margin-top: 8px; + padding: 12px; + background-color: var(--g-color-base-generic-ultralight); + border-radius: 6px; + border: 1px solid var(--g-color-line-generic); + display: flex; + flex-direction: column; + gap: 8px; + } + + &__remove-button { + align-self: flex-end; + margin-top: 4px; + } + + &__add-button { + margin-top: 8px; + } +} diff --git a/src/form-builder/components/SelectFieldRenderer/SelectFieldRenderer.tsx b/src/form-builder/components/SelectFieldRenderer/SelectFieldRenderer.tsx new file mode 100644 index 0000000000..f58fdfafeb --- /dev/null +++ b/src/form-builder/components/SelectFieldRenderer/SelectFieldRenderer.tsx @@ -0,0 +1,145 @@ +import * as React from 'react'; + +import {Button, DropdownMenu, Text, TextInput} from '@gravity-ui/uikit'; + +import {useFormContext} from '../../hooks/FormContext'; +import {FormField} from '../../types'; +import {formBuilderCn} from '../../utils/cn'; + +import './SelectFieldRenderer.scss'; + +const b = formBuilderCn('select-field-renderer'); + +interface SelectFieldRendererProps { + field: FormField; +} + +interface SelectFieldOption { + content: string; + value: string; +} + +export const SelectFieldRenderer: React.FC<SelectFieldRendererProps> = ({field}) => { + const {updateField} = useFormContext(); + + if (field.type !== 'select') { + return null; + } + + const view = field.view as 'select' | 'radiobutton'; + const mode = field.mode as 'single' | 'multiple'; + const enumValues = (field.enum || []) as SelectFieldOption[]; + + return ( + <React.Fragment> + <div className={b('config-row')}> + <Text variant="body-2">View:</Text> + <DropdownMenu + items={[ + { + action: () => updateField(field.id, {view: 'select'}), + text: 'Select', + }, + { + action: () => updateField(field.id, {view: 'radiobutton'}), + text: 'Radio Button', + }, + ]} + renderSwitcher={(props) => ( + <Button {...props} view="normal" size="s"> + {view === 'radiobutton' ? 'Radio Button' : 'Select'} + </Button> + )} + /> + </div> + + <div className={b('config-row')}> + <Text variant="body-2">Mode:</Text> + <DropdownMenu + items={[ + { + action: () => updateField(field.id, {mode: 'single'}), + text: 'Single', + }, + { + action: () => updateField(field.id, {mode: 'multiple'}), + text: 'Multiple', + }, + ]} + renderSwitcher={(props) => ( + <Button {...props} view="normal" size="s"> + {mode === 'multiple' ? 'Multiple' : 'Single'} + </Button> + )} + /> + </div> + + <div className={b('config-row', {vertical: true})}> + <Text variant="body-2" className={b('options-title')}> + Options: + </Text> + {enumValues.map((option, index) => ( + <div key={`enum_${index}`} className={b('enum-option')}> + <div className={b('config-row')}> + <Text variant="body-2">Label:</Text> + <TextInput + value={option.content} + onUpdate={(value) => { + const newEnum = [...enumValues]; + newEnum[index] = { + ...newEnum[index], + content: value, + }; + updateField(field.id, {enum: newEnum}); + }} + size="s" + /> + </div> + <div className={b('config-row')}> + <Text variant="body-2">Value:</Text> + <TextInput + value={option.value} + onUpdate={(value) => { + const newEnum = [...enumValues]; + newEnum[index] = { + ...newEnum[index], + value, + }; + updateField(field.id, {enum: newEnum}); + }} + size="s" + /> + </div> + <Button + view="flat-danger" + size="xs" + onClick={() => { + const newEnum = [...enumValues]; + newEnum.splice(index, 1); + updateField(field.id, {enum: newEnum}); + }} + className={b('remove-button')} + > + Remove + </Button> + </div> + ))} + <Button + view="normal" + size="s" + onClick={() => { + const newEnum = [...(enumValues || [])]; + newEnum.push({ + content: `Option ${newEnum.length + 1}`, + value: `option${newEnum.length + 1}`, + }); + updateField(field.id, {enum: newEnum}); + }} + className={b('add-button')} + > + + Add Option + </Button> + </div> + </React.Fragment> + ); +}; diff --git a/src/form-builder/hooks/FormContext.tsx b/src/form-builder/hooks/FormContext.tsx new file mode 100644 index 0000000000..64f2ddca8e --- /dev/null +++ b/src/form-builder/hooks/FormContext.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; + +import {FormContextType, FormField} from '../types'; + +import {useFormFields} from './useFormFields'; + +export const FormContext = React.createContext<FormContextType>({} as FormContextType); + +interface FormProviderProps { + children: React.ReactNode; + formFields: FormField[]; + onChange?: (fields: FormField[]) => void; +} + +export const FormProvider: React.FC<FormProviderProps> = ({children, formFields, onChange}) => { + const formFieldsData = useFormFields({initialFields: formFields, onChange}); + return <FormContext.Provider value={formFieldsData}>{children}</FormContext.Provider>; +}; + +export const useFormContext = () => { + const context = React.useContext(FormContext); + + if (!context) { + throw new Error('useFormContext must be used within a FormProvider'); + } + + return context; +}; diff --git a/src/form-builder/hooks/useFormFields.ts b/src/form-builder/hooks/useFormFields.ts new file mode 100644 index 0000000000..4663d10be6 --- /dev/null +++ b/src/form-builder/hooks/useFormFields.ts @@ -0,0 +1,864 @@ +import * as React from 'react'; + +import {AnyOfInput, ConfigInput, ObjectInput, OneOfInput} from '../../form-generator'; +import {FormArrayField, FormField} from '../types'; + +interface UseFormFieldsProps { + initialFields: FormField[]; + onChange?: (fields: FormField[]) => void; +} + +export const useFormFields = ({initialFields, onChange}: UseFormFieldsProps) => { + const [formFields, setFormFields] = React.useState<FormField[]>(initialFields); + + const [nextId, setNextId] = React.useState<number>(initialFields.length + 1); + + const generateId = React.useCallback(() => { + const id = `field_id_${nextId}`; + setNextId((prev) => prev + 1); + return id; + }, [nextId]); + + const generateName = React.useCallback(() => { + const name = `field_${nextId}`; + return name; + }, [nextId]); + + const updateFormFields = React.useCallback( + (fields: FormField[]) => { + setFormFields(fields); + onChange?.(fields); + }, + [onChange], + ); + + const createField = React.useCallback( + (type: ConfigInput['type'], name = ''): FormField => { + const fieldName = name || generateName(); + const fieldId = generateId(); + let newField: FormField; + + switch (type) { + case 'text': + newField = { + type: 'text', + name: fieldName, + title: 'Text Input', + id: fieldId, + } as FormField; + break; + case 'number': + newField = { + type: 'number', + name: fieldName, + title: 'Number Input', + id: fieldId, + } as FormField; + break; + case 'boolean': + newField = { + type: 'boolean', + name: fieldName, + title: 'Boolean Input', + id: fieldId, + } as FormField; + break; + case 'textarea': + newField = { + type: 'textarea', + name: fieldName, + title: 'Textarea Input', + id: fieldId, + } as FormField; + break; + case 'select': + newField = { + type: 'select', + name: fieldName, + title: 'Select Input', + view: 'select', + mode: 'single', + enum: [ + {content: 'Option 1', value: 'option1'}, + {content: 'Option 2', value: 'option2'}, + ], + id: fieldId, + } as FormField; + break; + case 'object': + newField = { + type: 'object', + name: fieldName, + title: 'Object Input', + properties: [ + { + type: 'text', + name: 'property1', + title: 'Property 1', + id: generateId(), + } as FormField, + ], + id: fieldId, + } as FormField; + break; + case 'array': + newField = { + type: 'array', + name: fieldName, + title: 'Array Input', + buttonText: 'Add Item', + arrayType: 'text', + id: fieldId, + } as FormField; + break; + case 'oneOf': + newField = { + type: 'oneOf', + name: fieldName, + title: 'OneOf Input', + options: [ + { + title: 'Option A', + value: 'optionA', + properties: [ + { + type: 'text', + name: 'textField', + title: 'Text Field', + id: generateId(), + } as FormField, + ], + }, + { + title: 'Option B', + value: 'optionB', + properties: [ + { + type: 'number', + name: 'numberField', + title: 'Number Field', + id: generateId(), + } as FormField, + ], + }, + ], + id: fieldId, + } as FormField; + break; + case 'anyOf': + newField = { + type: 'anyOf', + name: fieldName, + title: 'AnyOf Input', + options: [ + { + title: 'Option A', + value: 'optionA', + properties: [ + { + type: 'text', + name: 'textField', + title: 'Text Field', + id: generateId(), + } as FormField, + ], + }, + { + title: 'Option B', + value: 'optionB', + properties: [ + { + type: 'boolean', + name: 'booleanField', + title: 'Boolean Field', + id: generateId(), + } as FormField, + ], + }, + ], + id: fieldId, + } as FormField; + break; + default: + return createField('text', fieldName); + } + + return newField; + }, + [generateId, generateName], + ); + + const updateFieldById = React.useCallback( + (fields: FormField[], fieldId: string, updates: Partial<ConfigInput>): FormField[] => { + return fields.map((field) => { + if (field.id === fieldId) { + return {...field, ...updates} as FormField; + } + + if (field.type === 'object' && (field as ObjectInput).properties) { + const objectField = field as ObjectInput; + return { + ...field, + properties: updateFieldById( + objectField.properties as FormField[], + fieldId, + updates, + ), + } as FormField; + } + + if ( + (field.type === 'oneOf' || field.type === 'anyOf') && + (field as OneOfInput | AnyOfInput).options + ) { + const optionsField = field as OneOfInput | AnyOfInput; + return { + ...field, + options: optionsField.options.map((option) => ({ + ...option, + properties: updateFieldById( + option.properties as FormField[], + fieldId, + updates, + ), + })), + } as FormField; + } + + if (field.type === 'array' && field.arrayType === 'object') { + const arrayField = field as FormArrayField; + return { + ...field, + properties: updateFieldById( + arrayField.properties as FormField[], + fieldId, + updates, + ), + } as FormField; + } + + return field; + }); + }, + [], + ); + + const addField = React.useCallback( + (type: ConfigInput['type']) => { + const newField = createField(type); + updateFormFields([...formFields, newField]); + }, + [createField, formFields, updateFormFields], + ); + + const removeField = React.useCallback( + (fieldId: string) => { + updateFormFields(formFields.filter((field) => field.id !== fieldId)); + }, + [formFields, updateFormFields], + ); + + const updateField = React.useCallback( + (fieldId: string, updates: Partial<ConfigInput>) => { + updateFormFields(updateFieldById(formFields, fieldId, updates)); + }, + [formFields, updateFieldById, updateFormFields], + ); + + const addPropertyToObjectById = React.useCallback( + ( + fields: FormField[], + objectId: string, + type: ConfigInput['type'] = 'text', + ): FormField[] => { + return fields.map((field) => { + if (field.id === objectId && field.type === 'object') { + const objectField = field as ObjectInput; + const propertyName = `property${(objectField.properties?.length || 0) + 1}`; + const newProperty = createField(type, propertyName); + + return { + ...field, + properties: [...(objectField.properties || []), newProperty], + } as FormField; + } + + if (field.type === 'object' && (field as ObjectInput).properties) { + const objectField = field as ObjectInput; + return { + ...field, + properties: addPropertyToObjectById( + objectField.properties as FormField[], + objectId, + type, + ), + } as FormField; + } + + if ( + (field.type === 'oneOf' || field.type === 'anyOf') && + (field as OneOfInput | AnyOfInput).options + ) { + const optionsField = field as OneOfInput | AnyOfInput; + return { + ...field, + options: optionsField.options.map((option) => ({ + ...option, + properties: addPropertyToObjectById( + option.properties as FormField[], + objectId, + type, + ), + })), + } as FormField; + } + + if (field.type === 'array' && field.arrayType === 'object') { + const arrayField = field as FormArrayField; + + if (field.id === objectId) { + const propertyName = `property${(arrayField.properties?.length || 0) + 1}`; + const newProperty = createField(type, propertyName); + + return { + ...field, + properties: [...(arrayField.properties || []), newProperty], + } as FormField; + } + + if (arrayField.properties && arrayField.properties.length > 0) { + return { + ...field, + properties: addPropertyToObjectById( + arrayField.properties, + objectId, + type, + ), + } as FormField; + } + } + + return field; + }); + }, + [createField], + ); + + const addObjectProperty = React.useCallback( + (objectId: string, type: ConfigInput['type'] = 'text') => { + updateFormFields(addPropertyToObjectById(formFields, objectId, type)); + }, + [formFields, addPropertyToObjectById, updateFormFields], + ); + + const addOptionProperty = React.useCallback( + (fieldId: string, optionIndex: number, type: ConfigInput['type'] = 'text') => { + const addPropertyToOptionRecursive = ( + fields: FormField[], + targetFieldId: string, + targetOptionIndex: number, + ): FormField[] => { + return fields.map((field) => { + if ( + field.id === targetFieldId && + (field.type === 'oneOf' || field.type === 'anyOf') + ) { + const updatedField = JSON.parse(JSON.stringify(field)); + const options = updatedField.options; + + if ( + options && + Array.isArray(options) && + targetOptionIndex >= 0 && + targetOptionIndex < options.length && + options[targetOptionIndex].properties + ) { + const propertyName = `property${options[targetOptionIndex].properties.length + 1}`; + const newProperty = createField(type, propertyName); + + options[targetOptionIndex].properties.push(newProperty); + } + + return updatedField; + } + + if (field.type === 'object' && (field as ObjectInput).properties) { + const objectField = field as ObjectInput; + return { + ...field, + properties: addPropertyToOptionRecursive( + objectField.properties as FormField[], + targetFieldId, + targetOptionIndex, + ), + } as FormField; + } + + if ( + (field.type === 'oneOf' || field.type === 'anyOf') && + (field as OneOfInput | AnyOfInput).options + ) { + const optionsField = field as OneOfInput | AnyOfInput; + return { + ...field, + options: optionsField.options.map((option) => ({ + ...option, + properties: addPropertyToOptionRecursive( + option.properties as FormField[], + targetFieldId, + targetOptionIndex, + ), + })), + } as FormField; + } + + if ( + field.type === 'array' && + field.arrayType === 'object' && + (field as FormArrayField).properties + ) { + const arrayField = field as FormArrayField; + return { + ...field, + properties: addPropertyToOptionRecursive( + arrayField.properties as FormField[], + targetFieldId, + targetOptionIndex, + ), + } as FormField; + } + + return field; + }); + }; + + setFormFields((prev) => addPropertyToOptionRecursive(prev, fieldId, optionIndex)); + }, + [createField], + ); + + const removeObjectProperty = React.useCallback( + (fieldId: string, propertyIndex: number) => { + updateFormFields( + formFields.map((field) => { + if (field.id !== fieldId) return field; + + const updatedField = JSON.parse(JSON.stringify(field)); + const objectInput = updatedField as ObjectInput; + + objectInput.properties.splice(propertyIndex, 1); + + return updatedField; + }) as FormField[], + ); + }, + [formFields, updateFormFields], + ); + + const removeOptionProperty = React.useCallback( + (fieldId: string, optionIndex: number, propertyIndex: number) => { + const removePropertyFromOptionRecursive = ( + fields: FormField[], + targetFieldId: string, + targetOptionIndex: number, + targetPropertyIndex: number, + ): FormField[] => { + return fields.map((field) => { + if ( + field.id === targetFieldId && + (field.type === 'oneOf' || field.type === 'anyOf') + ) { + const updatedField = JSON.parse(JSON.stringify(field)); + const options = updatedField.options; + + if ( + options && + Array.isArray(options) && + targetOptionIndex >= 0 && + targetOptionIndex < options.length && + options[targetOptionIndex].properties && + targetPropertyIndex >= 0 && + targetPropertyIndex < options[targetOptionIndex].properties.length + ) { + options[targetOptionIndex].properties.splice(targetPropertyIndex, 1); + } + + return updatedField; + } + + if (field.type === 'object' && (field as ObjectInput).properties) { + const objectField = field as ObjectInput; + return { + ...field, + properties: removePropertyFromOptionRecursive( + objectField.properties as FormField[], + targetFieldId, + targetOptionIndex, + targetPropertyIndex, + ), + } as FormField; + } + + if ( + (field.type === 'oneOf' || field.type === 'anyOf') && + (field as OneOfInput | AnyOfInput).options + ) { + const optionsField = field as OneOfInput | AnyOfInput; + return { + ...field, + options: optionsField.options.map((option) => ({ + ...option, + properties: removePropertyFromOptionRecursive( + option.properties as FormField[], + targetFieldId, + targetOptionIndex, + targetPropertyIndex, + ), + })), + } as FormField; + } + + if ( + field.type === 'array' && + field.arrayType === 'object' && + (field as FormArrayField).properties + ) { + const arrayField = field as FormArrayField; + return { + ...field, + properties: removePropertyFromOptionRecursive( + arrayField.properties as FormField[], + targetFieldId, + targetOptionIndex, + targetPropertyIndex, + ), + } as FormField; + } + + return field; + }); + }; + + setFormFields((prev) => + removePropertyFromOptionRecursive(prev, fieldId, optionIndex, propertyIndex), + ); + }, + [], + ); + + const updateObjectProperty = React.useCallback( + (fieldId: string, propertyIndex: number, updates: Partial<ConfigInput>) => { + updateFormFields( + formFields.map((field) => { + if (field.id !== fieldId) return field; + + const updatedField = JSON.parse(JSON.stringify(field)); + const objectInput = updatedField as ObjectInput; + + objectInput.properties[propertyIndex] = { + ...objectInput.properties[propertyIndex], + ...updates, + } as ConfigInput; + + return updatedField; + }) as FormField[], + ); + }, + [formFields, updateFormFields], + ); + + const updateOptionProperty = React.useCallback( + ( + fieldId: string, + optionIndex: number, + propertyIndex: number, + updates: Partial<ConfigInput>, + ) => { + const updatePropertyInOptionRecursive = ( + fields: FormField[], + targetFieldId: string, + targetOptionIndex: number, + targetPropertyIndex: number, + propertyUpdates: Partial<ConfigInput>, + ): FormField[] => { + return fields.map((field) => { + if ( + field.id === targetFieldId && + (field.type === 'oneOf' || field.type === 'anyOf') + ) { + const updatedField = JSON.parse(JSON.stringify(field)); + const options = updatedField.options; + + if ( + options && + Array.isArray(options) && + targetOptionIndex >= 0 && + targetOptionIndex < options.length && + options[targetOptionIndex].properties && + targetPropertyIndex >= 0 && + targetPropertyIndex < options[targetOptionIndex].properties.length + ) { + options[targetOptionIndex].properties[targetPropertyIndex] = { + ...options[targetOptionIndex].properties[targetPropertyIndex], + ...propertyUpdates, + } as ConfigInput; + } + + return updatedField; + } + + if (field.type === 'object' && (field as ObjectInput).properties) { + const objectField = field as ObjectInput; + return { + ...field, + properties: updatePropertyInOptionRecursive( + objectField.properties as FormField[], + targetFieldId, + targetOptionIndex, + targetPropertyIndex, + propertyUpdates, + ), + } as FormField; + } + + if ( + (field.type === 'oneOf' || field.type === 'anyOf') && + (field as OneOfInput | AnyOfInput).options + ) { + const optionsField = field as OneOfInput | AnyOfInput; + return { + ...field, + options: optionsField.options.map((option) => ({ + ...option, + properties: updatePropertyInOptionRecursive( + option.properties as FormField[], + targetFieldId, + targetOptionIndex, + targetPropertyIndex, + propertyUpdates, + ), + })), + } as FormField; + } + + if ( + field.type === 'array' && + field.arrayType === 'object' && + (field as FormArrayField).properties + ) { + const arrayField = field as FormArrayField; + return { + ...field, + properties: updatePropertyInOptionRecursive( + arrayField.properties as FormField[], + targetFieldId, + targetOptionIndex, + targetPropertyIndex, + propertyUpdates, + ), + } as FormField; + } + + return field; + }); + }; + + setFormFields((prev) => + updatePropertyInOptionRecursive(prev, fieldId, optionIndex, propertyIndex, updates), + ); + }, + [], + ); + + const addOption = React.useCallback( + (fieldId: string) => { + const addOptionRecursive = ( + fields: FormField[], + targetFieldId: string, + ): FormField[] => { + return fields.map((field) => { + if ( + field.id === targetFieldId && + (field.type === 'oneOf' || field.type === 'anyOf') + ) { + const updatedField = JSON.parse(JSON.stringify(field)); + const options = updatedField.options; + + if (options && Array.isArray(options)) { + const optionLetter = String.fromCharCode(65 + options.length); + const newOption = { + title: `Option ${optionLetter}`, + value: `option${optionLetter.toLowerCase()}`, + properties: [ + { + type: 'text', + name: 'textField', + title: 'Text Field', + id: generateId(), + } as FormField, + ], + }; + + options.push(newOption); + } + + return updatedField; + } + + if (field.type === 'object' && (field as ObjectInput).properties) { + const objectField = field as ObjectInput; + return { + ...field, + properties: addOptionRecursive( + objectField.properties as FormField[], + targetFieldId, + ), + } as FormField; + } + + if ( + (field.type === 'oneOf' || field.type === 'anyOf') && + (field as OneOfInput | AnyOfInput).options + ) { + const optionsField = field as OneOfInput | AnyOfInput; + return { + ...field, + options: optionsField.options.map((option) => ({ + ...option, + properties: addOptionRecursive( + option.properties as FormField[], + targetFieldId, + ), + })), + } as FormField; + } + + if ( + field.type === 'array' && + field.arrayType === 'object' && + (field as FormArrayField).properties + ) { + const arrayField = field as FormArrayField; + return { + ...field, + properties: addOptionRecursive( + arrayField.properties as FormField[], + targetFieldId, + ), + } as FormField; + } + + return field; + }); + }; + + setFormFields((prev) => addOptionRecursive(prev, fieldId)); + }, + [generateId], + ); + + const removeOption = React.useCallback( + (fieldId: string, optionIndex: number) => { + const removeOptionRecursive = ( + fields: FormField[], + targetFieldId: string, + targetOptionIndex: number, + ): FormField[] => { + return fields.map((field) => { + if ( + field.id === targetFieldId && + (field.type === 'oneOf' || field.type === 'anyOf') + ) { + const updatedField = JSON.parse(JSON.stringify(field)); + const options = updatedField.options; + + if ( + options && + Array.isArray(options) && + targetOptionIndex >= 0 && + targetOptionIndex < options.length + ) { + if (options.length > 1) { + options.splice(targetOptionIndex, 1); + } + } + + return updatedField; + } + + if (field.type === 'object' && (field as ObjectInput).properties) { + const objectField = field as ObjectInput; + return { + ...field, + properties: removeOptionRecursive( + objectField.properties as FormField[], + targetFieldId, + targetOptionIndex, + ), + } as FormField; + } + + if ( + (field.type === 'oneOf' || field.type === 'anyOf') && + (field as OneOfInput | AnyOfInput).options + ) { + const optionsField = field as OneOfInput | AnyOfInput; + return { + ...field, + options: optionsField.options.map((option) => ({ + ...option, + properties: removeOptionRecursive( + option.properties as FormField[], + targetFieldId, + targetOptionIndex, + ), + })), + } as FormField; + } + + if ( + field.type === 'array' && + field.arrayType === 'object' && + (field as FormArrayField).properties + ) { + const arrayField = field as FormArrayField; + return { + ...field, + properties: removeOptionRecursive( + arrayField.properties as FormField[], + targetFieldId, + targetOptionIndex, + ), + } as FormField; + } + + return field; + }); + }; + + updateFormFields(removeOptionRecursive(formFields, fieldId, optionIndex)); + }, + [formFields, updateFormFields], + ); + + const resetForm = React.useCallback(() => { + updateFormFields([]); + }, [updateFormFields]); + + return { + formFields, + createField, + addField, + removeField, + updateField, + addObjectProperty, + addOptionProperty, + removeObjectProperty, + removeOptionProperty, + updateObjectProperty, + updateOptionProperty, + addOption, + removeOption, + resetForm, + }; +}; diff --git a/src/form-builder/index.ts b/src/form-builder/index.ts new file mode 100644 index 0000000000..3cae346ba0 --- /dev/null +++ b/src/form-builder/index.ts @@ -0,0 +1,4 @@ +export * from './FormBuilder'; +export * from './hooks/FormContext'; +export * from './hooks/useFormFields'; +export * from './types'; diff --git a/src/form-builder/styles/variables.scss b/src/form-builder/styles/variables.scss new file mode 100644 index 0000000000..2cff7415e4 --- /dev/null +++ b/src/form-builder/styles/variables.scss @@ -0,0 +1 @@ +$ns-form-builder: 'pcformbuilder-'; diff --git a/src/form-builder/types.ts b/src/form-builder/types.ts new file mode 100644 index 0000000000..06830a31dc --- /dev/null +++ b/src/form-builder/types.ts @@ -0,0 +1,69 @@ +import { + AnyOfInput, + ArrayObjectInput, + ArrayTextInput, + ConfigInput, + ObjectInput, + OneOfInput, +} from '../form-generator'; + +export type FormField = ConfigInput & {id: string}; + +export type FormObjectField = ObjectInput & {id: string}; +export type FormArrayField = (ArrayTextInput | ArrayObjectInput) & { + id: string; + properties?: FormField[]; +}; +export type FormOneOfField = OneOfInput & {id: string}; +export type FormAnyOfField = AnyOfInput & {id: string}; +export type FormOptionsField = (OneOfInput | AnyOfInput) & {id: string}; + +export interface InputTypeMenuItem { + action: () => void; + text: string; + type: string; +} + +export interface ContentConfig { + [key: string]: unknown; +} + +export interface FormFieldsActions { + createField: (type: ConfigInput['type'], name?: string) => FormField; + addField: (type: ConfigInput['type']) => void; + removeField: (fieldId: string) => void; + updateField: (fieldId: string, updates: Partial<ConfigInput>) => void; + + addObjectProperty: (objectId: string, type: ConfigInput['type']) => void; + removeObjectProperty: (fieldId: string, propertyIndex: number) => void; + updateObjectProperty: ( + fieldId: string, + propertyIndex: number, + updates: Partial<ConfigInput>, + ) => void; + + addOption: (fieldId: string) => void; + removeOption: (fieldId: string, optionIndex: number) => void; + addOptionProperty: (fieldId: string, optionIndex: number, type?: ConfigInput['type']) => void; + removeOptionProperty: (fieldId: string, optionIndex: number, propertyIndex: number) => void; + updateOptionProperty: ( + fieldId: string, + optionIndex: number, + propertyIndex: number, + updates: Partial<ConfigInput>, + ) => void; + + resetForm: () => void; +} + +export interface FormData { + formFields: Array<FormField>; +} + +export interface FormContextType extends FormData, FormFieldsActions {} + +export interface FormBuilderProps { + className?: string; + formFields: Array<FormField>; + onChange?: (fields: FormField[]) => void; +} diff --git a/src/form-builder/utils/cn.ts b/src/form-builder/utils/cn.ts new file mode 100644 index 0000000000..ece877dc91 --- /dev/null +++ b/src/form-builder/utils/cn.ts @@ -0,0 +1,5 @@ +import {withNaming} from '@bem-react/classname'; + +export const FORM_BUILDER_NAMESPACE = 'pcformbuilder-'; + +export const formBuilderCn = withNaming({n: FORM_BUILDER_NAMESPACE, e: '__', m: '_'}); diff --git a/src/form-generator-v2/FormGenerator.scss b/src/form-generator-v2/FormGenerator.scss new file mode 100644 index 0000000000..3763938bda --- /dev/null +++ b/src/form-generator-v2/FormGenerator.scss @@ -0,0 +1,12 @@ +.pc-fg-generator { + position: relative; + padding: 0 12px; + + & > .pc-fg-fields > .pc-fg-base-input:first-child { + padding-top: 8px; + } + + & > .pc-fg-fields > .pc-fg-text:first-child { + padding-top: 8px; + } +} diff --git a/src/form-generator-v2/FormGenerator.tsx b/src/form-generator-v2/FormGenerator.tsx new file mode 100644 index 0000000000..8452f0a5ab --- /dev/null +++ b/src/form-generator-v2/FormGenerator.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; + +import {cloneDeep, set, unset} from 'lodash'; + +import {DynamicFormValue} from '../form-generator/types'; +import {ClassNameProps} from '../models/common'; + +import Fields from './components/Fields/Fields'; +import {Content, Fields as FieldsType, OnUpdate} from './types'; +import {formGeneratorCn} from './utils/cn'; +import {getValueByPath} from './utils/fields'; + +import './FormGenerator.scss'; + +const b = formGeneratorCn('generator'); + +type FormGeneratorProps = ClassNameProps & { + blockConfig: FieldsType; + contentConfig: Content; + onUpdate?: (content: Content) => void; + onUpdateByKey?: (key: string, value: unknown) => void; +}; + +const FormGenerator = ({ + blockConfig, + contentConfig, + onUpdate, + onUpdateByKey, + className, +}: FormGeneratorProps) => { + const contentRef = React.useRef(contentConfig); + // Sync during render (not in useEffect) so that onDataUpdate calls within the same + // effects batch all build on top of each other. If we synced in useEffect, it would + // run *after* child effects, resetting any accumulated changes made by onDataUpdate. + contentRef.current = contentConfig; + + const onDataUpdate = React.useCallback( + ( + key: string, + value: DynamicFormValue, + options?: {removeArrayItemAt?: number; unset?: boolean}, + ) => { + if (!onUpdate && !onUpdateByKey) { + return; + } + + const newContentConfig = cloneDeep(contentRef.current ?? {}); + + const removeAt = options?.removeArrayItemAt; + if (typeof removeAt === 'number') { + const arr = getValueByPath(newContentConfig, key); + if (Array.isArray(arr) && removeAt >= 0 && removeAt < arr.length) { + arr.splice(removeAt, 1); + } + } else if (options?.unset || value === undefined) { + unset(newContentConfig, key); + } else { + set(newContentConfig, key, value); + } + + contentRef.current = newContentConfig; + + if (onUpdateByKey) { + // `removeArrayItemAt`: callers like BlockConfigForm use lodash `set(path, value)` on + // store — passing `undefined` would wipe the whole array at `path`, not one element. + if (typeof removeAt === 'number') { + onUpdateByKey(key, getValueByPath(newContentConfig, key)); + } else { + onUpdateByKey(key, value); + } + } + + if (onUpdate) { + onUpdate(newContentConfig); + } + }, + [onUpdate, onUpdateByKey], + ); + + return ( + <div className={b(null, className)}> + <Fields + fields={blockConfig} + content={contentConfig} + onUpdate={onDataUpdate as OnUpdate} + /> + </div> + ); +}; + +export default FormGenerator; diff --git a/src/form-generator-v2/README.md b/src/form-generator-v2/README.md new file mode 100644 index 0000000000..5bd4d82bc8 --- /dev/null +++ b/src/form-generator-v2/README.md @@ -0,0 +1,176 @@ +# Form generator v2 + +Declarative block editor forms: a `Fields` array describes the UI. + +## `FormGenerator` component + +```tsx +import FormGenerator from './FormGenerator'; + +<FormGenerator blockConfig={fields} contentConfig={content} onUpdate={setContent} />; +``` + +| Prop | Type | Required | Description | +| --------------- | --------------------------------------- | -------- | --------------------------------------------------------- | +| `blockConfig` | `Fields` | yes | Array of field descriptors | +| `contentConfig` | `Content` | yes | Current form values object | +| `onUpdate` | `(content: Content) => void` | no | Called with the full updated content object on any change | +| `onUpdateByKey` | `(key: string, value: unknown) => void` | no | Called with the individual changed key/value pair | +| `className` | `string` | no | Extra CSS class for the root element | + +--- + +## Core concepts + +### `When` condition shape + +Conditions are evaluated in order; use `operator` without `field` for logical combinators between previous results. + +| Property | Type | Description | +| ---------- | -------------------------------- | ----------------------------------------- | +| `field` | `string` | Path in `content` (dot bracket segments). | +| `operator` | `string` | One of the string literals listed below. | +| `value` | `string` or `boolean` (optional) | Right-hand side for `===` / `!==`. | + +Permitted `operator` values (TypeScript union): + +```ts +'===' | '!==' | '||' | '&&'; +``` + +--- + +## `section` + +Collapsible group **or** repeating card group, depending on whether `index` is set. + +- **Static mode** (`index` absent): renders a collapsible panel with a toggle. +- **Array mode** (`index` present): renders a list of items, one per array entry, with add/delete controls. Field name paths use `{{indexName}}` placeholders that are replaced with the row index at render time. + +| Property | Type | Required | Description | +| --------------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------- | +| `type` | `'section'` | yes | Discriminator. | +| `title` | `string` | yes | Section heading (static mode) or row title template — may include `{{indexName}}` (array mode). | +| `fields` | `Fields` | yes | Nested form items. | +| `when` | `When` | no | Show section only when conditions pass. | +| `opened` | `boolean` | no | Initial expanded state. Static mode only. | +| `index` | `string` | no | Placeholder id used in `{{index}}` inside child `name` paths (e.g. `'index'`, `'index1'`). Presence activates array mode. | +| `withAddButton` | `boolean` | no | Show "Add" button to append a row. Array mode only. | +| `itemTitle` | `string` | no | Header text template for each array item — may include `{{indexName}}`. Array mode only. | +| `itemView` | `'card' \| 'clear'` | no | `card` = bordered Card with padding; `clear` = flat div. Array mode only. Defaults to `clear`. | + +**Array mode example** — `buttons[{{index}}].text` with `index: 'index'` resolves to `buttons[0].text`, `buttons[1].text`, etc. + +--- + +## `textInput` + +Single-line text. + +| Property | Type | Required | Description | +| -------------- | ------------- | -------- | ---------------------------------------------------------------------------------------- | +| `type` | `'textInput'` | yes | Discriminator. | +| `name` | `string` | yes | Path in `content`. | +| `title` | `string` | yes | Label. | +| `defaultValue` | `string` | no | Value written into `content` when the field first becomes visible and the path is empty. | +| `when` | `When` | no | Visibility. | + +--- + +## `textArea` + +Multi-line text. + +| Property | Type | Required | Description | +| -------------- | ------------ | -------- | ---------------------------------------------------------------------------------------- | +| `type` | `'textArea'` | yes | Discriminator. | +| `name` | `string` | yes | Path in `content`. | +| `title` | `string` | yes | Label. | +| `defaultValue` | `string` | no | Value written into `content` when the field first becomes visible and the path is empty. | +| `when` | `When` | no | Visibility. | + +--- + +## `select` + +Dropdown (single value). + +| Property | Type | Required | Description | +| -------------- | -------------------------------------------- | -------- | ----------------------------------------------------------- | +| `type` | `'select'` | yes | Discriminator. | +| `name` | `string` | yes | Path in `content`. | +| `title` | `string` | yes | Label. | +| `options` | `Array<{ value: string; content?: string }>` | yes | Options; `content` is the visible label, `value` is stored. | +| `defaultValue` | `string` | no | Pre-selected value on mount if the path is empty. | +| `hasClear` | `boolean` | no | Allow clearing the selection. | +| `when` | `When` | no | Visibility. | + +--- + +## `segmentedRadioGroup` + +Segmented control (mutually exclusive options). + +| Property | Type | Required | Description | +| -------------- | -------------------------------------------- | -------- | ----------------------------------------------------------- | +| `type` | `'segmentedRadioGroup'` | yes | Discriminator. | +| `name` | `string` | yes | Path in `content`. | +| `title` | `string` | yes | Label. | +| `options` | `Array<{ value: string; content?: string }>` | yes | Segments. | +| `defaultValue` | `string` | no | Written into `content` on mount if the path is still empty. | +| `when` | `When` | no | Visibility. | + +--- + +## `switch` + +Boolean toggle. + +| Property | Type | Required | Description | +| -------------- | ---------- | -------- | ------------------ | +| `type` | `'switch'` | yes | Discriminator. | +| `name` | `string` | yes | Path in `content`. | +| `title` | `string` | yes | Label. | +| `defaultValue` | `boolean` | no | Default Value. | +| `when` | `When` | no | Visibility. | + +--- + +## `colorInput` + +Color picker (Gravity UI `unstable_ColorPicker`). + +| Property | Type | Required | Description | +| -------------- | -------------- | -------- | ----------------------------------------------------- | +| `type` | `'colorInput'` | yes | Discriminator. | +| `name` | `string` | yes | Path in `content`. | +| `title` | `string` | yes | Label. | +| `defaultValue` | `string` | no | Hex color string. Falls back to `#000000` if omitted. | +| `when` | `When` | no | Visibility. | + +--- + +## `divider` + +Horizontal line for visual separation between fields — no value stored in `content`. + +| Property | Type | Required | Description | +| -------- | ----------- | -------- | -------------- | +| `type` | `'divider'` | yes | Discriminator. | +| `when` | `When` | no | Visibility. | + +--- + +## `text` + +Static hint text — no value stored in `content`. + +| Property | Type | Required | Description | +| -------- | -------------------- | -------- | ---------------------------------------------------------------------- | +| `type` | `'text'` | yes | Discriminator. | +| `text` | `string` | yes | Copy shown in the form. | +| `level` | `'danger' \| 'info'` | no | Applies a coloured background banner style. | +| `color` | `TextColor` | no | Gravity UI text color token (`'primary'`, `'hint'`, `'danger'`, etc.). | +| `when` | `When` | no | Visibility. | + +--- diff --git a/src/form-generator-v2/__stories__/ColorInput.stories.tsx b/src/form-generator-v2/__stories__/ColorInput.stories.tsx new file mode 100644 index 0000000000..4f0051578e --- /dev/null +++ b/src/form-generator-v2/__stories__/ColorInput.stories.tsx @@ -0,0 +1,38 @@ +import {Meta, StoryFn} from '@storybook/react'; + +import {useResultPanel} from '../../../.storybook/addons/result-addon/useResultPanel'; +import FormGenerator from '../FormGenerator'; +import {Content, Fields} from '../types'; + +export default { + title: 'FormGenerator/ColorInput', + component: FormGenerator, + parameters: { + layout: 'padded', + resultPanel: true, + }, +} as Meta<typeof FormGenerator>; + +const Template: StoryFn<{blockConfig: Fields}> = ({blockConfig}) => { + const [content, setContent] = useResultPanel<Content>({}); + return ( + <div style={{maxWidth: 600}}> + <FormGenerator + blockConfig={blockConfig} + contentConfig={content} + onUpdate={setContent} + /> + </div> + ); +}; + +export const Default = Template.bind({}); +Default.args = { + blockConfig: [ + { + type: 'colorInput', + name: 'color', + title: 'Color', + }, + ] as Fields, +}; diff --git a/src/form-generator-v2/__stories__/ConditionalVisibility.stories.tsx b/src/form-generator-v2/__stories__/ConditionalVisibility.stories.tsx new file mode 100644 index 0000000000..cdc5fb3cd4 --- /dev/null +++ b/src/form-generator-v2/__stories__/ConditionalVisibility.stories.tsx @@ -0,0 +1,231 @@ +import {Meta, StoryFn} from '@storybook/react'; + +import {useResultPanel} from '../../../.storybook/addons/result-addon/useResultPanel'; +import FormGenerator from '../FormGenerator'; +import {Content, Fields} from '../types'; + +export default { + title: 'FormGenerator/Conditional Visibility', + component: FormGenerator, + parameters: { + layout: 'padded', + resultPanel: true, + docs: { + description: { + component: + 'Field visibility is controlled by the `when` array on any field. ' + + 'Conditions support `===`, `!==` (compare a field value), and `&&`, `||` (logical combinators between previous results). ' + + 'When a hidden field had a value, it is automatically unset from content.', + }, + }, + }, +} as Meta<typeof FormGenerator>; + +const Template: StoryFn<{blockConfig: Fields}> = ({blockConfig}) => { + const [content, setContent] = useResultPanel<Content>({}); + return ( + <div style={{maxWidth: 600}}> + <FormGenerator + blockConfig={blockConfig} + contentConfig={content} + onUpdate={setContent} + /> + </div> + ); +}; + +/** + * Switch controls a whole group of fields. + * Any field type supports `when` — here a colorInput, a segmentedRadioGroup, + * and a textInput all appear only when the switch is on. + */ +export const SwitchGate = Template.bind({}); +SwitchGate.storyName = 'Switch gate (===)'; +SwitchGate.args = { + blockConfig: [ + { + type: 'switch', + name: 'showOverlay', + title: 'Show overlay', + }, + { + type: 'colorInput', + name: 'overlayColor', + title: 'Overlay Color', + when: [{field: 'showOverlay', operator: '===', value: true}], + }, + { + type: 'segmentedRadioGroup', + name: 'overlayOpacity', + title: 'Opacity', + defaultValue: '50', + when: [{field: 'showOverlay', operator: '===', value: true}], + options: [ + {value: '25', content: '25%'}, + {value: '50', content: '50%'}, + {value: '75', content: '75%'}, + {value: '100', content: '100%'}, + ], + }, + { + type: 'textInput', + name: 'overlayText', + title: 'Overlay Text', + defaultValue: 'This text is visible only when the overlay is shown', + when: [{field: 'showOverlay', operator: '===', value: true}], + }, + ] as Fields, +}; + +/** + * Select drives which branch of fields is shown. + * `text` → only text fields; `media` → only media fields; `mixed` → both. + * Achieved with `||` between two `===` conditions. + */ +export const SelectBranching = Template.bind({}); +SelectBranching.storyName = 'Select branching (=== + ||)'; +SelectBranching.args = { + blockConfig: [ + { + type: 'segmentedRadioGroup', + name: 'contentType', + title: 'Content type', + defaultValue: 'text', + options: [ + {value: 'text', content: 'Text'}, + {value: 'media', content: 'Media'}, + {value: 'mixed', content: 'Mixed'}, + ], + }, + { + type: 'textInput', + name: 'text', + title: 'Text', + defaultValue: 'This text is visible only when the content type is text', + when: [ + {field: 'contentType', operator: '===', value: 'text'}, + {operator: '||'}, + {field: 'contentType', operator: '===', value: 'mixed'}, + ], + }, + { + type: 'textArea', + name: 'caption', + title: 'Caption', + when: [ + {field: 'contentType', operator: '===', value: 'text'}, + {operator: '||'}, + {field: 'contentType', operator: '===', value: 'mixed'}, + ], + }, + { + type: 'textInput', + name: 'mediaUrl', + title: 'Media URL', + when: [ + {field: 'contentType', operator: '===', value: 'media'}, + {operator: '||'}, + {field: 'contentType', operator: '===', value: 'mixed'}, + ], + }, + { + type: 'select', + name: 'mediaFit', + title: 'Media Fit', + hasClear: true, + defaultValue: 'cover', + when: [ + {field: 'contentType', operator: '===', value: 'media'}, + {operator: '||'}, + {field: 'contentType', operator: '===', value: 'mixed'}, + ], + options: [ + {value: 'cover', content: 'Cover'}, + {value: 'contain', content: 'Contain'}, + {value: 'fill', content: 'Fill'}, + ], + }, + ] as Fields, +}; + +/** + * Two independent conditions combined with `&&`. + * The advanced field is only shown when mode is "advanced" AND the feature is enabled. + */ +export const AndCondition = Template.bind({}); +AndCondition.storyName = 'AND condition (&&)'; +AndCondition.args = { + blockConfig: [ + { + type: 'switch', + name: 'featureEnabled', + title: 'Enable feature', + }, + { + type: 'segmentedRadioGroup', + name: 'mode', + title: 'Mode', + defaultValue: 'simple', + options: [ + {value: 'simple', content: 'Simple'}, + {value: 'advanced', content: 'Advanced'}, + ], + }, + { + type: 'text', + text: 'Advanced settings appear only when the feature is enabled AND mode is Advanced.', + }, + { + type: 'textInput', + name: 'advancedConfig', + title: 'Advanced Config', + when: [ + {field: 'featureEnabled', operator: '===', value: true}, + {operator: '&&'}, + {field: 'mode', operator: '===', value: 'advanced'}, + ], + }, + { + type: 'colorInput', + name: 'advancedColor', + title: 'Advanced Color', + when: [ + {field: 'featureEnabled', operator: '===', value: true}, + {operator: '&&'}, + {field: 'mode', operator: '===', value: 'advanced'}, + ], + }, + ] as Fields, +}; + +/** + * A field hidden with `!==` — shown for everything except the excluded value. + */ +export const NotEqual = Template.bind({}); +NotEqual.storyName = 'Exclude value (!==)'; +NotEqual.args = { + blockConfig: [ + { + type: 'select', + name: 'theme', + title: 'Theme', + options: [ + {value: 'light', content: 'Light'}, + {value: 'dark', content: 'Dark'}, + {value: 'custom', content: 'Custom'}, + ], + }, + { + type: 'colorInput', + name: 'customColor', + title: 'Custom Color', + when: [{field: 'theme', operator: '===', value: 'custom'}], + }, + { + type: 'textInput', + name: 'subtitle', + title: 'Subtitle', + when: [{field: 'theme', operator: '!==', value: 'custom'}], + }, + ] as Fields, +}; diff --git a/src/form-generator-v2/__stories__/DefaultValues.stories.tsx b/src/form-generator-v2/__stories__/DefaultValues.stories.tsx new file mode 100644 index 0000000000..f32340a37d --- /dev/null +++ b/src/form-generator-v2/__stories__/DefaultValues.stories.tsx @@ -0,0 +1,135 @@ +import {Meta, StoryFn} from '@storybook/react'; + +import {useResultPanel} from '../../../.storybook/addons/result-addon/useResultPanel'; +import FormGenerator from '../FormGenerator'; +import {Content, Fields} from '../types'; + +export default { + title: 'FormGenerator/Default Values', + component: FormGenerator, + parameters: { + layout: 'padded', + resultPanel: true, + docs: { + description: { + component: + 'Fields support a `defaultValue` prop. When the field first becomes visible ' + + 'and its path in `content` is empty, the default value is written automatically. ' + + 'This works both on mount and when a hidden field appears via a `when` condition.', + }, + }, + }, +} as Meta<typeof FormGenerator>; + +const Template: StoryFn<{blockConfig: Fields}> = ({blockConfig}) => { + const [content, setContent] = useResultPanel<Content>({}); + return ( + <div style={{maxWidth: 600}}> + <FormGenerator + blockConfig={blockConfig} + contentConfig={content} + onUpdate={setContent} + /> + </div> + ); +}; + +/** + * Each supported field type with `defaultValue` set. + * Open the result panel to see values written into content on mount. + */ +export const FieldDefaultValues = Template.bind({}); +FieldDefaultValues.storyName = 'Default values across field types'; +FieldDefaultValues.args = { + blockConfig: [ + { + type: 'textInput', + name: 'title', + title: 'Title', + defaultValue: 'My page title', + }, + { + type: 'textArea', + name: 'description', + title: 'Description', + defaultValue: 'A short description of the page.', + }, + { + type: 'select', + name: 'layout', + title: 'Layout', + defaultValue: 'centered', + options: [ + {value: 'default', content: 'Default'}, + {value: 'centered', content: 'Centered'}, + {value: 'wide', content: 'Wide'}, + ], + }, + { + type: 'segmentedRadioGroup', + name: 'size', + title: 'Size', + defaultValue: 'md', + options: [ + {value: 'sm', content: 'S'}, + {value: 'md', content: 'M'}, + {value: 'lg', content: 'L'}, + ], + }, + { + type: 'colorInput', + name: 'accentColor', + title: 'Accent Color', + defaultValue: '#027bf3', + }, + ] as Fields, +}; + +/** + * A conditional field with a `defaultValue`. + * Enable the switch to reveal the hidden fields — their default values are + * written into content automatically when they appear. + * Disable the switch to hide them — values are cleared from content automatically. + */ +export const ConditionalDefaultValue = Template.bind({}); +ConditionalDefaultValue.storyName = 'Default value on conditional reveal'; +ConditionalDefaultValue.args = { + blockConfig: [ + { + type: 'switch', + name: 'enableBanner', + title: 'Enable banner', + }, + { + type: 'text', + text: 'Toggle the switch above. The fields below appear with pre-filled defaults.', + when: [{field: 'enableBanner', operator: '===', value: true}], + }, + { + type: 'textInput', + name: 'bannerTitle', + title: 'Banner Title', + defaultValue: 'Special offer', + when: [{field: 'enableBanner', operator: '===', value: true}], + }, + { + type: 'select', + name: 'bannerTheme', + title: 'Banner Theme', + defaultValue: 'info', + options: [ + {value: 'info', content: 'Info'}, + {value: 'warning', content: 'Warning'}, + {value: 'danger', content: 'Danger'}, + ], + when: [{field: 'enableBanner', operator: '===', value: true}], + }, + { + type: 'colorInput', + name: 'bannerColor', + title: 'Banner Color', + defaultValue: '#f5c518', + when: [{field: 'enableBanner', operator: '===', value: true}], + }, + ] as Fields, +}; diff --git a/src/form-generator-v2/__stories__/Divider.stories.tsx b/src/form-generator-v2/__stories__/Divider.stories.tsx new file mode 100644 index 0000000000..28a0bb3445 --- /dev/null +++ b/src/form-generator-v2/__stories__/Divider.stories.tsx @@ -0,0 +1,114 @@ +import {Meta, StoryFn} from '@storybook/react'; + +import {useResultPanel} from '../../../.storybook/addons/result-addon/useResultPanel'; +import FormGenerator from '../FormGenerator'; +import {Content, Fields} from '../types'; + +export default { + title: 'FormGenerator/Divider', + component: FormGenerator, + parameters: { + layout: 'padded', + resultPanel: true, + docs: { + description: { + component: + 'A horizontal divider for visual separation between form fields. ' + + 'Has no `name` and writes nothing to content. Supports `when` like any other field.', + }, + }, + }, +} as Meta<typeof FormGenerator>; + +const Template: StoryFn<{blockConfig: Fields}> = ({blockConfig}) => { + const [content, setContent] = useResultPanel<Content>({}); + return ( + <div style={{maxWidth: 600}}> + <FormGenerator + blockConfig={blockConfig} + contentConfig={content} + onUpdate={setContent} + /> + </div> + ); +}; + +export const Default = Template.bind({}); +Default.args = { + blockConfig: [ + { + type: 'textInput', + name: 'title', + title: 'Title', + }, + { + type: 'divider', + }, + { + type: 'textArea', + name: 'description', + title: 'Description', + }, + ] as Fields, +}; + +export const BetweenSections = Template.bind({}); +BetweenSections.storyName = 'Between sections'; +BetweenSections.args = { + blockConfig: [ + { + type: 'text', + text: 'General settings', + color: 'secondary', + }, + { + type: 'textInput', + name: 'title', + title: 'Title', + }, + { + type: 'divider', + }, + { + type: 'text', + text: 'Appearance', + color: 'secondary', + }, + { + type: 'colorInput', + name: 'backgroundColor', + title: 'Background', + }, + { + type: 'colorInput', + name: 'textColor', + title: 'Text color', + }, + ] as Fields, +}; + +export const Conditional = Template.bind({}); +Conditional.args = { + blockConfig: [ + { + type: 'switch', + name: 'showAdvanced', + title: 'Show advanced settings', + }, + { + type: 'textInput', + name: 'title', + title: 'Title', + }, + { + type: 'divider', + when: [{field: 'showAdvanced', operator: '===', value: true}], + }, + { + type: 'textInput', + name: 'customClass', + title: 'Custom CSS class', + when: [{field: 'showAdvanced', operator: '===', value: true}], + }, + ] as Fields, +}; diff --git a/src/form-generator-v2/__stories__/FormGenerator.stories.tsx b/src/form-generator-v2/__stories__/FormGenerator.stories.tsx new file mode 100644 index 0000000000..c0d0b2e5ca --- /dev/null +++ b/src/form-generator-v2/__stories__/FormGenerator.stories.tsx @@ -0,0 +1,769 @@ +import { + ArrowRight, + CircleInfoFill, + Display, + Eye, + Flask, + Globe, + GraduationCap, + Grip, + Key, + Lock, + Microscope, + Person, + Rocket, + Shield, + StarFill, + TriangleExclamationFill, + Wrench, +} from '@gravity-ui/icons'; +import {Card, Icon, Label, Progress, Text, ThemeProvider, User, UserLabel} from '@gravity-ui/uikit'; +import {Meta, StoryFn} from '@storybook/react'; + +import {useResultPanel} from '../../../.storybook/addons/result-addon/useResultPanel'; +import FormGenerator from '../FormGenerator'; +import {Content, Fields} from '../types'; + +export default { + title: 'FormGenerator', + component: FormGenerator, + parameters: { + layout: 'fullscreen', + resultPanel: true, + docs: { + description: { + component: + 'FormGenerator is a declarative form builder driven entirely by a JSON/YAML config (`blockConfig`). ' + + 'You describe the shape of the form — field types, labels, options, and visibility rules — ' + + 'and FormGenerator renders it, manages internal state, and reports changes via `onUpdate` or `onUpdateByKey`. ' + + '\n\n' + + 'Supported field types: `textInput`, `textArea`, `select`, `segmentedRadioGroup`, `switch`, `colorInput`, `section`, `text`. ' + + 'Any field can be shown or hidden conditionally using the `when` array. ' + + 'Sections can be static (collapsible) or repeating (array mode via `index`). ' + + 'When a hidden field had a value, it is automatically removed from content.', + }, + }, + }, +} as Meta<typeof FormGenerator>; + +// ─── Interstellar prefill ──────────────────────────────────────────────────── + +const INTERSTELLAR_PREFILL: Content = { + shipName: 'Endurance', + registrationNo: 'NASA-EX-2067', + missionLog: + "Crew: proceed through the wormhole near Saturn. Primary targets: Miller's Planet, Mann's Planet, Edmunds' Planet. Duration: indeterminate. The gravity equation must be solved — humanity's survival depends on it. Do not return without coordinates. — Prof. Brand, NASA", + shipClass: 'station', + faction: 'nasa', + hullColor: '#1a3a5c', + aiCopilot: true, + tarsHonesty: '100', + tarsHumor: '75', + tarsCharm: '0', + tarsBulkApperception: '75', + hyperdrive: 'class1', + hullManufacturer: 'Lockheed Martin', + crew: [ + {name: 'Cooper', role: 'pilot', species: 'human'}, + {name: 'Dr. Brand', role: 'medic', species: 'human'}, + {name: 'Romilly', role: 'engineer', species: 'human'}, + {name: 'TARS', role: 'captain', species: 'droid'}, + ], + routes: [ + {from: 'Earth', to: 'Saturn Orbit', warp: 'warp1'}, + {from: 'Saturn Orbit', to: 'Wormhole', warp: 'hyperspace'}, + {from: 'Wormhole', to: "Miller's Planet", warp: 'warp2'}, + ], +}; + +// ─── Preview helpers ───────────────────────────────────────────────────────── + +const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); + +const FACTION_THEME = { + nasa: 'info', + esa: 'success', + cnsa: 'warning', + lazarus: 'unknown', + unknown: 'danger', +} as const; + +const FACTION_LABEL: Record<string, string> = { + nasa: 'NASA', + esa: 'ESA', + cnsa: 'CNSA', + lazarus: 'Lazarus Mission', + unknown: 'Unknown', +}; + +const WEAPON_LABEL: Record<string, string> = { + lasers: 'Laser Cannons', + ion: 'Ion Torpedoes', + turbo: 'Turbolasers', + thermal: 'Thermal Detonators', +}; + +const ROLE_ICON: Record<string, React.ReactNode> = { + captain: <Icon data={StarFill} size={12} />, + pilot: <Icon data={Rocket} size={12} />, + engineer: <Icon data={Wrench} size={12} />, + gunner: <Icon data={Shield} size={12} />, + medic: <Icon data={Flask} size={12} />, + smuggler: <Icon data={Key} size={12} />, + scientist: <Icon data={Microscope} size={12} />, +}; + +const SPECIES_ICON: Record<string, React.ReactNode> = { + human: <Icon data={Person} size={12} />, + wookiee: <Icon data={GraduationCap} size={12} />, + droid: <Icon data={Display} size={12} />, + twilek: <Icon data={Eye} size={12} />, +}; + +const WARP_PROGRESS: Record<string, number> = { + warp1: 22, + warp2: 45, + warp3: 68, + jump: 85, + hyperspace: 100, +}; + +const WARP_PROGRESS_THEME: Record<string, 'default' | 'info' | 'success' | 'warning' | 'danger'> = { + warp1: 'default', + warp2: 'info', + warp3: 'warning', + jump: 'warning', + hyperspace: 'success', +}; + +const WARP_LABEL_THEME: Record<string, 'normal' | 'info' | 'success' | 'warning' | 'danger'> = { + warp1: 'normal', + warp2: 'info', + warp3: 'warning', + jump: 'warning', + hyperspace: 'success', +}; + +// ─── Mission Brief ──────────────────────────────────────────────────────────── + +const MissionBrief = ({text}: {text: string}) => ( + <Card + style={{ + padding: '16px 20px', + borderLeft: '3px solid var(--g-color-base-brand)', + }} + > + <div style={{display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8}}> + <span style={{color: 'var(--g-color-text-hint)', display: 'flex'}}> + <Icon data={Lock} size={14} /> + </span> + <Text variant="caption-2" color="hint"> + CLASSIFIED — MISSION BRIEF + </Text> + </div> + <Text variant="body-2">{text}</Text> + </Card> +); + +// ─── StarshipPreview ────────────────────────────────────────────────────────── + +interface CrewMember { + name?: string; + role?: string; + species?: string; +} + +interface Route { + from?: string; + to?: string; + warp?: string; +} + +interface StatusBadgesProps { + faction?: string; + factionTheme: string; + shipClass?: string; + armed?: boolean; + weaponSystem?: string; +} + +const StatusBadges = ({ + faction, + factionTheme, + shipClass, + armed, + weaponSystem, +}: StatusBadgesProps) => ( + <div style={{display: 'flex', gap: 6, flexWrap: 'wrap'}}> + {faction && ( + <Label theme={factionTheme as Parameters<typeof Label>[0]['theme']} size="s"> + {FACTION_LABEL[faction] ?? faction} + </Label> + )} + {shipClass && ( + <Label theme="normal" size="s" icon={<Icon data={Globe} size={12} />}> + {capitalize(shipClass)} + </Label> + )} + <Label + theme={armed ? 'danger' : 'normal'} + size="s" + icon={armed ? <Icon data={TriangleExclamationFill} size={12} /> : undefined} + > + {armed ? 'Armed' : 'Unarmed'} + </Label> + {armed && weaponSystem && ( + <Label theme="warning" size="s"> + {WEAPON_LABEL[weaponSystem] ?? weaponSystem} + </Label> + )} + </div> +); + +const CrewManifest = ({crew}: {crew: CrewMember[]}) => ( + <div> + <div style={{display: 'flex', alignItems: 'center', gap: 6, marginBottom: 10}}> + <span style={{color: 'var(--g-color-text-hint)', display: 'flex'}}> + <Icon data={Grip} size={14} /> + </span> + <Text variant="caption-2" color="hint"> + CREW MANIFEST + </Text> + </div> + {crew.length > 0 ? ( + <div style={{display: 'flex', flexDirection: 'column', gap: 8}}> + {crew.map((member, i) => ( + <div + key={i} + style={{display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap'}} + > + <UserLabel + text={member.name || '—'} + type={member.species === 'droid' ? 'empty' : 'person'} + size="s" + /> + {member.role && ( + <Label theme="normal" size="s" icon={ROLE_ICON[member.role]}> + {capitalize(member.role)} + </Label> + )} + {member.species && ( + <Label theme="unknown" size="s" icon={SPECIES_ICON[member.species]}> + {capitalize(member.species)} + </Label> + )} + </div> + ))} + </div> + ) : ( + <Text variant="body-1" color="hint"> + No crew assigned + </Text> + )} + </div> +); + +const FlightPlan = ({routes}: {routes: Route[]}) => ( + <div> + <div style={{display: 'flex', alignItems: 'center', gap: 6, marginBottom: 10}}> + <span style={{color: 'var(--g-color-text-hint)', display: 'flex'}}> + <Icon data={CircleInfoFill} size={14} /> + </span> + <Text variant="caption-2" color="hint"> + FLIGHT PLAN + </Text> + </div> + {routes.length > 0 ? ( + <div style={{display: 'flex', flexDirection: 'column', gap: 12}}> + {routes.map((route, i) => { + const warpProgress = route.warp ? (WARP_PROGRESS[route.warp] ?? 50) : 50; + const warpProgressTheme = route.warp + ? (WARP_PROGRESS_THEME[route.warp] ?? 'default') + : 'default'; + const warpLabelTheme = route.warp + ? (WARP_LABEL_THEME[route.warp] ?? 'normal') + : 'normal'; + return ( + <div key={i}> + <div + style={{ + display: 'flex', + alignItems: 'center', + gap: 6, + marginBottom: 4, + }} + > + <Text variant="body-2">{route.from || '—'}</Text> + <span style={{color: 'var(--g-color-text-hint)', display: 'flex'}}> + <Icon data={ArrowRight} size={12} /> + </span> + <Text variant="body-2">{route.to || '—'}</Text> + {route.warp && ( + <Label theme={warpLabelTheme} size="xs"> + {capitalize(route.warp)} + </Label> + )} + </div> + <Progress + value={warpProgress} + theme={warpProgressTheme} + size="xs" + text={`${warpProgress}% drive capacity`} + /> + </div> + ); + })} + </div> + ) : ( + <Text variant="body-1" color="hint"> + No routes logged + </Text> + )} + </div> +); + +const StarshipPreview = ({content}: {content: Content}) => { + const shipName = content.shipName as string | undefined; + const registrationNo = content.registrationNo as string | undefined; + const shipClass = content.shipClass as string | undefined; + const faction = content.faction as string | undefined; + const hullColor = content.hullColor as string | undefined; + const armed = content.armed as boolean | undefined; + const weaponSystem = content.weaponSystem as string | undefined; + const hyperdrive = content.hyperdrive as string | undefined; + const aiCopilot = content.aiCopilot as boolean | undefined; + const hullManufacturer = content.hullManufacturer as string | undefined; + const tarsHonesty = parseInt((content.tarsHonesty as string) ?? '90', 10); + const tarsHumor = parseInt((content.tarsHumor as string) ?? '75', 10); + const tarsCharm = parseInt((content.tarsCharm as string) ?? '0', 10); + const tarsBulkApperception = parseInt((content.tarsBulkApperception as string) ?? '85', 10); + const crew = ((content.crew as CrewMember[] | undefined) ?? []).filter(Boolean); + const routes = ((content.routes as Route[] | undefined) ?? []).filter(Boolean); + + const factionTheme = faction + ? (FACTION_THEME[faction as keyof typeof FACTION_THEME] ?? 'normal') + : 'normal'; + + return ( + <Card style={{padding: 20, display: 'flex', flexDirection: 'column', gap: 20}}> + {/* Header */} + <div> + <Text variant="header-1">{shipName || '— Unnamed Vessel —'}</Text> + {registrationNo && ( + <div> + <Text variant="caption-1" color="hint"> + REG: {registrationNo} + </Text> + </div> + )} + </div> + + {/* Status badges */} + <StatusBadges + faction={faction} + factionTheme={factionTheme} + shipClass={shipClass} + armed={armed} + weaponSystem={weaponSystem} + /> + + {/* Hull color */} + {hullColor && ( + <div style={{display: 'flex', alignItems: 'center', gap: 8}}> + <div + style={{ + width: 18, + height: 18, + borderRadius: 3, + background: hullColor, + border: '1px solid var(--g-color-line-generic)', + flexShrink: 0, + }} + /> + <Text variant="body-1" color="secondary"> + {hullColor} + </Text> + {hullManufacturer && ( + <Text variant="caption-1" color="hint"> + · {hullManufacturer} + </Text> + )} + </div> + )} + + {/* TARS personality matrix (when AI copilot enabled) */} + {aiCopilot && ( + <div> + <div style={{display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12}}> + <User + name="TARS" + description="Tactical Assistance & Rescue System" + size="s" + avatar={{text: 'TA', theme: 'brand'}} + /> + </div> + <div style={{display: 'flex', flexDirection: 'column', gap: 6}}> + {[ + {label: 'Honesty', value: tarsHonesty, theme: 'success' as const}, + {label: 'Humor', value: tarsHumor, theme: 'info' as const}, + {label: 'Charm', value: tarsCharm, theme: 'danger' as const}, + { + label: 'Bulk Apperception', + value: tarsBulkApperception, + theme: 'info' as const, + }, + ].map(({label, value, theme}) => ( + <div key={label}> + <div + style={{ + display: 'flex', + justifyContent: 'space-between', + marginBottom: 2, + }} + > + <Text variant="caption-1" color="secondary"> + {label} + </Text> + <Text variant="caption-1" color="hint"> + {value}% + </Text> + </div> + <Progress value={value} theme={theme} size="xs" /> + </div> + ))} + </div> + </div> + )} + + {!aiCopilot && hyperdrive && ( + <div style={{display: 'flex', gap: 6, flexWrap: 'wrap'}}> + <div style={{display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4}}> + <span style={{color: 'var(--g-color-text-hint)', display: 'flex'}}> + <Icon data={Display} size={14} /> + </span> + <Text variant="caption-2" color="hint"> + SHIP SYSTEMS + </Text> + </div> + <Label theme="normal" size="s"> + Hyperdrive: {capitalize(hyperdrive)} + </Label> + <Label theme="normal" size="s"> + AI: Offline + </Label> + </div> + )} + + {/* Crew */} + <CrewManifest crew={crew} /> + + {/* Flight routes */} + <FlightPlan routes={routes} /> + </Card> + ); +}; + +// ─── Template ───────────────────────────────────────────────────────────────── + +const Template: StoryFn<{blockConfig: Fields}> = ({blockConfig}) => { + const [content, setContent] = useResultPanel<Content>(INTERSTELLAR_PREFILL); + const missionLog = content.missionLog as string | undefined; + + return ( + <ThemeProvider theme="dark"> + <div + style={{ + padding: 24, + minHeight: '100vh', + background: 'var(--g-color-base-background)', + boxSizing: 'border-box', + }} + > + <div style={{display: 'flex', gap: 24}}> + <div + style={{ + flex: 1, + minWidth: 280, + display: 'flex', + flexDirection: 'column', + gap: 24, + position: 'sticky', + top: 24, + alignSelf: 'flex-start', + }} + > + {missionLog && <MissionBrief text={missionLog} />} + <StarshipPreview content={content} /> + </div> + <div style={{width: 530, flexShrink: 0}}> + <Card style={{overflow: 'hidden'}}> + <div + style={{ + padding: '8px 16px', + borderBottom: '1px solid var(--g-color-line-generic)', + }} + > + <Text variant="caption-1" color="hint"> + GALACTIC REGISTRY — TERMINAL v4.2 + </Text> + </div> + <FormGenerator + blockConfig={blockConfig} + contentConfig={content} + onUpdate={setContent} + /> + </Card> + </div> + </div> + </div> + </ThemeProvider> + ); +}; + +// ─── Stories ────────────────────────────────────────────────────────────────── + +export const Overview = Template.bind({}); +Overview.args = { + blockConfig: [ + { + type: 'text', + level: 'info', + text: 'Authorized personnel only. Unauthorized access will trigger Lazarus Protocol.', + }, + { + type: 'textInput', + name: 'shipName', + title: 'Ship Name', + }, + { + type: 'textInput', + name: 'registrationNo', + title: 'Registration No.', + }, + { + type: 'textArea', + name: 'missionLog', + title: 'Mission Log', + }, + { + type: 'segmentedRadioGroup', + name: 'shipClass', + title: 'Ship Class', + options: [ + {value: 'ranger', content: 'Ranger'}, + {value: 'lander', content: 'Lander'}, + {value: 'station', content: 'Station'}, + {value: 'probe', content: 'Probe'}, + ], + }, + { + type: 'select', + name: 'faction', + title: 'Affiliation', + hasClear: true, + options: [ + {value: 'nasa', content: 'NASA'}, + {value: 'esa', content: 'ESA'}, + {value: 'cnsa', content: 'CNSA'}, + {value: 'lazarus', content: 'Lazarus Mission'}, + {value: 'unknown', content: 'Unknown'}, + ], + }, + { + type: 'colorInput', + name: 'hullColor', + title: 'Hull Color', + }, + { + type: 'switch', + name: 'armed', + title: 'Armed', + }, + { + type: 'select', + name: 'weaponSystem', + title: 'Weapon System', + when: [{field: 'armed', operator: '===', value: true}], + options: [ + {value: 'lasers', content: 'Laser Cannons'}, + {value: 'ion', content: 'Ion Torpedoes'}, + {value: 'turbo', content: 'Turbolasers'}, + {value: 'thermal', content: 'Thermal Detonators'}, + ], + }, + { + type: 'text', + level: 'danger', + text: 'Armed Probe detected. Galactic Senate has been notified.', + when: [ + {field: 'armed', operator: '===', value: true}, + {operator: '&&'}, + {field: 'shipClass', operator: '===', value: 'probe'}, + ], + }, + { + type: 'section', + title: 'Ship Systems', + opened: true, + fields: [ + { + type: 'text', + text: 'Internal systems configuration.', + color: 'secondary', + }, + { + type: 'segmentedRadioGroup', + name: 'hyperdrive', + title: 'Hyperdrive Class', + options: [ + {value: 'class1', content: 'Class 1'}, + {value: 'class2', content: 'Class 2'}, + {value: 'none', content: 'None'}, + ], + }, + { + type: 'switch', + name: 'aiCopilot', + title: 'AI Copilot (TARS)', + }, + { + type: 'textInput', + name: 'hullManufacturer', + title: 'Hull Manufacturer', + }, + ], + }, + { + type: 'section', + title: 'TARS Personality Matrix', + opened: true, + when: [{field: 'aiCopilot', operator: '===', value: true}], + fields: [ + { + type: 'text', + text: 'Adjust TARS personality parameters. Setting Honesty below 50% is not recommended.', + color: 'secondary', + }, + { + type: 'segmentedRadioGroup', + name: 'tarsHonesty', + title: 'Honesty', + options: [ + {value: '0', content: '0%'}, + {value: '25', content: '25%'}, + {value: '50', content: '50%'}, + {value: '75', content: '75%'}, + {value: '100', content: '100%'}, + ], + }, + { + type: 'segmentedRadioGroup', + name: 'tarsHumor', + title: 'Humor', + options: [ + {value: '0', content: '0%'}, + {value: '25', content: '25%'}, + {value: '50', content: '50%'}, + {value: '75', content: '75%'}, + {value: '100', content: '100%'}, + ], + }, + { + type: 'segmentedRadioGroup', + name: 'tarsCharm', + title: 'Charm', + options: [ + {value: '0', content: '0%'}, + {value: '25', content: '25%'}, + {value: '50', content: '50%'}, + {value: '75', content: '75%'}, + {value: '100', content: '100%'}, + ], + }, + { + type: 'segmentedRadioGroup', + name: 'tarsBulkApperception', + title: 'Bulk Apperception', + options: [ + {value: '0', content: '0%'}, + {value: '25', content: '25%'}, + {value: '50', content: '50%'}, + {value: '75', content: '75%'}, + {value: '100', content: '100%'}, + ], + }, + ], + }, + { + type: 'section', + title: 'Crew Members', + index: 'index', + withAddButton: true, + itemTitle: 'Crew Member {{index}}', + itemView: 'card', + fields: [ + { + type: 'textInput', + name: 'crew[{{index}}].name', + title: 'Name', + }, + { + type: 'select', + name: 'crew[{{index}}].role', + title: 'Role', + options: [ + {value: 'captain', content: 'Captain'}, + {value: 'pilot', content: 'Pilot'}, + {value: 'engineer', content: 'Engineer'}, + {value: 'gunner', content: 'Gunner'}, + {value: 'medic', content: 'Medic'}, + {value: 'smuggler', content: 'Smuggler'}, + ], + }, + { + type: 'segmentedRadioGroup', + name: 'crew[{{index}}].species', + title: 'Species', + options: [ + {value: 'human', content: 'Human'}, + {value: 'wookiee', content: 'Wookiee'}, + {value: 'droid', content: 'Droid'}, + {value: 'twilek', content: "Twi'lek"}, + ], + }, + ], + }, + { + type: 'section', + title: 'Flight Routes', + index: 'index', + withAddButton: true, + itemTitle: 'Route {{index}}', + itemView: 'clear', + fields: [ + { + type: 'textInput', + name: 'routes[{{index}}].from', + title: 'From', + }, + { + type: 'textInput', + name: 'routes[{{index}}].to', + title: 'To', + }, + { + type: 'select', + name: 'routes[{{index}}].warp', + title: 'Warp Class', + options: [ + {value: 'warp1', content: 'Warp 1'}, + {value: 'warp2', content: 'Warp 2'}, + {value: 'warp3', content: 'Warp 3'}, + {value: 'jump', content: 'Jump'}, + {value: 'hyperspace', content: 'Hyperspace'}, + ], + }, + ], + }, + ] as Fields, +}; diff --git a/src/form-generator-v2/__stories__/PrePopulated.stories.tsx b/src/form-generator-v2/__stories__/PrePopulated.stories.tsx new file mode 100644 index 0000000000..acc8018591 --- /dev/null +++ b/src/form-generator-v2/__stories__/PrePopulated.stories.tsx @@ -0,0 +1,165 @@ +import {Meta, StoryFn} from '@storybook/react'; + +import {useResultPanel} from '../../../.storybook/addons/result-addon/useResultPanel'; +import FormGenerator from '../FormGenerator'; +import {Content, Fields} from '../types'; + +export default { + title: 'FormGenerator/Pre-Populated', + component: FormGenerator, + parameters: { + layout: 'padded', + resultPanel: true, + docs: { + description: { + component: + 'Pass existing data via `contentConfig` to render the form in edit mode. ' + + 'All fields render with their current values pre-filled. ' + + 'This is the typical pattern when editing an already-saved block.', + }, + }, + }, +} as Meta<typeof FormGenerator>; + +const blockConfig: Fields = [ + { + type: 'textInput', + name: 'title', + title: 'Title', + }, + { + type: 'textArea', + name: 'description', + title: 'Description', + }, + { + type: 'select', + name: 'layout', + title: 'Layout', + hasClear: true, + options: [ + {value: 'default', content: 'Default'}, + {value: 'centered', content: 'Centered'}, + {value: 'wide', content: 'Wide'}, + ], + }, + { + type: 'segmentedRadioGroup', + name: 'size', + title: 'Size', + options: [ + {value: 'sm', content: 'S'}, + {value: 'md', content: 'M'}, + {value: 'lg', content: 'L'}, + ], + }, + { + type: 'switch', + name: 'showMedia', + title: 'Show media', + }, + { + type: 'section', + title: 'Media', + when: [{field: 'showMedia', operator: '===', value: true}], + opened: true, + fields: [ + { + type: 'textInput', + name: 'media.src', + title: 'Image URL', + }, + { + type: 'textInput', + name: 'media.alt', + title: 'Alt text', + }, + { + type: 'colorInput', + name: 'media.overlayColor', + title: 'Overlay Color', + }, + ], + }, + { + type: 'section', + title: 'Button', + index: 'i', + withAddButton: true, + itemTitle: 'Button {{i}}', + itemView: 'card', + fields: [ + { + type: 'textInput', + name: 'buttons[{{i}}].text', + title: 'Label', + }, + { + type: 'textInput', + name: 'buttons[{{i}}].url', + title: 'URL', + }, + { + type: 'select', + name: 'buttons[{{i}}].theme', + title: 'Theme', + options: [ + {value: 'action', content: 'Action'}, + {value: 'outlined', content: 'Outlined'}, + {value: 'normal', content: 'Normal'}, + ], + }, + ], + }, +]; + +const Template: StoryFn<{initialContent: Content}> = ({initialContent}) => { + const [content, setContent] = useResultPanel<Content>(initialContent); + return ( + <div style={{maxWidth: 600}}> + <FormGenerator + blockConfig={blockConfig} + contentConfig={content} + onUpdate={setContent} + /> + </div> + ); +}; + +/** + * All fields pre-filled — simulates opening an already-saved block for editing. + */ +export const EditMode = Template.bind({}); +EditMode.storyName = 'Edit mode (fully pre-filled)'; +EditMode.args = { + initialContent: { + title: 'Getting started with page-constructor', + description: 'A comprehensive guide to building pages with the block editor.', + layout: 'centered', + size: 'lg', + showMedia: true, + media: { + src: 'https://example.com/hero.jpg', + alt: 'Hero image', + overlayColor: '#00000066', + }, + buttons: [ + {text: 'Read docs', url: '/docs', theme: 'action'}, + {text: 'GitHub', url: 'https://github.com', theme: 'outlined'}, + ], + }, +}; + +/** + * Only some fields pre-filled — the rest remain empty. + * Common when a block was saved with minimal data and the user needs to fill in the rest. + */ +export const PartialData = Template.bind({}); +PartialData.storyName = 'Partial data (edit incomplete block)'; +PartialData.args = { + initialContent: { + title: 'Untitled block', + layout: 'default', + showMedia: false, + }, +}; diff --git a/src/form-generator-v2/__stories__/Section.stories.tsx b/src/form-generator-v2/__stories__/Section.stories.tsx new file mode 100644 index 0000000000..e8cc8d6e8b --- /dev/null +++ b/src/form-generator-v2/__stories__/Section.stories.tsx @@ -0,0 +1,332 @@ +import {Meta, StoryFn} from '@storybook/react'; + +import {useResultPanel} from '../../../.storybook/addons/result-addon/useResultPanel'; +import FormGenerator from '../FormGenerator'; +import {Content, Fields} from '../types'; + +export default { + title: 'FormGenerator/Section', + component: FormGenerator, + parameters: { + layout: 'padded', + resultPanel: true, + }, +} as Meta<typeof FormGenerator>; + +const Template: StoryFn<{blockConfig: Fields}> = ({blockConfig}) => { + const [content, setContent] = useResultPanel<Content>({}); + return ( + <div style={{maxWidth: 600}}> + <FormGenerator + blockConfig={blockConfig} + contentConfig={content} + onUpdate={setContent} + /> + </div> + ); +}; + +export const Collapsible = Template.bind({}); +Collapsible.storyName = 'Collapsible (collapsed by default)'; +Collapsible.args = { + blockConfig: [ + { + type: 'textInput', + name: 'title', + title: 'Title', + }, + { + type: 'section', + title: 'Advanced Settings', + fields: [ + { + type: 'textInput', + name: 'slug', + title: 'Slug', + }, + { + type: 'select', + name: 'theme', + title: 'Theme', + options: [ + {value: 'light', content: 'Light'}, + {value: 'dark', content: 'Dark'}, + ], + }, + ], + }, + ] as Fields, +}; + +export const OpenedByDefault = Template.bind({}); +OpenedByDefault.storyName = 'Opened by Default'; +OpenedByDefault.args = { + blockConfig: [ + { + type: 'section', + title: 'Settings (opened by default)', + opened: true, + fields: [ + { + type: 'textInput', + name: 'name', + title: 'Name', + }, + { + type: 'textArea', + name: 'bio', + title: 'Bio', + }, + ], + }, + ] as Fields, +}; + +export const WithInfoNote = Template.bind({}); +WithInfoNote.storyName = 'With Info Note'; +WithInfoNote.args = { + blockConfig: [ + { + type: 'section', + title: 'Analytics', + fields: [ + { + type: 'text', + text: 'Analytics data is collected anonymously.', + level: 'info', + }, + { + type: 'switch', + name: 'analytics.enabled', + title: 'Enable Analytics', + }, + { + type: 'textInput', + name: 'analytics.id', + title: 'Tracking ID', + }, + ], + }, + ] as Fields, +}; + +export const WithDangerNote = Template.bind({}); +WithDangerNote.storyName = 'With Danger Note'; +WithDangerNote.args = { + blockConfig: [ + { + type: 'section', + title: 'Danger Zone', + fields: [ + { + type: 'text', + text: 'Changes here may break your page layout.', + level: 'danger', + }, + { + type: 'textInput', + name: 'customCss', + title: 'Custom CSS Class', + }, + { + type: 'textInput', + name: 'customId', + title: 'Custom Element ID', + }, + ], + }, + ] as Fields, +}; + +export const ArraySection = Template.bind({}); +ArraySection.storyName = 'Array Section (repeating items)'; +ArraySection.args = { + blockConfig: [ + { + type: 'section', + title: 'Button', + index: 'index', + withAddButton: true, + itemTitle: 'Button {{index}}', + fields: [ + { + type: 'textInput', + name: 'buttons[{{index}}].text', + title: 'Button Text', + }, + { + type: 'textInput', + name: 'buttons[{{index}}].url', + title: 'URL', + }, + { + type: 'select', + name: 'buttons[{{index}}].theme', + title: 'Theme', + options: [ + {value: 'action', content: 'Action'}, + {value: 'outlined', content: 'Outlined'}, + {value: 'normal', content: 'Normal'}, + ], + }, + ], + }, + ] as Fields, +}; + +export const ArraySectionCard = Template.bind({}); +ArraySectionCard.storyName = 'Array Section – card view'; +ArraySectionCard.args = { + blockConfig: [ + { + type: 'section', + title: 'Buttons', + index: 'index', + withAddButton: true, + itemTitle: 'Button {{index}}', + itemView: 'card', + fields: [ + { + type: 'textInput', + name: 'buttons[{{index}}].text', + title: 'Button Text', + }, + { + type: 'textInput', + name: 'buttons[{{index}}].url', + title: 'URL', + }, + { + type: 'select', + name: 'buttons[{{index}}].theme', + title: 'Theme', + options: [ + {value: 'action', content: 'Action'}, + {value: 'outlined', content: 'Outlined'}, + {value: 'normal', content: 'Normal'}, + ], + }, + ], + }, + ] as Fields, +}; + +export const ArraySectionClear = Template.bind({}); +ArraySectionClear.storyName = 'Array Section – clear view'; +ArraySectionClear.args = { + blockConfig: [ + { + type: 'section', + title: 'Links', + index: 'index', + withAddButton: true, + itemTitle: 'Link {{index}}', + itemView: 'clear', + fields: [ + { + type: 'textInput', + name: 'links[{{index}}].text', + title: 'Text', + }, + { + type: 'textInput', + name: 'links[{{index}}].url', + title: 'URL', + }, + ], + }, + ] as Fields, +}; + +export const NestedArraySection = Template.bind({}); +NestedArraySection.storyName = 'Nested Array Section'; +NestedArraySection.args = { + blockConfig: [ + { + type: 'section', + title: 'Navigation', + opened: true, + fields: [ + { + type: 'textInput', + name: 'navigation.label', + title: 'Navigation Label', + }, + { + type: 'section', + title: 'Item', + index: 'index', + withAddButton: true, + itemTitle: 'Item {{index}}', + fields: [ + { + type: 'textInput', + name: 'navigation.items[{{index}}].text', + title: 'Text', + }, + { + type: 'textInput', + name: 'navigation.items[{{index}}].url', + title: 'URL', + }, + ], + }, + ], + }, + ] as Fields, +}; + +export const NestedSections = Template.bind({}); +NestedSections.storyName = 'Nested Sections'; +NestedSections.args = { + blockConfig: [ + { + type: 'textInput', + name: 'pageTitle', + title: 'Page Title', + }, + { + type: 'section', + title: 'Header', + fields: [ + { + type: 'textInput', + name: 'header.title', + title: 'Header Title', + }, + { + type: 'section', + title: 'Header Media', + fields: [ + { + type: 'textInput', + name: 'header.media.src', + title: 'Image URL', + }, + { + type: 'textInput', + name: 'header.media.alt', + title: 'Alt Text', + }, + ], + }, + ], + }, + { + type: 'section', + title: 'Footer', + fields: [ + { + type: 'textInput', + name: 'footer.text', + title: 'Footer Text', + }, + { + type: 'textInput', + name: 'footer.url', + title: 'Footer URL', + }, + ], + }, + ] as Fields, +}; diff --git a/src/form-generator-v2/__stories__/SegmentedRadioGroup.stories.tsx b/src/form-generator-v2/__stories__/SegmentedRadioGroup.stories.tsx new file mode 100644 index 0000000000..5837f9fbed --- /dev/null +++ b/src/form-generator-v2/__stories__/SegmentedRadioGroup.stories.tsx @@ -0,0 +1,60 @@ +import {Meta, StoryFn} from '@storybook/react'; + +import {useResultPanel} from '../../../.storybook/addons/result-addon/useResultPanel'; +import FormGenerator from '../FormGenerator'; +import {Content, Fields} from '../types'; + +export default { + title: 'FormGenerator/SegmentedRadioGroup', + component: FormGenerator, + parameters: { + layout: 'padded', + resultPanel: true, + }, +} as Meta<typeof FormGenerator>; + +const Template: StoryFn<{blockConfig: Fields}> = ({blockConfig}) => { + const [content, setContent] = useResultPanel<Content>({}); + return ( + <div style={{maxWidth: 600}}> + <FormGenerator + blockConfig={blockConfig} + contentConfig={content} + onUpdate={setContent} + /> + </div> + ); +}; + +const sizeOptions = [ + {value: 's', content: 'S'}, + {value: 'm', content: 'M'}, + {value: 'l', content: 'L'}, + {value: 'xl', content: 'XL'}, +]; + +export const Default = Template.bind({}); +Default.args = { + blockConfig: [ + { + type: 'segmentedRadioGroup', + name: 'size', + title: 'Size', + options: sizeOptions, + }, + ] as Fields, +}; + +export const WithDefaultValue = Template.bind({}); +WithDefaultValue.storyName = 'With Default Value'; +WithDefaultValue.args = { + blockConfig: [ + { + type: 'segmentedRadioGroup', + name: 'size', + title: 'Size (default: M)', + defaultValue: 'm', + options: sizeOptions, + }, + ] as Fields, +}; diff --git a/src/form-generator-v2/__stories__/Select.stories.tsx b/src/form-generator-v2/__stories__/Select.stories.tsx new file mode 100644 index 0000000000..cae44953a7 --- /dev/null +++ b/src/form-generator-v2/__stories__/Select.stories.tsx @@ -0,0 +1,59 @@ +import {Meta, StoryFn} from '@storybook/react'; + +import {useResultPanel} from '../../../.storybook/addons/result-addon/useResultPanel'; +import FormGenerator from '../FormGenerator'; +import {Content, Fields} from '../types'; + +export default { + title: 'FormGenerator/Select', + component: FormGenerator, + parameters: { + layout: 'padded', + resultPanel: true, + }, +} as Meta<typeof FormGenerator>; + +const Template: StoryFn<{blockConfig: Fields}> = ({blockConfig}) => { + const [content, setContent] = useResultPanel<Content>({}); + return ( + <div style={{maxWidth: 600}}> + <FormGenerator + blockConfig={blockConfig} + contentConfig={content} + onUpdate={setContent} + /> + </div> + ); +}; + +const layoutOptions = [ + {value: 'default', content: 'Default'}, + {value: 'centered', content: 'Centered'}, + {value: 'wide', content: 'Wide'}, +]; + +export const Default = Template.bind({}); +Default.args = { + blockConfig: [ + { + type: 'select', + name: 'layout', + title: 'Layout', + options: layoutOptions, + }, + ] as Fields, +}; + +export const WithClear = Template.bind({}); +WithClear.storyName = 'With Clear Button (hasClear)'; +WithClear.args = { + blockConfig: [ + { + type: 'select', + name: 'layout', + title: 'Layout (with clear button)', + hasClear: true, + options: layoutOptions, + }, + ] as Fields, +}; diff --git a/src/form-generator-v2/__stories__/Switch.stories.tsx b/src/form-generator-v2/__stories__/Switch.stories.tsx new file mode 100644 index 0000000000..e5b00ca534 --- /dev/null +++ b/src/form-generator-v2/__stories__/Switch.stories.tsx @@ -0,0 +1,38 @@ +import {Meta, StoryFn} from '@storybook/react'; + +import {useResultPanel} from '../../../.storybook/addons/result-addon/useResultPanel'; +import FormGenerator from '../FormGenerator'; +import {Content, Fields} from '../types'; + +export default { + title: 'FormGenerator/Switch', + component: FormGenerator, + parameters: { + layout: 'padded', + resultPanel: true, + }, +} as Meta<typeof FormGenerator>; + +const Template: StoryFn<{blockConfig: Fields}> = ({blockConfig}) => { + const [content, setContent] = useResultPanel<Content>({}); + return ( + <div style={{maxWidth: 600}}> + <FormGenerator + blockConfig={blockConfig} + contentConfig={content} + onUpdate={setContent} + /> + </div> + ); +}; + +export const Default = Template.bind({}); +Default.args = { + blockConfig: [ + { + type: 'switch', + name: 'enabled', + title: 'Enable feature', + }, + ] as Fields, +}; diff --git a/src/form-generator-v2/__stories__/Text.stories.tsx b/src/form-generator-v2/__stories__/Text.stories.tsx new file mode 100644 index 0000000000..f20171af7c --- /dev/null +++ b/src/form-generator-v2/__stories__/Text.stories.tsx @@ -0,0 +1,155 @@ +import {Meta, StoryFn} from '@storybook/react'; + +import {useResultPanel} from '../../../.storybook/addons/result-addon/useResultPanel'; +import FormGenerator from '../FormGenerator'; +import {Content, Fields} from '../types'; + +export default { + title: 'FormGenerator/Text', + component: FormGenerator, + parameters: { + layout: 'padded', + resultPanel: true, + docs: { + description: { + component: + 'A read-only text label rendered inline among other fields. ' + + 'Useful for hints, section descriptions, or warnings. ' + + 'Has no `name` and writes nothing to content. Supports `when` like any other field.', + }, + }, + }, +} as Meta<typeof FormGenerator>; + +const Template: StoryFn<{blockConfig: Fields}> = ({blockConfig}) => { + const [content, setContent] = useResultPanel<Content>({}); + return ( + <div style={{maxWidth: 600}}> + <FormGenerator + blockConfig={blockConfig} + contentConfig={content} + onUpdate={setContent} + /> + </div> + ); +}; + +export const Default = Template.bind({}); +Default.args = { + blockConfig: [ + { + type: 'text', + text: 'Fill in the fields below to configure the block.', + }, + { + type: 'textInput', + name: 'title', + title: 'Title', + }, + { + type: 'textArea', + name: 'description', + title: 'Description', + }, + ] as Fields, +}; + +export const WithInfoLevel = Template.bind({}); +WithInfoLevel.storyName = 'Info level'; +WithInfoLevel.args = { + blockConfig: [ + { + type: 'text', + text: 'Analytics data is collected anonymously.', + level: 'info', + }, + { + type: 'switch', + name: 'analytics.enabled', + title: 'Enable Analytics', + }, + { + type: 'textInput', + name: 'analytics.id', + title: 'Tracking ID', + }, + ] as Fields, +}; + +export const WithDangerLevel = Template.bind({}); +WithDangerLevel.storyName = 'Danger level'; +WithDangerLevel.args = { + blockConfig: [ + { + type: 'text', + text: 'Changes here may break your page layout.', + level: 'danger', + }, + { + type: 'textInput', + name: 'customCss', + title: 'Custom CSS Class', + }, + { + type: 'textInput', + name: 'customId', + title: 'Custom Element ID', + }, + ] as Fields, +}; + +export const Colors: StoryFn<{blockConfig: Fields}> = ({blockConfig}) => { + const [content, setContent] = useResultPanel<Content>({}); + return ( + <div style={{maxWidth: 600}}> + <FormGenerator + blockConfig={blockConfig} + contentConfig={content} + onUpdate={setContent} + /> + </div> + ); +}; +Colors.args = { + blockConfig: [ + {type: 'text', text: 'primary — main text', color: 'primary'}, + {type: 'text', text: 'secondary — supporting text', color: 'secondary'}, + {type: 'text', text: 'hint — helper text', color: 'hint'}, + {type: 'text', text: 'info — informational', color: 'info'}, + {type: 'text', text: 'positive — success / positive', color: 'positive'}, + {type: 'text', text: 'warning — warning', color: 'warning'}, + {type: 'text', text: 'danger — error / danger', color: 'danger'}, + {type: 'text', text: 'utility — utility', color: 'utility'}, + {type: 'text', text: 'misc — miscellaneous', color: 'misc'}, + ] as Fields, +}; + +export const AsHintBetweenFields = Template.bind({}); +AsHintBetweenFields.storyName = 'Hint between fields'; +AsHintBetweenFields.args = { + blockConfig: [ + { + type: 'switch', + name: 'customColors', + title: 'Custom colors', + }, + { + type: 'text', + text: 'Choose colors that match your brand palette.', + color: 'hint', + when: [{field: 'customColors', operator: '===', value: true}], + }, + { + type: 'colorInput', + name: 'primaryColor', + title: 'Primary Color', + when: [{field: 'customColors', operator: '===', value: true}], + }, + { + type: 'colorInput', + name: 'secondaryColor', + title: 'Secondary Color', + when: [{field: 'customColors', operator: '===', value: true}], + }, + ] as Fields, +}; diff --git a/src/form-generator-v2/__stories__/TextArea.stories.tsx b/src/form-generator-v2/__stories__/TextArea.stories.tsx new file mode 100644 index 0000000000..85415396c6 --- /dev/null +++ b/src/form-generator-v2/__stories__/TextArea.stories.tsx @@ -0,0 +1,38 @@ +import {Meta, StoryFn} from '@storybook/react'; + +import {useResultPanel} from '../../../.storybook/addons/result-addon/useResultPanel'; +import FormGenerator from '../FormGenerator'; +import {Content, Fields} from '../types'; + +export default { + title: 'FormGenerator/TextArea', + component: FormGenerator, + parameters: { + layout: 'padded', + resultPanel: true, + }, +} as Meta<typeof FormGenerator>; + +const Template: StoryFn<{blockConfig: Fields}> = ({blockConfig}) => { + const [content, setContent] = useResultPanel<Content>({}); + return ( + <div style={{maxWidth: 600}}> + <FormGenerator + blockConfig={blockConfig} + contentConfig={content} + onUpdate={setContent} + /> + </div> + ); +}; + +export const Default = Template.bind({}); +Default.args = { + blockConfig: [ + { + type: 'textArea', + name: 'description', + title: 'Description', + }, + ] as Fields, +}; diff --git a/src/form-generator-v2/__stories__/TextInput.stories.tsx b/src/form-generator-v2/__stories__/TextInput.stories.tsx new file mode 100644 index 0000000000..f6495a5178 --- /dev/null +++ b/src/form-generator-v2/__stories__/TextInput.stories.tsx @@ -0,0 +1,38 @@ +import {Meta, StoryFn} from '@storybook/react'; + +import {useResultPanel} from '../../../.storybook/addons/result-addon/useResultPanel'; +import FormGenerator from '../FormGenerator'; +import {Content, Fields} from '../types'; + +export default { + title: 'FormGenerator/TextInput', + component: FormGenerator, + parameters: { + layout: 'padded', + resultPanel: true, + }, +} as Meta<typeof FormGenerator>; + +const Template: StoryFn<{blockConfig: Fields}> = ({blockConfig}) => { + const [content, setContent] = useResultPanel<Content>({}); + return ( + <div style={{maxWidth: 600}}> + <FormGenerator + blockConfig={blockConfig} + contentConfig={content} + onUpdate={setContent} + /> + </div> + ); +}; + +export const Default = Template.bind({}); +Default.args = { + blockConfig: [ + { + type: 'textInput', + name: 'title', + title: 'Title', + }, + ] as Fields, +}; diff --git a/src/hooks/useMetrika.ts b/src/form-generator-v2/components/Base/Base.scss similarity index 100% rename from src/hooks/useMetrika.ts rename to src/form-generator-v2/components/Base/Base.scss diff --git a/src/form-generator-v2/components/Base/Base.tsx b/src/form-generator-v2/components/Base/Base.tsx new file mode 100644 index 0000000000..5dacc257bb --- /dev/null +++ b/src/form-generator-v2/components/Base/Base.tsx @@ -0,0 +1,97 @@ +import * as React from 'react'; + +import {Content, OnUpdate, When} from '../../types'; +import {getValueByPath} from '../../utils/fields'; + +import './Base.scss'; + +type BaseProps = { + when?: When; + content: Content; + name?: string; + onUpdate?: OnUpdate; + defaultValue?: unknown; + children: React.ReactNode; +}; + +const Base = ({when, content, children, name, onUpdate, defaultValue}: BaseProps) => { + const verifiedConditions = React.useMemo(() => { + if (!when) { + return true; + } + + let result = null; + let currentOperator = null; + + for (let i = 0; i < when.length; i++) { + const condition = when[i]; + + if (condition.operator && !condition.field) { + currentOperator = condition.operator; + continue; + } + + let currentResult = false; + if (condition.field) { + const fieldValue = getValueByPath(content, condition.field); + const value = condition.value; + + switch (condition.operator) { + case '===': + currentResult = fieldValue === value; + break; + case '!==': + currentResult = fieldValue !== value; + break; + } + } + + if (result === null) { + result = currentResult; + } else if (currentOperator === '||') { + result = result || currentResult; + } else if (currentOperator === '&&') { + result = result && currentResult; + } + + currentOperator = null; + } + + return Boolean(result); + }, [content, when]); + + const isShow = React.useMemo( + () => Boolean(!when || !content || verifiedConditions), + [content, verifiedConditions, when], + ); + + const wasVisibleRef = React.useRef(false); + + React.useEffect(() => { + const wasVisible = wasVisibleRef.current; + wasVisibleRef.current = isShow; + + if ( + wasVisible && + !isShow && + onUpdate && + name && + getValueByPath(content, name) !== undefined + ) { + onUpdate(name, undefined, {unset: true}); + } else if ( + !wasVisible && + isShow && + onUpdate && + name && + defaultValue !== undefined && + getValueByPath(content, name) === undefined + ) { + onUpdate(name, defaultValue); + } + }, [content, isShow, name, onUpdate, defaultValue]); + + return isShow ? children : null; +}; + +export default Base; diff --git a/src/form-generator-v2/components/BaseInput/BaseInput.scss b/src/form-generator-v2/components/BaseInput/BaseInput.scss new file mode 100644 index 0000000000..718b6d039b --- /dev/null +++ b/src/form-generator-v2/components/BaseInput/BaseInput.scss @@ -0,0 +1,37 @@ +.pc-fg-base-input { + display: flex; + align-items: stretch; + + margin-bottom: 12px; + + &:not(:first-child) { + margin-top: 12px; + } + + &:last-child { + margin-bottom: 0; + } + + &__title { + padding: 5px 0; + min-width: 64px; + max-width: 128px; + display: inline-block; + white-space: break-spaces; + word-break: break-word; + width: 20%; + align-items: flex-start; + + &::first-letter { + text-transform: uppercase; + } + } + + &__input { + margin-left: 8px; + flex-grow: 1; + min-height: 28px; + display: flex; + align-items: center; + } +} diff --git a/src/form-generator-v2/components/BaseInput/BaseInput.tsx b/src/form-generator-v2/components/BaseInput/BaseInput.tsx new file mode 100644 index 0000000000..255d325a8e --- /dev/null +++ b/src/form-generator-v2/components/BaseInput/BaseInput.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; + +import {Text} from '@gravity-ui/uikit'; + +import {ClassNameProps} from '../../../models/common'; +import {formGeneratorCn} from '../../utils/cn'; + +import './BaseInput.scss'; + +const b = formGeneratorCn('base-input'); + +type BaseInputProps = ClassNameProps & { + title: string; + children: React.ReactNode; +}; + +const BaseInput = ({children, title, className}: BaseInputProps) => { + return ( + <div className={b(null, className)}> + <Text variant="body-1" className={b('title')}> + {title} + </Text> + <div className={b('input')}>{children}</div> + </div> + ); +}; + +export default BaseInput; diff --git a/src/form-generator-v2/components/ColorInput/ColorInput.tsx b/src/form-generator-v2/components/ColorInput/ColorInput.tsx new file mode 100644 index 0000000000..fd9eac1913 --- /dev/null +++ b/src/form-generator-v2/components/ColorInput/ColorInput.tsx @@ -0,0 +1,54 @@ +import {Xmark} from '@gravity-ui/icons'; +import {Button, Icon} from '@gravity-ui/uikit'; +import {unstable_ColorPicker as ColorPicker} from '@gravity-ui/uikit/unstable'; + +import {ClassNameProps} from '../../../models/common'; +import {ColorField, CommonProps} from '../../types'; +import {getValueByPath} from '../../utils/fields'; +import Base from '../Base/Base'; +import BaseInput from '../BaseInput/BaseInput'; + +type ColorInputProps = ClassNameProps & CommonProps & ColorField; + +const ColorInput = ({ + title, + name, + when, + content, + onUpdate, + className, + defaultValue, +}: ColorInputProps) => { + const storedValue = getValueByPath(content, name); + const value = (storedValue ?? defaultValue ?? '') as string; + + return ( + <Base + when={when} + content={content} + name={name} + onUpdate={onUpdate} + defaultValue={defaultValue} + > + <BaseInput title={title} className={className}> + <ColorPicker + onUpdate={(v: string) => onUpdate(name, v)} + value={value} + size="s" + withAlpha + /> + {storedValue ? ( + <Button + view="flat" + size="s" + onClick={() => onUpdate(name, undefined, {unset: true})} + > + <Icon data={Xmark} width={16} height={16} /> + </Button> + ) : null} + </BaseInput> + </Base> + ); +}; + +export default ColorInput; diff --git a/src/form-generator-v2/components/Divider/Divider.scss b/src/form-generator-v2/components/Divider/Divider.scss new file mode 100644 index 0000000000..2688e8807e --- /dev/null +++ b/src/form-generator-v2/components/Divider/Divider.scss @@ -0,0 +1,4 @@ +.pc-fg-divider { + --g-divider-color: transparent; + margin: 10px 0; +} diff --git a/src/form-generator-v2/components/Divider/Divider.tsx b/src/form-generator-v2/components/Divider/Divider.tsx new file mode 100644 index 0000000000..bdebdc63ad --- /dev/null +++ b/src/form-generator-v2/components/Divider/Divider.tsx @@ -0,0 +1,23 @@ +import {Divider as DividerUIKit} from '@gravity-ui/uikit'; + +import {ClassNameProps} from '../../../models/common'; +import {Content, When} from '../../types'; +import {formGeneratorCn} from '../../utils/cn'; +import Base from '../Base/Base'; + +import './Divider.scss'; + +const b = formGeneratorCn('divider'); + +type DividerProps = ClassNameProps & { + when?: When; + content: Content; +}; + +const Divider = ({when, content, className}: DividerProps) => ( + <Base when={when} content={content}> + <DividerUIKit className={b(null, className)} /> + </Base> +); + +export default Divider; diff --git a/src/form-generator-v2/components/Fields/Fields.scss b/src/form-generator-v2/components/Fields/Fields.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/form-generator-v2/components/Fields/Fields.tsx b/src/form-generator-v2/components/Fields/Fields.tsx new file mode 100644 index 0000000000..794c8072d4 --- /dev/null +++ b/src/form-generator-v2/components/Fields/Fields.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; + +import {ClassNameProps} from '../../../models/common'; +import {Content, Fields as FieldsType, OnUpdate} from '../../types'; +import {formGeneratorCn} from '../../utils/cn'; +import {componentMap} from '../constants'; + +import './Fields.scss'; + +const b = formGeneratorCn('fields'); + +type FieldsProps = ClassNameProps & { + fields: FieldsType; + content: Content; + onUpdate: OnUpdate; +}; +const Fields = ({fields, content, onUpdate, className}: FieldsProps) => ( + <div className={b(null, className)}> + {fields.map((field, index) => { + const Component = componentMap[field.type] as React.ComponentType< + Record<string, unknown> + >; + + if (!Component) { + // eslint-disable-next-line + console.warn(`NOT FOUND COMPONENT FOR TYPE ${field.type}`); + return null; + } + + return ( + <Component + {...field} + className={b('field')} + key={index} + content={content} + onUpdate={onUpdate} + /> + ); + })} + </div> +); + +export default Fields; diff --git a/src/form-generator-v2/components/Section/Section.scss b/src/form-generator-v2/components/Section/Section.scss new file mode 100644 index 0000000000..7c31937f23 --- /dev/null +++ b/src/form-generator-v2/components/Section/Section.scss @@ -0,0 +1,208 @@ +@import '../../../../styles/mixins'; + +.pc-fg-section { + $class: &; + + position: relative; + + &:before { + position: absolute; + top: 0; + left: -24px; + width: calc(100% + (12px * 2)); + height: 1px; + background: var(--g-color-line-generic); + content: ''; + display: block; + margin-left: 12px; + } + + &:after { + position: absolute; + bottom: 0; + left: -24px; + width: calc(100% + (12px * 2)); + height: 1px; + background: var(--g-color-line-generic); + content: ''; + display: block; + margin-left: 12px; + } + + & + & { + &:before { + display: none; + } + } + + &:last-child { + &#{$class}:after { + content: none; + } + } + + &_nested { + margin-top: 16px; + margin-bottom: 8px; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + + &:before, + &:after { + display: none; + } + + #{$class}__header { + padding-top: 0; + padding-bottom: 0; + } + + #{$class}__fields { + // TODO: maybe we need this + // margin-top: 8px; + } + + #{$class}__children_opened > #{$class}__children-inner { + padding-top: 8px; + } + } + + &__header { + display: flex; + align-items: center; + padding-top: 12px; + padding-bottom: 12px; + } + + &__title { + display: inline-block; + + &::first-letter { + text-transform: uppercase; + } + } + + &__header-button { + @include reset-button-style(); + + min-height: 28px; + display: flex; + align-items: center; + flex-grow: 1; + + &_with-hover { + & > * { + transition: color 0.4s; + } + + &:hover { + & > * { + color: var(--g-color-text-primary); + } + } + } + } + + &__arrow { + margin-right: 4px; + color: var(--g-color-text-hint); + } + + &__plus { + width: 28px; + height: 28px; + padding: 6px; + color: var(--g-color-text-hint); + margin-left: auto; + box-sizing: border-box; + } + + &__children { + display: grid; + grid-template-rows: 0fr; + overflow: hidden; + margin-bottom: 0; + transition: + grid-template-rows 0.35s cubic-bezier(0.4, 0, 0.2, 1), + margin-bottom 0.35s cubic-bezier(0.4, 0, 0.2, 1); + + &_opened { + grid-template-rows: 1fr; + padding-bottom: 16px; + padding-top: 16px; + + & & { + padding-bottom: 0; + } + } + } + + &__children-inner { + min-height: 0; + overflow: hidden; + transition: padding-top 0.35s cubic-bezier(0.4, 0, 0.2, 1); + } + + &__card { + padding: 12px; + padding-bottom: 16px; + + &:not(&:nth-last-child(1)) { + margin-bottom: 12px; + } + + .pc-fg-section { + &:before { + content: none; + } + } + } + + &__item-clear { + margin-top: 12px; + + &:first-child { + margin-top: 0; + } + + &:not(&:nth-last-child(1)) { + margin-bottom: 12px; + } + + .pc-fg-section { + &:before { + content: none; + } + } + } + + &__card-header { + display: flex; + align-items: center; + margin-bottom: 12px; + } + + &__card-title { + display: block; + flex-grow: 1; + + &::first-letter { + text-transform: uppercase; + } + } + + &__dropdown { + opacity: 0; + transition: opacity 0.2s ease; + + &_opened { + opacity: 1; + } + } +} diff --git a/src/form-generator-v2/components/Section/Section.tsx b/src/form-generator-v2/components/Section/Section.tsx new file mode 100644 index 0000000000..6427ea28da --- /dev/null +++ b/src/form-generator-v2/components/Section/Section.tsx @@ -0,0 +1,315 @@ +import * as React from 'react'; + +import {EllipsisVertical, Plus, TrashBin} from '@gravity-ui/icons'; +import {ArrowToggle, Button, Card, Dialog, DropdownMenu, Icon, Text} from '@gravity-ui/uikit'; + +import {ClassNameProps} from '../../../models/common'; +import {CommonProps, SectionField} from '../../types'; +import {formGeneratorCn} from '../../utils/cn'; +import { + clearSectionFormContent, + findAllNames, + findNameWithIndexName, + getArrayPathForNameWithIndexName, + getSpliceTarget, + getValueByPath, + sectionHasContentData, +} from '../../utils/fields'; +import Base from '../Base/Base'; +import Fields from '../Fields/Fields'; + +import {SectionOpenContext} from './SectionOpenContext'; + +import './Section.scss'; + +const b = formGeneratorCn('section'); + +const makeRowKey = (): string => + typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' + ? crypto.randomUUID() + : `row-${Math.random().toString(36).slice(2, 11)}`; + +type SectionProp = ClassNameProps & CommonProps & SectionField; + +const Section = ({ + title, + opened, + fields, + when, + content, + onUpdate, + className, + index, + withAddButton, + itemTitle, + itemView, +}: SectionProp) => { + const isGroupMode = index !== undefined; + + const parentContext = React.useContext(SectionOpenContext); + const nestingLevel = parentContext?.nestingLevel ?? 0; + + // --- Static section state --- + const [isOpened, setOpened] = React.useState(opened); + const [confirmDialogOpen, setConfirmDialogOpen] = React.useState(false); + const hasData = sectionHasContentData(fields, content); + const showArrowTogler = hasData || isOpened; + + const handleConfirmClear = () => { + if (onUpdate) { + clearSectionFormContent(fields, onUpdate); + } + setConfirmDialogOpen(false); + setOpened(false); + }; + + // --- Group mode state --- + const [rowKeys, setRowKeys] = React.useState<string[]>([]); + + const nameWithIndexName = React.useMemo( + () => (isGroupMode && index ? findNameWithIndexName(fields, index) : undefined), + [fields, index, isGroupMode], + ); + + const arrayPath = React.useMemo( + () => + nameWithIndexName && index + ? getArrayPathForNameWithIndexName(nameWithIndexName, index) + : undefined, + [nameWithIndexName, index], + ); + + const valuesInside = React.useMemo( + () => (arrayPath ? getValueByPath(content, arrayPath) : undefined), + [arrayPath, content], + ); + const valuesInsideLength = Array.isArray(valuesInside) ? valuesInside.length : 0; + + React.useEffect(() => { + if (!isGroupMode) return; + if (!nameWithIndexName || !arrayPath) { + setRowKeys((prev) => (prev.length === 0 ? [makeRowKey()] : prev)); + return; + } + if (!content) { + return; + } + + setRowKeys((prev) => { + if (prev.length === valuesInsideLength) { + return prev; + } + if (valuesInsideLength > prev.length) { + return [ + ...prev, + ...Array.from({length: valuesInsideLength - prev.length}, makeRowKey), + ]; + } + return prev.slice(0, valuesInsideLength); + }); + }, [content, arrayPath, nameWithIndexName, valuesInside, valuesInsideLength, isGroupMode]); + + const replaceIndex = (i: number) => + fields ? JSON.parse(JSON.stringify(fields).replaceAll(`{{${index}}}`, String(i))) : fields; + + const handleAdd = () => { + if (arrayPath && onUpdate) { + const next = Array.isArray(valuesInside) ? [...valuesInside, {}] : [{}]; + onUpdate(arrayPath, next); + return; + } + setRowKeys((prev) => [...prev, makeRowKey()]); + }; + + const deleteItem = (itemIndex: number) => () => { + if (!nameWithIndexName || !onUpdate || !index) { + return; + } + + const names = findAllNames(replaceIndex(itemIndex)); + const resolvedName = names.find((name) => { + const splice = getSpliceTarget(nameWithIndexName, name, index); + return splice !== undefined && splice.itemIndex === itemIndex; + }); + + if (!resolvedName) { + return; + } + + const spliceTarget = getSpliceTarget(nameWithIndexName, resolvedName, index); + if (!spliceTarget) { + return; + } + + onUpdate(spliceTarget.arrayPath, undefined, {removeArrayItemAt: spliceTarget.itemIndex}); + setRowKeys((prev) => prev.filter((_key, keyIndex) => keyIndex !== itemIndex)); + }; + + // --- Group mode render --- + if (isGroupMode) { + const hasGroupLabel = Boolean(title); + const isEmpty = rowKeys.length === 0; + const isGroupOpen = isOpened ?? true; + const resolvedItemTitle = (i: number) => { + const src = itemTitle || title; + return src ? src.replaceAll(`{{${index}}}`, String(i)) : undefined; + }; + const labelTemplate = itemTitle || title; + const addButtonLabel = labelTemplate + ? labelTemplate.replaceAll(`{{${index}}}`, String(rowKeys.length)) + : 'Add'; + + const renderItem = (rowKey: string, rowIndex: number) => { + const cardTitle = resolvedItemTitle(rowIndex); + const header = ( + <div className={b('card-header')}> + {cardTitle && ( + <Text variant="subheader-2" className={b('card-title')}> + {cardTitle} + </Text> + )} + <Button view="flat" onClick={deleteItem(rowIndex)}> + <Icon width={16} height={16} data={TrashBin} /> + </Button> + </div> + ); + const inner = ( + <SectionOpenContext.Provider value={{isOpen: true, nestingLevel: nestingLevel + 1}}> + <Fields fields={replaceIndex(rowIndex)} content={content} onUpdate={onUpdate} /> + </SectionOpenContext.Provider> + ); + + if (itemView === 'clear') { + return ( + <div key={rowKey} className={b('item-clear')}> + {header} + {inner} + </div> + ); + } + return ( + <Card key={rowKey} className={b('card')}> + {header} + {inner} + </Card> + ); + }; + + return ( + <Base when={when} content={content}> + <div className={b({nested: nestingLevel > 0}, className)}> + {hasGroupLabel && ( + <div className={b('header')}> + <button + type="button" + className={b('header-button', {'with-hover': isEmpty})} + onClick={isEmpty ? handleAdd : () => setOpened((prev) => !prev)} + > + {!isEmpty && ( + <ArrowToggle + direction={isGroupOpen ? 'top' : 'bottom'} + className={b('arrow')} + /> + )} + <Text variant="subheader-1" color="hint" className={b('title')}> + {title} + </Text> + {isEmpty && <Plus width={16} height={16} className={b('plus')} />} + </button> + </div> + )} + <div + className={b('children', { + opened: !isEmpty && (!hasGroupLabel || isGroupOpen), + })} + > + <div className={b('children-inner')}> + {rowKeys.map((rowKey, rowIndex) => renderItem(rowKey, rowIndex))} + {withAddButton && !isEmpty && ( + <Button onClick={handleAdd}> + <Icon data={Plus} width={16} height={16} /> + {addButtonLabel} + </Button> + )} + </div> + </div> + </div> + </Base> + ); + } + + // --- Static section render --- + return ( + <Base when={when} content={content}> + <React.Fragment> + <div className={b({opened: isOpened, nested: nestingLevel > 0}, className)}> + <div className={b('header')}> + <button + type="button" + className={b('header-button', { + 'with-hover': !showArrowTogler, + })} + onClick={() => setOpened((prev) => !prev)} + > + {showArrowTogler && ( + <ArrowToggle + direction={isOpened ? 'top' : 'bottom'} + className={b('arrow')} + /> + )} + <Text variant="subheader-1" color="hint" className={b('title')}> + {title} + </Text> + </button> + <div className={b('dropdown', {opened: isOpened})}> + <DropdownMenu + icon={<Icon data={EllipsisVertical} width={16} height={16} />} + items={[ + { + text: 'Clear all fields', + action: () => setConfirmDialogOpen(true), + }, + ]} + /> + </div> + </div> + <div className={b('children', {opened: isOpened})}> + <div className={b('children-inner')}> + <SectionOpenContext.Provider + value={{isOpen: isOpened ?? false, nestingLevel: nestingLevel + 1}} + > + <Fields + className={b('fields')} + fields={fields} + content={content} + onUpdate={onUpdate} + /> + </SectionOpenContext.Provider> + </div> + </div> + </div> + <Dialog + open={confirmDialogOpen} + onClose={() => setConfirmDialogOpen(false)} + size="s" + > + <Dialog.Header caption="Clear all fields in this block?" /> + <Dialog.Body> + <Text variant="body-1"> + All field values will be deleted, and the block settings will be reset + to their default state. + </Text> + </Dialog.Body> + <Dialog.Footer + textButtonApply="Approve" + textButtonCancel="Cancel" + onClickButtonApply={handleConfirmClear} + onClickButtonCancel={() => setConfirmDialogOpen(false)} + /> + </Dialog> + </React.Fragment> + </Base> + ); +}; + +export default Section; diff --git a/src/form-generator-v2/components/Section/SectionOpenContext.tsx b/src/form-generator-v2/components/Section/SectionOpenContext.tsx new file mode 100644 index 0000000000..4c09b2c431 --- /dev/null +++ b/src/form-generator-v2/components/Section/SectionOpenContext.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +export interface SectionContextValue { + isOpen: boolean; + nestingLevel: number; +} + +export const SectionOpenContext = React.createContext<SectionContextValue>({ + isOpen: false, + nestingLevel: 0, +}); diff --git a/src/form-generator-v2/components/SegmentedRadioGroup/SegmentedRadioGroup.tsx b/src/form-generator-v2/components/SegmentedRadioGroup/SegmentedRadioGroup.tsx new file mode 100644 index 0000000000..6350a37947 --- /dev/null +++ b/src/form-generator-v2/components/SegmentedRadioGroup/SegmentedRadioGroup.tsx @@ -0,0 +1,43 @@ +import {SegmentedRadioGroup as SegmentedRadioGroupUIKIT} from '@gravity-ui/uikit'; + +import {CommonProps, SegmentedRadioGroupField} from '../../types'; +import {getValueByPath} from '../../utils/fields'; +import Base from '../Base/Base'; +import BaseInput from '../BaseInput/BaseInput'; + +type SegmentedRadioGroupProps = CommonProps & SegmentedRadioGroupField; + +const SegmentedRadioGroup = ({ + title, + name, + options, + when, + content, + onUpdate, + defaultValue, +}: SegmentedRadioGroupProps) => { + const selected = content ? getValueByPath(content, name) : undefined; + + const value = selected ?? defaultValue ?? null; + + return ( + <Base + when={when} + content={content} + name={name} + onUpdate={onUpdate} + defaultValue={defaultValue} + > + <BaseInput title={title}> + <SegmentedRadioGroupUIKIT + name={name} + options={options} + onUpdate={(v) => onUpdate(name, v)} + value={value} + /> + </BaseInput> + </Base> + ); +}; + +export default SegmentedRadioGroup; diff --git a/src/form-generator-v2/components/Select/Select.scss b/src/form-generator-v2/components/Select/Select.scss new file mode 100644 index 0000000000..187de6845b --- /dev/null +++ b/src/form-generator-v2/components/Select/Select.scss @@ -0,0 +1,3 @@ +.pc-fg-select { + width: 100%; +} diff --git a/src/form-generator-v2/components/Select/Select.tsx b/src/form-generator-v2/components/Select/Select.tsx new file mode 100644 index 0000000000..c03759fabe --- /dev/null +++ b/src/form-generator-v2/components/Select/Select.tsx @@ -0,0 +1,62 @@ +import {Select as SelectUIKIT} from '@gravity-ui/uikit'; + +import {CommonProps, SelectField} from '../../types'; +import {formGeneratorCn} from '../../utils/cn'; +import {getValueByPath} from '../../utils/fields'; +import Base from '../Base/Base'; +import BaseInput from '../BaseInput/BaseInput'; + +import './Select.scss'; + +type SelectProps = CommonProps & SelectField; + +const b = formGeneratorCn('select'); +const Select = ({ + title, + name, + options, + when, + content, + onUpdate, + hasClear, + defaultValue, + placeholder, +}: SelectProps) => { + const stored = getValueByPath(content, name); + const hasStored = + stored !== undefined && + stored !== null && + (typeof stored !== 'string' || stored.length > 0); + const value = hasStored ? [String(stored as string | number | boolean)] : []; + + return ( + <Base + when={when} + content={content} + name={name} + onUpdate={onUpdate} + defaultValue={defaultValue} + > + <BaseInput title={title}> + <SelectUIKIT + name={name} + options={options.map((option) => ({ + value: option.value, + content: option.content || option.value, + }))} + placeholder={placeholder || 'Not selected'} + onUpdate={(v) => + v.length > 0 + ? onUpdate(name, v[0] as string) + : onUpdate(name, undefined, {unset: true}) + } + value={value} + hasClear={hasClear} + className={b()} + /> + </BaseInput> + </Base> + ); +}; + +export default Select; diff --git a/src/form-generator-v2/components/Switch/Switch.tsx b/src/form-generator-v2/components/Switch/Switch.tsx new file mode 100644 index 0000000000..d5fc82feff --- /dev/null +++ b/src/form-generator-v2/components/Switch/Switch.tsx @@ -0,0 +1,23 @@ +import {Switch as SwitchUIKIT} from '@gravity-ui/uikit'; + +import {ClassNameProps} from '../../../models/common'; +import {CommonProps, SwitchField} from '../../types'; +import {getValueByPath} from '../../utils/fields'; +import Base from '../Base/Base'; +import BaseInput from '../BaseInput/BaseInput'; + +type SwitchProps = ClassNameProps & SwitchField & CommonProps; + +const Switch = ({title, when, name, content, onUpdate, className, defaultValue}: SwitchProps) => { + const value = getValueByPath(content, name) ?? defaultValue ?? false; + + return ( + <Base content={content} when={when} onUpdate={onUpdate}> + <BaseInput title={title} className={className}> + <SwitchUIKIT name={name} onUpdate={(v) => onUpdate(name, v)} checked={value} /> + </BaseInput> + </Base> + ); +}; + +export default Switch; diff --git a/src/form-generator-v2/components/Text/Text.scss b/src/form-generator-v2/components/Text/Text.scss new file mode 100644 index 0000000000..212a51eb15 --- /dev/null +++ b/src/form-generator-v2/components/Text/Text.scss @@ -0,0 +1,27 @@ +.pc-fg-text { + display: flex; + align-items: center; + min-height: 28px; + text-overflow: ellipsis; + overflow: hidden; + + margin-bottom: 12px; + + &_as-title { + margin-top: 20px; + } + + &:last-child { + margin-bottom: 0; + } + + &__note { + display: flex; + } + + &__note-icon { + margin-right: 4px; + min-width: 14px; + min-height: 14px; + } +} diff --git a/src/form-generator-v2/components/Text/Text.tsx b/src/form-generator-v2/components/Text/Text.tsx new file mode 100644 index 0000000000..f71771aa2e --- /dev/null +++ b/src/form-generator-v2/components/Text/Text.tsx @@ -0,0 +1,47 @@ +import {CircleInfoFill, TriangleExclamationFill} from '@gravity-ui/icons'; +import {Icon, Text as TextUIKIT} from '@gravity-ui/uikit'; + +import {ClassNameProps} from '../../../models/common'; +import {Content, Text as TextField, When} from '../../types'; +import {formGeneratorCn} from '../../utils/cn'; +import Base from '../Base/Base'; + +import './Text.scss'; + +const b = formGeneratorCn('text'); + +const LEVEL_ICON = { + info: CircleInfoFill, + danger: TriangleExclamationFill, +}; + +type TextProps = ClassNameProps & { + text: string; + level?: TextField['level']; + color?: TextField['color']; + when?: When; + content: Content; +}; + +const Text = ({text, level, color, when, content, className}: TextProps) => ( + <Base when={when} content={content}> + {level ? ( + <div className={b(null, className)}> + <div className={b('note', null, className)}> + <Icon className={b('note-icon')} data={LEVEL_ICON[level]} color={level} /> + <TextUIKIT variant="body-1">{text}</TextUIKIT> + </div> + </div> + ) : ( + <TextUIKIT + className={b({'as-title': true}, className)} + variant="subheader-1" + color={color} + > + {text} + </TextUIKIT> + )} + </Base> +); + +export default Text; diff --git a/src/form-generator-v2/components/TextArea/TextArea.tsx b/src/form-generator-v2/components/TextArea/TextArea.tsx new file mode 100644 index 0000000000..ce5e3c7a4d --- /dev/null +++ b/src/form-generator-v2/components/TextArea/TextArea.tsx @@ -0,0 +1,42 @@ +import {TextArea as TextAreaUIKIT} from '@gravity-ui/uikit'; + +import {CommonProps, TextField} from '../../types'; +import {getValueByPath} from '../../utils/fields'; +import Base from '../Base/Base'; +import BaseInput from '../BaseInput/BaseInput'; + +type TextAreaProps = CommonProps & TextField; + +const TextArea = ({ + when, + title, + content, + name, + onUpdate, + defaultValue, + placeholder, +}: TextAreaProps) => { + const value = getValueByPath(content, name) ?? defaultValue ?? ''; + + return ( + <Base + when={when} + content={content} + name={name} + onUpdate={onUpdate} + defaultValue={defaultValue} + > + <BaseInput title={title}> + <TextAreaUIKIT + value={value} + name={name} + onUpdate={(v) => onUpdate(name, v)} + minRows={3} + placeholder={placeholder} + /> + </BaseInput> + </Base> + ); +}; + +export default TextArea; diff --git a/src/form-generator-v2/components/TextInput/TextInput.tsx b/src/form-generator-v2/components/TextInput/TextInput.tsx new file mode 100644 index 0000000000..edeeff0e56 --- /dev/null +++ b/src/form-generator-v2/components/TextInput/TextInput.tsx @@ -0,0 +1,41 @@ +import {TextInput as TextInputUIKIT} from '@gravity-ui/uikit'; + +import {CommonProps, TextField} from '../../types'; +import {getValueByPath} from '../../utils/fields'; +import Base from '../Base/Base'; +import BaseInput from '../BaseInput/BaseInput'; + +type TextInputProps = CommonProps & TextField; + +const TextInput = ({ + title, + name, + when, + content, + onUpdate, + defaultValue, + placeholder, +}: TextInputProps) => { + const value = getValueByPath(content, name) ?? defaultValue ?? ''; + + return ( + <Base + when={when} + content={content} + name={name} + onUpdate={onUpdate} + defaultValue={defaultValue} + > + <BaseInput title={title}> + <TextInputUIKIT + name={name} + onUpdate={(v) => onUpdate(name, v)} + value={value} + placeholder={placeholder} + /> + </BaseInput> + </Base> + ); +}; + +export default TextInput; diff --git a/src/form-generator-v2/components/constants.ts b/src/form-generator-v2/components/constants.ts new file mode 100644 index 0000000000..635626c68f --- /dev/null +++ b/src/form-generator-v2/components/constants.ts @@ -0,0 +1,21 @@ +import ColorInput from './ColorInput/ColorInput'; +import Divider from './Divider/Divider'; +import Section from './Section/Section'; +import SegmentedRadioGroup from './SegmentedRadioGroup/SegmentedRadioGroup'; +import Select from './Select/Select'; +import Switch from './Switch/Switch'; +import Text from './Text/Text'; +import TextArea from './TextArea/TextArea'; +import TextInput from './TextInput/TextInput'; + +export const componentMap = { + section: Section, + select: Select, + textInput: TextInput, + segmentedRadioGroup: SegmentedRadioGroup, + colorInput: ColorInput, + text: Text, + divider: Divider, + textArea: TextArea, + switch: Switch, +}; diff --git a/src/form-generator-v2/index.ts b/src/form-generator-v2/index.ts new file mode 100644 index 0000000000..150e67bfcf --- /dev/null +++ b/src/form-generator-v2/index.ts @@ -0,0 +1,2 @@ +export {default as FormGeneratorV2} from './FormGenerator'; +export * from './types'; diff --git a/src/form-generator-v2/types.ts b/src/form-generator-v2/types.ts new file mode 100644 index 0000000000..f968c4fe59 --- /dev/null +++ b/src/form-generator-v2/types.ts @@ -0,0 +1,129 @@ +import {IconProps} from '@gravity-ui/uikit'; + +export type Content = Record<string, unknown>; + +export type When = { + field?: string; + operator: '===' | '!==' | '||' | '&&'; + value?: string | boolean; +}[]; + +export type Option = { + content?: string; + value: string; +}; + +export type SectionField = { + type: 'section'; + title: string; + fields: Fields; + when?: When; + // Static section props + opened?: boolean; + // Repeating group props — presence of `index` activates array mode + index?: string; + withAddButton?: boolean; + itemTitle?: string; // header text on each repeated item card + itemView?: 'card' | 'clear'; // card = Card with borders/padding; clear = flat div +}; + +export type SelectField = { + type: 'select'; + name: string; + title: string; + options: Option[]; + defaultValue?: string; + when?: When; + hasClear?: boolean; + placeholder?: string; +}; + +export type TextField = { + type: 'textInput' | 'textArea'; + name: string; + title: string; + defaultValue?: string; + when?: When; + placeholder?: string; +}; + +export type SegmentedRadioGroupField = { + type: 'segmentedRadioGroup'; + name: string; + title: string; + options: Option[]; + defaultValue?: string; + when?: When; +}; + +export type TextColor = + | 'primary' + | 'secondary' + | 'hint' + | 'info' + | 'positive' + | 'warning' + | 'danger' + | 'utility' + | 'misc'; + +export type Text = { + type: 'text'; + text: string; + level?: 'danger' | 'info'; + color?: TextColor; + when?: When; +}; + +export type DividerField = { + type: 'divider'; + when?: When; +}; + +export type SwitchField = { + type: 'switch'; + name: string; + title: string; + defaultValue?: boolean; + when?: When; +}; + +export type ColorField = { + type: 'colorInput'; + name: string; + title: string; + defaultValue?: string; + when?: When; +}; + +export type Fields = ( + | SectionField + | SelectField + | TextField + | SegmentedRadioGroupField + | Text + | DividerField + | SwitchField + | ColorField +)[]; + +export type OnUpdate = ( + name: string, + value: unknown, + options?: {unset?: boolean; removeArrayItemAt?: number}, +) => void; + +export type CommonProps = { + content: Content; + onUpdate: OnUpdate; +}; + +export interface BlockConfig { + name: string; + inputs: Fields; + group?: string; + hidden?: boolean; + default?: object; + previewImg?: string; + previewIcon?: IconProps; +} diff --git a/src/form-generator-v2/utils/cn.ts b/src/form-generator-v2/utils/cn.ts new file mode 100644 index 0000000000..2ba263d422 --- /dev/null +++ b/src/form-generator-v2/utils/cn.ts @@ -0,0 +1,5 @@ +import {withNaming} from '@bem-react/classname'; + +export const FORM_GENERATOR_NAMESPACE = 'pc-fg-'; + +export const formGeneratorCn = withNaming({n: FORM_GENERATOR_NAMESPACE, e: '__', m: '_'}); diff --git a/src/form-generator-v2/utils/fields.ts b/src/form-generator-v2/utils/fields.ts new file mode 100644 index 0000000000..1d26f99440 --- /dev/null +++ b/src/form-generator-v2/utils/fields.ts @@ -0,0 +1,314 @@ +import {get} from 'lodash'; + +import {Content, Fields} from '../types'; + +type NamePathSegment = + | {type: 'fixed'; prop: string; index: number} + | {type: 'placeholder'; prop: string; placeholder: string}; + +const getStaticPrefixBeforeArrayProp = (templateName: string): string => { + const firstBracketIndex = templateName.indexOf('['); + if (firstBracketIndex < 0) { + return ''; + } + + const beforeFirstBracket = templateName.slice(0, firstBracketIndex); + const lastDotIndex = beforeFirstBracket.lastIndexOf('.'); + if (lastDotIndex < 0) { + return ''; + } + + return beforeFirstBracket.slice(0, lastDotIndex); +}; + +const getSegmentsFromTemplate = (templateName: string): NamePathSegment[] => { + const re = /(\w+)\[(?:\{\{(\w+)\}\}|(\d+))\]/g; + const segments: NamePathSegment[] = []; + let match: RegExpExecArray | null; + + while ((match = re.exec(templateName)) !== null) { + const prop = match[1] ?? ''; + const placeholder = match[2]; + const fixedIndex = match[3]; + + if (placeholder !== undefined) { + segments.push({type: 'placeholder', prop, placeholder}); + } else if (fixedIndex !== undefined) { + const index = parseInt(fixedIndex, 10); + if (!Number.isNaN(index)) { + segments.push({type: 'fixed', prop, index}); + } + } + } + + return segments; +}; + +export const getArrayPathForNameWithIndexName = ( + templateName: string, + placeholder: string, +): string | undefined => { + const staticPrefix = getStaticPrefixBeforeArrayProp(templateName); + const segments = getSegmentsFromTemplate(templateName); + const placeholderIdx = segments.findIndex( + (s) => s.type === 'placeholder' && s.placeholder === placeholder, + ); + if (placeholderIdx < 0) { + return undefined; + } + + for (let i = 0; i < placeholderIdx; i++) { + const seg = segments[i]; + if (!seg || seg.type !== 'fixed') { + return undefined; + } + } + + const prefixParts: string[] = []; + for (let i = 0; i < placeholderIdx; i++) { + const s = segments[i] as {type: 'fixed'; prop: string; index: number}; + prefixParts.push(`${s.prop}[${s.index}]`); + } + + const at = segments[placeholderIdx] as {type: 'placeholder'; prop: string}; + const dynamicPrefix = prefixParts.join('.'); + const combinedPrefix = [staticPrefix, dynamicPrefix].filter(Boolean).join('.'); + return combinedPrefix ? `${combinedPrefix}.${at.prop}` : at.prop; +}; + +const getResolvedBrackets = (resolvedName: string): Array<{prop: string; index: number}> => + [...resolvedName.matchAll(/(\w+)\[(\d+)\]/g)].map((m) => ({ + prop: m[1] ?? '', + index: parseInt(m[2] ?? '0', 10), + })); + +export const findNameWithIndexName = (fields: Fields, indexName: string) => { + const indexNameWithBrackets = `{{${indexName}}}`; + const stack = [...fields]; + + while (stack.length > 0) { + const current = stack.pop(); + + if (current === null) { + continue; + } + + if (Array.isArray(current)) { + stack.push(...current); + continue; + } + + if (typeof current !== 'object') { + continue; + } + + const name = 'name' in current ? (current as {name?: string}).name : undefined; + + if (name && name.includes(indexNameWithBrackets)) { + return name; + } + + stack.push( + ...Object.values(current).filter( + (v): v is Fields[number] => v !== null && typeof v === 'object', + ), + ); + } + + return undefined; +}; + +export const getSpliceTarget = ( + nameWithIndexName: string, + resolvedName: string, + indexName: string, +): + | { + arrayPath: string; + itemIndex: number; + } + | undefined => { + const staticPrefix = getStaticPrefixBeforeArrayProp(nameWithIndexName); + const templateSegments = getSegmentsFromTemplate(nameWithIndexName); + const placeholderIdx = templateSegments.findIndex( + (s) => s.type === 'placeholder' && s.placeholder === indexName, + ); + if (placeholderIdx < 0) { + return undefined; + } + + const resolved = getResolvedBrackets(resolvedName); + const at = resolved[placeholderIdx]; + if (!at || Number.isNaN(at.index)) { + return undefined; + } + + const prefix: string[] = []; + for (let i = 0; i < placeholderIdx; i++) { + const part = resolved[i]; + if (!part) { + return undefined; + } + prefix.push(`${part.prop}[${part.index}]`); + } + + const dynamicPrefix = prefix.join('.'); + const combinedPrefix = [staticPrefix, dynamicPrefix].filter(Boolean).join('.'); + const arrayPath = combinedPrefix ? `${combinedPrefix}.${at.prop}` : at.prop; + + return { + arrayPath, + itemIndex: at.index, + }; +}; + +export const findAllNames = (fields: Fields) => { + const names: string[] = []; + const stack: (Fields | Fields[number])[] = [fields]; + + while (stack.length > 0) { + const current = stack.pop(); + + if (Array.isArray(current)) { + stack.push(...current); + } else if (current && typeof current === 'object') { + const name = 'name' in current && current.name; + if (typeof name === 'string') { + names.push(name); + } + stack.push(...Object.values(current)); + } + } + + return names; +}; + +export const getValueByPath = (content: Content, path: string | string[]) => { + return get(content, path); +}; + +const isValueNotEmpty = (value?: string | boolean | object | number) => { + if (value === null) return false; + if (typeof value === 'string') return value.trim() !== ''; + if (typeof value === 'boolean') return true; + if (typeof value === 'number') return !Number.isNaN(value); + if (Array.isArray(value)) return value.length > 0; + if (typeof value === 'object') return Object.keys(value as object).length > 0; + return true; +}; + +export const sectionHasContentData = (fields: Fields, content: Content) => { + if (!fields.length) { + return false; + } + + for (const field of fields) { + if (typeof field !== 'object') { + continue; + } + + if (field.type === 'section') { + if (field.index !== undefined) { + const nameWithIndexName = findNameWithIndexName(field.fields, field.index); + const arrayPath = nameWithIndexName + ? getArrayPathForNameWithIndexName(nameWithIndexName, field.index) + : undefined; + const arr = arrayPath ? getValueByPath(content, arrayPath) : undefined; + if (Array.isArray(arr) && arr.length > 0) return true; + } else if (sectionHasContentData(field.fields, content)) { + return true; + } + continue; + } + + if ('name' in field && field.name) { + return isValueNotEmpty(getValueByPath(content, field.name)); + } + } + + return false; +}; + +export type SectionClearOnUpdate = ( + path: string, + value: unknown, + options?: {unset?: boolean; removeArrayItemAt?: number}, +) => void; + +export type SectionScalarReset = + | {path: string; mode: 'unset'} + | {path: string; mode: 'set'; value: unknown}; + +export const collectSectionClearTargets = ( + fields: unknown[] | undefined | null, +): {arrayPaths: string[]; scalarResets: SectionScalarReset[]} => { + const arrayPaths = new Set<string>(); + const scalarResets: SectionScalarReset[] = []; + + const walk = (list: unknown[] | undefined | null) => { + if (!list?.length) { + return; + } + for (const field of list) { + if (!field || typeof field !== 'object') { + continue; + } + const f = field as Record<string, unknown>; + const type = f.type; + + if (type === 'section') { + const groupFields = f.fields; + const placeholder = f.index; + if (typeof placeholder === 'string' && Array.isArray(groupFields)) { + const templateName = findNameWithIndexName(groupFields, placeholder); + if (templateName) { + const arrayPath = getArrayPathForNameWithIndexName( + templateName, + placeholder, + ); + if (arrayPath) { + arrayPaths.add(arrayPath); + } + } + walk(groupFields); + } else { + walk(f.fields as unknown[]); + } + continue; + } + + if (typeof f.name === 'string' && f.name.length > 0 && !f.name.includes('{{')) { + if (type === 'segmentedRadioGroup' && Object.hasOwn(f, 'defaultValue')) { + scalarResets.push({path: f.name, mode: 'set', value: f.defaultValue}); + } else { + scalarResets.push({path: f.name, mode: 'unset'}); + } + } + } + }; + + walk(fields); + + return { + arrayPaths: [...arrayPaths], + scalarResets, + }; +}; + +export const clearSectionFormContent = ( + fields: unknown[] | undefined | null, + onUpdate: SectionClearOnUpdate, +) => { + const {arrayPaths, scalarResets} = collectSectionClearTargets(fields); + const arraySorted = [...arrayPaths].sort((a, b) => b.length - a.length); + for (const path of arraySorted) { + onUpdate(path, undefined, {unset: true}); + } + for (const item of scalarResets) { + if (item.mode === 'set') { + onUpdate(item.path, item.value as never); + } else { + onUpdate(item.path, undefined, {unset: true}); + } + } +}; diff --git a/src/form-generator-v2/utils/generateFormFieldsFromAjv.ts b/src/form-generator-v2/utils/generateFormFieldsFromAjv.ts new file mode 100644 index 0000000000..46a845219b --- /dev/null +++ b/src/form-generator-v2/utils/generateFormFieldsFromAjv.ts @@ -0,0 +1,502 @@ +import type {JSONSchemaType} from 'ajv'; + +import type {Fields, SectionField, SegmentedRadioGroupField, When} from '../types'; + +export type GenerateFormFieldsFromAjvSchemaOptions = { + arrayIndexPlaceholder?: (arrayDepth: number) => string; + wrapInRootSection?: boolean; + rootSectionTitle?: string; + generalSectionTitle?: string; + skipProperties?: string[]; +}; + +type ResolvedOptions = Required< + Pick<GenerateFormFieldsFromAjvSchemaOptions, 'arrayIndexPlaceholder' | 'skipProperties'> +> & + Pick< + GenerateFormFieldsFromAjvSchemaOptions, + 'wrapInRootSection' | 'rootSectionTitle' | 'generalSectionTitle' + >; + +const defaultArrayPlaceholder = (arrayDepth: number): string => `index${arrayDepth + 1}`; + +const DEFAULT_SKIP_PROPERTIES = ['when']; + +function resolveOptions(options?: GenerateFormFieldsFromAjvSchemaOptions): ResolvedOptions { + return { + arrayIndexPlaceholder: options?.arrayIndexPlaceholder ?? defaultArrayPlaceholder, + skipProperties: options?.skipProperties ?? DEFAULT_SKIP_PROPERTIES, + wrapInRootSection: options?.wrapInRootSection, + rootSectionTitle: options?.rootSectionTitle, + generalSectionTitle: options?.generalSectionTitle, + }; +} + +// ─── Title helpers ──────────────────────────────────────────────────────────── + +function formatKeyAsTitle(key: string): string { + if (!key) return ''; + // strip leading underscores/dashes, then split on camelCase and separators + const clean = key.replace(/^[_-]+/, ''); + return clean + .replace(/([a-z])([A-Z])/g, '$1 $2') + .replace(/[_-]+/g, ' ') + .replace(/\s+/g, ' ') + .trim() + .replace(/^\w/, (c) => c.toUpperCase()); +} + +function resolveTitle(schema: JSONSchemaType<{}>, key: string): string { + const s = schema as Record<string, unknown>; + if (typeof s['title'] === 'string' && s['title']) return s['title']; + if (typeof s['optionName'] === 'string' && s['optionName']) return s['optionName']; + return formatKeyAsTitle(key); +} + +// ─── Path helpers ───────────────────────────────────────────────────────────── + +const joinPath = (base: string, key: string): string => { + if (!key) return base; + return base ? `${base}.${key}` : key; +}; + +function makeArrayIndexPlaceholder( + pathPrefix: string, + fieldName: string, + arrayDepth: number, + placeholder: (d: number) => string, +): string { + const base = placeholder(arrayDepth); + const fullPath = joinPath(pathPrefix, fieldName); + const safeSuffix = fullPath.replace(/[^\w]/g, '_'); + return safeSuffix ? `${base}_${safeSuffix}` : base; +} + +function oneOfDiscriminatorKey(pathPrefix: string, fieldName: string): string { + const safeName = fieldName.replace(/[^\w]/g, '_'); + return joinPath(pathPrefix, `__oneOf_${safeName}`); +} + +// ─── When helpers ───────────────────────────────────────────────────────────── + +function sanitizeWhen(when: When | undefined): When | undefined { + if (!when?.length) return undefined; + + const result: When = []; + for (const token of when) { + if (token.field) { + if ( + (token.operator === '===' || token.operator === '!==') && + token.value !== undefined + ) { + result.push(token); + } + continue; + } + if ((token.operator === '&&' || token.operator === '||') && result.length > 0) { + result.push(token); + } + } + + while (result.length > 0 && !result[result.length - 1]?.field) { + result.pop(); + } + + return result.length > 0 ? result : undefined; +} + +function mergeWhen(existing: When | undefined, extra: When | undefined): When | undefined { + const left = sanitizeWhen(existing); + const right = sanitizeWhen(extra); + if (!right?.length) return left; + if (!left?.length) return right; + return sanitizeWhen([...left, {operator: '&&' as const}, ...right]); +} + +function parseShowIfToWhen(showIf?: string): When | undefined { + if (!showIf) return undefined; + const parts = showIf.trim().split(/\s+/); + if (parts.length !== 3) return undefined; + const [field, op, raw] = parts; + if (op !== '===' && op !== '!==') return undefined; + let value: string | boolean = raw; + if (raw === 'true') value = true; + else if (raw === 'false') value = false; + else if ( + (raw.startsWith('"') && raw.endsWith('"')) || + (raw.startsWith("'") && raw.endsWith("'")) + ) { + value = raw.slice(1, -1); + } + return [{field, operator: op, value}]; +} + +function withWhen(field: Fields[number], when: When | undefined): Fields[number] { + if (!when?.length) return field; + return {...field, when} as Fields[number]; +} + +function applyBranchWhenToFields(fields: Fields, branchWhen: When | undefined): Fields { + if (!branchWhen?.length) return fields; + return fields.map( + (field) => + ({ + ...field, + when: mergeWhen((field as {when?: When}).when, branchWhen), + }) as Fields[number], + ); +} + +// ─── Sort helpers ───────────────────────────────────────────────────────────── + +function sortGeneratedFieldsSectionsLast(fields: Fields): Fields { + const withSortedChildren = fields.map((field) => { + if (field.type === 'section') { + return {...field, fields: sortGeneratedFieldsSectionsLast(field.fields)}; + } + return field; + }); + const nonSection: Fields = []; + const sections: Fields = []; + for (const field of withSortedChildren) { + if (field.type === 'section') sections.push(field); + else nonSection.push(field); + } + return [...nonSection, ...sections]; +} + +// ─── Schema type helpers ────────────────────────────────────────────────────── + +function isTextArea(schema: JSONSchemaType<{}>): boolean { + const s = schema as Record<string, unknown>; + return s['inputType'] === 'textarea' || s['contentType'] === 'yfm'; +} + +// ─── Converters ─────────────────────────────────────────────────────────────── + +function convertScalar( + fieldPath: string, + schema: JSONSchemaType<{}>, + when: When | undefined, +): Fields { + const s = schema as Record<string, unknown>; + const title = resolveTitle(schema, fieldPath.split('.').pop() ?? fieldPath); + + const type = schema.type as string | undefined; + const enumValues = (s['enum'] as string[] | undefined) ?? undefined; + + if (enumValues?.length || (!type && enumValues)) { + return [ + withWhen( + { + type: 'select' as const, + name: fieldPath, + title, + options: (enumValues ?? []).map((e) => ({ + value: String(e), + content: String(e), + })), + }, + when, + ), + ]; + } + + if (type === 'string') { + if (isTextArea(schema)) { + return [withWhen({type: 'textArea' as const, name: fieldPath, title}, when)]; + } + return [withWhen({type: 'textInput' as const, name: fieldPath, title}, when)]; + } + + if (type === 'number' || type === 'integer') { + return [withWhen({type: 'textInput' as const, name: fieldPath, title}, when)]; + } + + if (type === 'boolean') { + return [withWhen({type: 'switch' as const, name: fieldPath, title}, when)]; + } + + return []; +} + +function convertVariantFields( + variant: JSONSchemaType<{}>, + parentPath: string, + arrayDepth: number, + opts: ResolvedOptions, +): Fields { + const s = variant as Record<string, unknown>; + if (variant.type === 'object' && s['properties']) { + return convertProperties( + Object.entries(s['properties'] as Record<string, JSONSchemaType<{}>>), + parentPath, + arrayDepth, + opts, + ); + } + return convertScalar(parentPath, variant, undefined); +} + +function convertOneOrAnyOf( + key: string, + variants: JSONSchemaType<{}>[], + fieldPath: string, + title: string, + when: When | undefined, + pathPrefix: string, + arrayDepth: number, + opts: ResolvedOptions, +): Fields { + const metaKey = oneOfDiscriminatorKey(pathPrefix, key); + + const variantOptions = variants.map((v, i) => { + const s = v as Record<string, unknown>; + const value = + (s['optionName'] as string | undefined) ?? + (s['type'] as string | undefined) ?? + String(i); + const content = + (s['optionName'] as string | undefined) ?? + (s['title'] as string | undefined) ?? + (s['type'] as string | undefined) ?? + String(i); + return {value, content}; + }); + + const discriminator: SegmentedRadioGroupField = { + type: 'segmentedRadioGroup', + name: metaKey, + title, + options: variantOptions, + }; + + const branchFields = variants.flatMap((variant, i) => { + const branchWhen: When = [ + {field: metaKey, operator: '===', value: variantOptions[i].value}, + ]; + const fields = convertVariantFields(variant, fieldPath, arrayDepth, opts); + return applyBranchWhenToFields(fields, branchWhen); + }); + + const section: SectionField = { + type: 'section', + title, + fields: [discriminator, ...branchFields], + }; + + return [withWhen(section, when)]; +} + +function convertArray( + key: string, + schema: JSONSchemaType<{}>, + fieldPath: string, + title: string, + when: When | undefined, + pathPrefix: string, + arrayDepth: number, + opts: ResolvedOptions, +): Fields { + const items = (schema as Record<string, unknown>)['items'] as JSONSchemaType<{}> | undefined; + if (!items) return []; + + const ph = makeArrayIndexPlaceholder(pathPrefix, key, arrayDepth, opts.arrayIndexPlaceholder); + const itemPath = `${fieldPath}[{{${ph}}}]`; + + const itemsType = (items as Record<string, unknown>)['type'] as string | undefined; + const itemsEnum = (items as Record<string, unknown>)['enum'] as unknown[] | undefined; + + let innerFields: Fields; + if (itemsType === 'string' || itemsEnum) { + innerFields = [{type: 'textInput', name: itemPath, title}]; + } else if (itemsType === 'object') { + const itemProps = (items as Record<string, unknown>)['properties'] as + | Record<string, JSONSchemaType<{}>> + | undefined; + innerFields = itemProps + ? convertProperties(Object.entries(itemProps), itemPath, arrayDepth + 1, opts) + : []; + } else { + // items with oneOf/anyOf or other complex types — delegate to convertProperty + innerFields = convertProperty('', items, itemPath, arrayDepth + 1, opts); + } + + const section: SectionField = { + type: 'section', + index: ph, + withAddButton: true, + title, + itemTitle: `${title} {{${ph}}}`, + fields: innerFields, + }; + + return [withWhen(section, when)]; +} + +function convertProperty( + key: string, + schema: JSONSchemaType<{}>, + pathPrefix: string, + arrayDepth: number, + opts: ResolvedOptions, +): Fields { + const s = schema as Record<string, unknown>; + const fieldPath = joinPath(pathPrefix, key); + const title = resolveTitle(schema, key); + + const showIf = (s['showIf'] as string | undefined) ?? undefined; + const when = parseShowIfToWhen(showIf); + + const schemaEnum = s['enum'] as string[] | undefined; + const schemaType = schema.type as string | undefined; + + // oneOf / anyOf + if (s['oneOf']) { + return convertOneOrAnyOf( + key, + s['oneOf'] as JSONSchemaType<{}>[], + fieldPath, + title, + when, + pathPrefix, + arrayDepth, + opts, + ); + } + if (s['anyOf']) { + return convertOneOrAnyOf( + key, + s['anyOf'] as JSONSchemaType<{}>[], + fieldPath, + title, + when, + pathPrefix, + arrayDepth, + opts, + ); + } + + // enum without explicit type + if (schemaEnum && !schemaType) { + return [ + withWhen( + { + type: 'select' as const, + name: fieldPath, + title, + options: schemaEnum.map((e) => ({value: String(e), content: String(e)})), + }, + when, + ), + ]; + } + + switch (schemaType) { + case 'string': { + if (schemaEnum) { + return [ + withWhen( + { + type: 'select' as const, + name: fieldPath, + title, + options: schemaEnum.map((e) => ({ + value: String(e), + content: String(e), + })), + }, + when, + ), + ]; + } + if (isTextArea(schema)) { + return [withWhen({type: 'textArea' as const, name: fieldPath, title}, when)]; + } + return [withWhen({type: 'textInput' as const, name: fieldPath, title}, when)]; + } + + case 'number': + case 'integer': + return [withWhen({type: 'textInput' as const, name: fieldPath, title}, when)]; + + case 'boolean': + return [withWhen({type: 'switch' as const, name: fieldPath, title}, when)]; + + case 'object': { + const props = s['properties'] as Record<string, JSONSchemaType<{}>> | undefined; + if (!props) return []; + return [ + withWhen( + { + type: 'section' as const, + title, + fields: convertProperties( + Object.entries(props), + fieldPath, + arrayDepth, + opts, + ), + }, + when, + ), + ]; + } + + case 'array': + return convertArray(key, schema, fieldPath, title, when, pathPrefix, arrayDepth, opts); + + default: + return []; + } +} + +function convertProperties( + entries: [string, JSONSchemaType<{}>][], + pathPrefix: string, + arrayDepth: number, + opts: ResolvedOptions, +): Fields { + return entries + .filter(([key]) => !opts.skipProperties.includes(key)) + .flatMap(([key, schema]) => convertProperty(key, schema, pathPrefix, arrayDepth, opts)); +} + +export function generateFormFieldsFromAjvSchema( + schema: JSONSchemaType<{}>, + options?: GenerateFormFieldsFromAjvSchemaOptions, +): Fields { + const opts = resolveOptions(options); + const props = (schema as Record<string, unknown>)['properties'] as + | Record<string, JSONSchemaType<{}>> + | undefined; + + if (!props) return []; + + const inner = sortGeneratedFieldsSectionsLast( + convertProperties(Object.entries(props), '', 0, opts), + ); + + if (opts.wrapInRootSection) { + return [ + { + type: 'section', + title: opts.rootSectionTitle ?? 'Settings', + fields: inner, + }, + ]; + } + + const nonSections = inner.filter((f) => f.type !== 'section'); + const sections = inner.filter((f) => f.type === 'section'); + + if (nonSections.length === 0) return sections; + + const generalSection: SectionField = { + type: 'section', + title: opts.generalSectionTitle ?? 'General', + fields: nonSections, + }; + + return [generalSection, ...sections]; +} diff --git a/src/form-generator/FormGenerator.scss b/src/form-generator/FormGenerator.scss new file mode 100644 index 0000000000..95d96eee2f --- /dev/null +++ b/src/form-generator/FormGenerator.scss @@ -0,0 +1,10 @@ +@import './styles/variables.scss'; +@import './styles/mixins.scss'; + +$block: '.#{$ns-form-generator}form-generator'; + +#{$block} { + display: flex; + flex-direction: column; + gap: 12px; +} diff --git a/src/form-generator/FormGenerator.tsx b/src/form-generator/FormGenerator.tsx new file mode 100644 index 0000000000..f353e8e0f6 --- /dev/null +++ b/src/form-generator/FormGenerator.tsx @@ -0,0 +1,249 @@ +import * as React from 'react'; + +import _ from 'lodash'; + +import AnyOfDynamicField from './components/Fields/AnyOf/AnyOf'; +import ArrayDynamicField from './components/Fields/Array/Array'; +import BooleanDynamicField from './components/Fields/Boolean/Boolean'; +import NumberDynamicField from './components/Fields/Number/Number'; +import ObjectDynamicField from './components/Fields/Object/Object'; +import OneOfDynamicField from './components/Fields/OneOf/OneOf'; +import SelectDynamicField from './components/Fields/Select/Select'; +import TextDynamicField from './components/Fields/Text/Text'; +import TextAreaDynamicField from './components/Fields/TextArea/TextArea'; +import {ConfigInput, DynamicFormValue} from './types'; +import {formGeneratorCn} from './utils/cn'; +import {getContent, getFullPath} from './utils/common'; + +import './FormGenerator.scss'; + +const b = formGeneratorCn('form-generator'); + +interface FormGeneratorProps { + blockConfig: Array<ConfigInput>; + contentConfig?: object; + onUpdateByKey?: (key: string, value: DynamicFormValue) => void; + onUpdate?: (value: object) => void; + className?: string; +} + +export const FormGenerator = ({ + blockConfig, + onUpdateByKey, + onUpdate, + contentConfig, +}: FormGeneratorProps) => { + const inputs = blockConfig; + + const onDataUpdate = React.useCallback( + (key: string, value: DynamicFormValue) => { + if (onUpdateByKey) { + onUpdateByKey(key, value); + } + if (onUpdate && contentConfig) { + const newContentConfig = _.cloneDeep(contentConfig); + _.set(newContentConfig, key, value); + onUpdate(newContentConfig); + } + }, + [onUpdateByKey, onUpdate, contentConfig], + ); + + const getData = React.useCallback( + (variable: string) => { + if (variable.startsWith('block.')) { + const purePath = variable.replace('block.', ''); + return _.get(contentConfig, purePath); + } + + if ( + (variable.startsWith(`'`) && variable.endsWith(`'`)) || + (variable.startsWith(`"`) && variable.endsWith(`"`)) + ) { + // @ts-ignore TODO: replaceAll types + return variable.replaceAll(`'`, '').replaceAll(`"`, ''); + } + + return undefined; + }, + [contentConfig], + ); + + const decide = React.useCallback( + (showIf: string): boolean => { + const parts = showIf.split(' '); + + if (!(parts.length === 3)) { + // eslint-disable-next-line no-console + console.log('Something bad happened in showIf, ignored'); + return true; + } + + const [firstVariable, equals, secondVariable] = parts; + + const data1 = getData(firstVariable); + const data2 = getData(secondVariable); + + if (equals === '===') { + return data1 === data2; + } else { + return data1 !== data2; + } + }, + [getData], + ); + + const renderInput = React.useCallback( + (input: ConfigInput) => { + const fieldPath = input.name; + const fieldValue = getContent(contentConfig, input.name); + + if (input.showIf) { + const decision = decide(input.showIf); + + if (!decision) { + return <div>Hidden Field: {input.name}</div>; + } + } + + // Text, Select, Boolean and etc + const onSimpleDynamicFieldUpdate = (value: DynamicFormValue) => { + onDataUpdate(fieldPath, value); + }; + + // Array and Objects + const onComplexDynamicFieldUpdate = (key: string, value: DynamicFormValue) => { + onDataUpdate(getFullPath(fieldPath, key), value); + }; + + switch (input.type) { + case 'text': { + return ( + <TextDynamicField + onRefresh={(value) => onDataUpdate(fieldPath, value)} + title={input.title} + value={fieldValue} + onUpdate={onSimpleDynamicFieldUpdate} + /> + ); + } + case 'boolean': { + return ( + <BooleanDynamicField + onRefresh={(value) => onDataUpdate(fieldPath, value)} + title={input.title} + value={fieldValue} + onUpdate={onSimpleDynamicFieldUpdate} + /> + ); + } + case 'textarea': { + return ( + <TextAreaDynamicField + onRefresh={(value) => onDataUpdate(fieldPath, value)} + title={input.title} + value={fieldValue} + onUpdate={onSimpleDynamicFieldUpdate} + /> + ); + } + case 'select': { + return ( + <SelectDynamicField + onRefresh={(value) => onDataUpdate(fieldPath, value)} + input={input} + value={fieldValue} + onUpdate={onSimpleDynamicFieldUpdate} + /> + ); + } + case 'number': { + return ( + <NumberDynamicField + onRefresh={(value) => onDataUpdate(fieldPath, value)} + title={input.title} + value={fieldValue} + onUpdate={onSimpleDynamicFieldUpdate} + /> + ); + } + case 'object': { + if (!input || !('properties' in input)) { + return null; + } + + return ( + <ObjectDynamicField + onRefresh={(value) => onDataUpdate(fieldPath, value)} + blockConfig={input.properties} + title={input.title} + value={fieldValue} + onUpdate={onComplexDynamicFieldUpdate} + /> + ); + } + case 'array': { + return ( + <ArrayDynamicField + blockConfig={input} + title={input.title} + values={fieldValue} + onUpdate={onComplexDynamicFieldUpdate} + /> + ); + } + case 'oneOf': { + if (!input || !('options' in input)) { + return null; + } + + return ( + <OneOfDynamicField + inputConfig={input} + contentConfig={contentConfig} + onUpdate={onComplexDynamicFieldUpdate} + /> + ); + } + case 'anyOf': { + if (!input || !('options' in input)) { + return null; + } + + return ( + <AnyOfDynamicField + inputConfig={input} + contentConfig={contentConfig} + onUpdate={onComplexDynamicFieldUpdate} + /> + ); + } + default: { + return <div>Ignore {JSON.stringify(input)}</div>; + } + } + }, + [contentConfig, decide, onDataUpdate], + ); + + const sortedInputs = inputs.sort((x, y) => { + const nestingFieldTypes = ['object', 'array', 'oneOf', 'anyOf']; + if (nestingFieldTypes.includes(x.type)) { + return 1; + } + if (nestingFieldTypes.includes(y.type)) { + return -1; + } + return 0; + }); + + return ( + <div className={b()}> + {sortedInputs.map((input, index) => ( + <React.Fragment key={index}>{renderInput(input)}</React.Fragment> + ))} + </div> + ); +}; + +export default FormGenerator; diff --git a/src/form-generator/README.md b/src/form-generator/README.md new file mode 100644 index 0000000000..818cbee29c --- /dev/null +++ b/src/form-generator/README.md @@ -0,0 +1,161 @@ +# FormGenerator + +Компонент для динамической генерации форм на основе конфигурации. Поддерживает различные типы полей, вложенные структуры и условное отображение. + +## Подключение + +Импортируйте компонент в ваш файл: + +```typescript +import {FormGenerator} from '@gravity-ui/page-constructor/form-generator'; +``` + +## Пропсы + +Компонент принимает следующие пропсы: + +| Пропс | Тип | Описание | +| --------------- | ------------------------------------------------ | --------------------------------------------------------------------------------- | +| `blockConfig` | `Array<ConfigInput>` | Массив конфигураций полей формы | +| `contentConfig` | `object` | Объект с текущими значениями полей | +| `onUpdateByKey` | `(key: string, value: DynamicFormValue) => void` | Колбэк, вызываемый при изменении значения поля по ключу | +| `onUpdate` | `(value: object) => void` | Колбэк, вызываемый при изменении любого поля, с обновленным объектом конфигурации | +| `className` | `string` | Дополнительный CSS-класс для обертки формы | + +## Типы полей + +FormGenerator поддерживает следующие типы полей: + +- `text` - текстовое поле +- `boolean` - переключатель +- `textarea` - многострочное текстовое поле +- `select` - выпадающий список или радиокнопки +- `number` - числовое поле +- `object` - вложенный объект с полями +- `array` - массив значений +- `oneOf` - выбор одного варианта из нескольких +- `anyOf` - выбор нескольких вариантов из нескольких + +## Условное отображение + +Поле можно показывать/скрывать на основе значения другого поля с помощью параметра `showIf` в конфигурации поля: + +```typescript +{ + name: 'advancedOptions', + title: 'Расширенные настройки', + type: 'object', + properties: [...], + showIf: 'showAdvanced === true' +} +``` + +В `showIf` можно использовать: + +- `block.<path>` - для доступа к значениям других полей +- Строковые литералы в кавычках +- Операторы `===` и `!==` + +## Примеры использования + +### Базовое использование + +```tsx +import * as React from 'react'; +import DynamicForm from '../../src/form-generator/FormGenerator'; + +const MyForm = () => { + const [contentConfig, setContentConfig] = React.useState({}); + + const blockConfig = [ + { + type: 'text', + name: 'title', + title: 'Заголовок', + }, + { + type: 'number', + name: 'count', + title: 'Количество', + }, + { + type: 'boolean', + name: 'isActive', + title: 'Активный', + }, + ]; + + return ( + <DynamicForm + blockConfig={blockConfig} + contentConfig={contentConfig} + onUpdate={setContentConfig} + /> + ); +}; +``` + +### Вложенные структуры + +```tsx +const blockConfig = [ + { + type: 'object', + name: 'style', + title: 'Стиль', + properties: [ + { + type: 'text', + name: 'color', + title: 'Цвет', + }, + { + type: 'number', + name: 'fontSize', + title: 'Размер шрифта', + }, + ], + }, +]; +``` + +### Массивы + +```tsx +const blockConfig = [ + { + type: 'array', + name: 'items', + title: 'Элементы', + arrayType: 'text', + buttonText: 'Добавить элемент', + }, +]; +``` + +### Условное отображение + +```tsx +const blockConfig = [ + { + type: 'select', + name: 'styleType', + title: 'Тип стиля', + view: 'select', + mode: 'single', + enum: [ + {value: 'simple', content: 'Простой'}, + {value: 'advanced', content: 'Расширенный'}, + ], + }, + { + type: 'object', + name: 'advancedStyle', + title: 'Расширенные настройки', + properties: [ + // ... поля + ], + showIf: 'styleType === "advanced"', + }, +]; +``` diff --git a/src/form-generator/components/FieldBase/FieldBase.scss b/src/form-generator/components/FieldBase/FieldBase.scss new file mode 100644 index 0000000000..569faa5104 --- /dev/null +++ b/src/form-generator/components/FieldBase/FieldBase.scss @@ -0,0 +1,109 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +$block: '.#{$ns-form-generator}field-base'; + +#{$block} { + $class: &; + padding: 0; + + position: relative; + display: flex; + flex-direction: column; + align-items: stretch; + + &:hover { + & > #{$class}__top #{$class}__title > #{$class}__button { + opacity: 1; + } + } + + &_expandable { + flex-direction: column; + + & > #{$block}__top { + width: 100%; + flex-basis: initial; + } + + & > #{$block}__children { + width: 100%; + } + } + + &__top { + display: flex; + align-items: center; + gap: 4px; + word-break: break-word; + } + + &__children { + &_expandable { + border-left: 3px solid var(--g-color-line-generic); + padding: 6px 12px; + } + } + + &__foldable { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + padding: 6px 0; + // border: 3px solid var(--g-color-line-generic); + border-top-left-radius: var(--g-border-radius-m); + border-top-right-radius: var(--g-border-radius-m); + + &:hover { + // border: 3px solid transparent; + color: var(--g-color-text-link-hover); + } + + &:active { + color: var(--g-color-text-link-active); + } + } + + &__non-foldable { + &:before { + //content: '•'; + position: absolute; + right: calc(100% + 5px); + } + } + + &__button { + transition: opacity 0.3s ease; + opacity: 0; + margin-left: 2px; + } + + &__title { + @include text-body-2; + font-weight: 500; + margin: 5px 16px 5px 0; + display: flex; + flex-direction: row; + align-items: center; + + &_size { + &_s { + @include text-body-1; + } + + &_m { + @include text-body-2; + } + + &_l { + @include text-body-3; + } + } + } + + &:last-child { + border-bottom: none; + } +} diff --git a/src/form-generator/components/FieldBase/FieldBase.tsx b/src/form-generator/components/FieldBase/FieldBase.tsx new file mode 100644 index 0000000000..9295d7174c --- /dev/null +++ b/src/form-generator/components/FieldBase/FieldBase.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; + +import {ArrowRotateLeft} from '@gravity-ui/icons'; +import {ArrowToggle, Button, Icon} from '@gravity-ui/uikit'; +import _ from 'lodash'; + +import {formGeneratorCn} from '../../utils/cn'; + +import './FieldBase.scss'; + +const b = formGeneratorCn('field-base'); + +export interface FieldBaseParams { + title?: string; + textSize?: 's' | 'm' | 'l'; + onRefresh?: (value: undefined) => void; + expandable?: boolean; +} + +export interface FieldBaseProps extends React.PropsWithChildren, FieldBaseParams { + className?: string; +} + +const FieldBase: React.FC<FieldBaseProps> = ({ + className, + title, + textSize = 's', + children, + expandable = false, + onRefresh, +}) => { + const [showChildren, setShowChildren] = React.useState(!expandable); + + const titleComponent = React.useMemo(() => { + if (title) { + const defaultTitle = ( + <div className={b('title', {size: textSize})}> + <span>{_.capitalize(title)}</span> + {onRefresh && ( + <Button + className={b('button')} + onClick={(e: React.MouseEvent<HTMLButtonElement>) => { + e.stopPropagation(); + onRefresh(undefined); + }} + view={'flat'} + size={'xs'} + > + <Icon data={ArrowRotateLeft} size={14} /> + </Button> + )} + </div> + ); + + if (expandable) { + return ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions + <div className={b('foldable')} onClick={() => setShowChildren(!showChildren)}> + {defaultTitle} + <ArrowToggle + direction={showChildren ? 'bottom' : 'right'} + className={b('arrow-toggle')} + /> + </div> + ); + } + + return <div className={b('non-foldable')}>{defaultTitle}</div>; + } + + return null; + }, [expandable, showChildren, textSize, title, onRefresh]); + + return ( + <div className={b({expandable}, className)}> + {title && <div className={b('top')}>{titleComponent}</div>} + {(!title || showChildren) && ( + <div className={b('children', {expandable})}>{children}</div> + )} + </div> + ); +}; + +export default FieldBase; diff --git a/src/form-generator/components/Fields/AnyOf/AnyOf.scss b/src/form-generator/components/Fields/AnyOf/AnyOf.scss new file mode 100644 index 0000000000..a53177753e --- /dev/null +++ b/src/form-generator/components/Fields/AnyOf/AnyOf.scss @@ -0,0 +1,14 @@ +@import '../../../styles/variables.scss'; +@import '../../../styles/mixins.scss'; + +$block: '.#{$ns-form-generator}anyof-dynamic-field'; + +#{$block} { + &__radio { + margin: 16px 12px; + } + + &__select { + min-width: 80px; + } +} diff --git a/src/form-generator/components/Fields/AnyOf/AnyOf.tsx b/src/form-generator/components/Fields/AnyOf/AnyOf.tsx new file mode 100644 index 0000000000..478e5a6528 --- /dev/null +++ b/src/form-generator/components/Fields/AnyOf/AnyOf.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; + +import {Card, SegmentedRadioGroup, Select} from '@gravity-ui/uikit'; + +import DynamicForm from '../../../FormGenerator'; +import {AnyOfInput, DynamicFormValue} from '../../../types'; +import {formGeneratorCn} from '../../../utils/cn'; +import FieldBase from '../../FieldBase/FieldBase'; + +import './AnyOf.scss'; + +const b = formGeneratorCn('anyof-dynamic-field'); + +interface AnyOfDynamicFieldProps { + contentConfig: DynamicFormValue; + onUpdate: (key: string, value: DynamicFormValue) => void; + inputConfig: AnyOfInput; + className?: string; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getAnyOfContentConfig = (contentConfig: any, name: string) => { + if (name) { + return contentConfig ? contentConfig[name] : {}; + } + return contentConfig; +}; + +const AnyOfDynamicField = ({ + contentConfig, + onUpdate, + className, + inputConfig, +}: AnyOfDynamicFieldProps) => { + const defaultValue = inputConfig.options[0].value; + + const [anyOfMetaValue, setAnyOfMetaValue] = React.useState(defaultValue); + + const anyOfContentConfig = getAnyOfContentConfig(contentConfig, inputConfig.name); + const anyOfChosenOption = React.useMemo( + () => + inputConfig.options.find( + ({value: foundAnyOfValue}) => foundAnyOfValue === anyOfMetaValue, + ), + [inputConfig.options, anyOfMetaValue], + ); + + const onUpdateAnyOf = React.useCallback((value: string) => { + setAnyOfMetaValue(value); + }, []); + + return ( + <FieldBase + title={inputConfig.title} + className={b(null, className)} + onRefresh={(value) => onUpdate('', value)} + expandable + > + <Card> + {inputConfig.options.length < 4 ? ( + <SegmentedRadioGroup + className={b('radio')} + options={inputConfig.options.map((option) => ({ + content: option.title, + value: option.value, + }))} + value={anyOfMetaValue} + onUpdate={onUpdateAnyOf} + /> + ) : ( + <Select + options={inputConfig.options.map((option) => ({ + content: option.title, + value: option.value, + }))} + value={anyOfMetaValue ? [anyOfMetaValue] : []} + onUpdate={([selectValue]) => onUpdateAnyOf(selectValue)} + className={b('select')} + /> + )} + {anyOfChosenOption && ( + <DynamicForm + blockConfig={anyOfChosenOption.properties} + contentConfig={anyOfContentConfig} + onUpdateByKey={onUpdate} + /> + )} + </Card> + </FieldBase> + ); +}; + +export default AnyOfDynamicField; diff --git a/src/form-generator/components/Fields/Array/Array.scss b/src/form-generator/components/Fields/Array/Array.scss new file mode 100644 index 0000000000..4118d3a472 --- /dev/null +++ b/src/form-generator/components/Fields/Array/Array.scss @@ -0,0 +1,52 @@ +@import '../../../styles/variables.scss'; +@import '../../../styles/mixins.scss'; + +$block: '.#{$ns-form-generator}array-dynamic-field'; + +#{$block} { + &__card { + padding: 12px; + margin-top: 12px; + border-left: 3px solid var(--g-color-line-generic); + + &:first-child { + margin-top: 0; + } + } + + &__row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + + margin-top: 10px; + + &:first-child { + margin-top: 0; + } + } + + &__card-head { + margin-bottom: 8px; + } + + &__row-title { + @include text-subheader-3; + } + + &__row-title, + &__row-field { + flex: 1; + } + + &__empty { + padding: 10px; + display: flex; + justify-content: center; + } + + &__add-button { + margin-top: 12px; + } +} diff --git a/src/form-generator/components/Fields/Array/Array.tsx b/src/form-generator/components/Fields/Array/Array.tsx new file mode 100644 index 0000000000..cf6aa2cc32 --- /dev/null +++ b/src/form-generator/components/Fields/Array/Array.tsx @@ -0,0 +1,156 @@ +import * as React from 'react'; + +import {Plus} from '@gravity-ui/icons'; +import {Button, Icon} from '@gravity-ui/uikit'; + +import {removeFromArray, swapArrayItems} from '../../../../editor-v2/utils'; +import DynamicForm from '../../../FormGenerator'; +import {ArrayObjectInput, ArrayTextInput, DynamicFormValue} from '../../../types'; +import {formGeneratorCn} from '../../../utils/cn'; +import FieldBase from '../../FieldBase/FieldBase'; +import Text from '../Text/Text'; + +import ItemButton from './ItemButton/ItemButton'; + +import './Array.scss'; + +const b = formGeneratorCn('array-dynamic-field'); + +type ArrayInput = ArrayTextInput | ArrayObjectInput; + +interface ArrayFieldProps { + title: string; + values: Array<DynamicFormValue>; + onUpdate: (key: string, value: DynamicFormValue) => void; + blockConfig: ArrayInput; + className?: string; +} + +const ArrayDynamicField = ({title, values, onUpdate, className, blockConfig}: ArrayFieldProps) => { + const haveItems = values && Array.isArray(values) && values.length; + + const onAddItem = React.useCallback(() => { + if (blockConfig.arrayType === 'text') { + onUpdate('', haveItems ? [...values, ''] : ['']); + } else if (blockConfig.arrayType === 'object') { + onUpdate('', haveItems ? [...values, {}] : [{}]); + } + }, [blockConfig.arrayType, haveItems, onUpdate, values]); + + const onDeleteItem = React.useCallback( + (index: number) => { + if (Array.isArray(values)) { + const newArray = removeFromArray(values, index); + onUpdate('', newArray); + } + }, + [onUpdate, values], + ); + + const onReorderItem = React.useCallback( + (index: number, placement: 'up' | 'down') => { + if (Array.isArray(values)) { + const newArray = swapArrayItems( + values, + index, + placement === 'up' ? index - 1 : index + 1, + ); + onUpdate('', newArray); + } + }, + [onUpdate, values], + ); + + const renderInput = React.useCallback( + (value: DynamicFormValue, index: number) => { + const arrayItemButton = ( + <ItemButton + onRemove={() => onDeleteItem(index)} + onReorderUp={() => onReorderItem(index, 'up')} + onReorderDown={() => onReorderItem(index, 'down')} + disableReorderUp={index === 0} + disableReorderDown={Boolean(haveItems) && values.length === index + 1} + /> + ); + + switch (blockConfig.arrayType) { + case 'text': { + return ( + <div className={b('row')}> + <Text + className={b('row-field')} + value={String(value)} + onUpdate={(updateValue) => onUpdate(`[${index}]`, updateValue)} + onRefresh={(updatedValue) => onUpdate('', updatedValue)} + /> + {arrayItemButton} + </div> + ); + } + case 'object': { + if (!blockConfig.properties) { + return null; + } + return ( + <div key={index} className={b('card')}> + <div className={`${b('row')} ${b('card-head')}`}> + <div className={b('row-title')}>Item {index + 1}</div> + {arrayItemButton} + </div> + <DynamicForm + contentConfig={value as object} + blockConfig={blockConfig.properties} + onUpdateByKey={(key, updateValue) => + onUpdate(`[${index}].${key}`, updateValue) + } + /> + </div> + ); + } + default: { + return null; + } + } + }, + [blockConfig, haveItems, onDeleteItem, onReorderItem, onUpdate, values], + ); + + const renderInputs = React.useCallback(() => { + if (haveItems) { + const renderItems = values + .map(renderInput) + .filter(Boolean) as unknown as React.ReactNode[]; + return ( + <React.Fragment> + {renderItems} + <Button className={b('add-button')} onClick={onAddItem}> + <Icon data={Plus} /> + {blockConfig.buttonText} + </Button> + </React.Fragment> + ); + } else { + return ( + <div className={b('empty')}> + <Button className={b('add-button')} onClick={onAddItem}> + <Icon data={Plus} /> + Please, add new item + </Button> + </div> + ); + } + }, [blockConfig.buttonText, haveItems, onAddItem, renderInput, values]); + + return ( + <FieldBase + title={title} + className={b(null, className)} + onRefresh={(value) => onUpdate('', value)} + expandable + > + {renderInputs()} + </FieldBase> + ); +}; + +export default ArrayDynamicField; diff --git a/src/form-generator/components/Fields/Array/ItemButton/ItemButton.tsx b/src/form-generator/components/Fields/Array/ItemButton/ItemButton.tsx new file mode 100644 index 0000000000..4ba6e60dde --- /dev/null +++ b/src/form-generator/components/Fields/Array/ItemButton/ItemButton.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; + +import {ArrowDown, ArrowUp, EllipsisVertical, TrashBin} from '@gravity-ui/icons'; +import {Button, Icon, Menu, Popup} from '@gravity-ui/uikit'; + +import {formGeneratorCn} from '../../../../utils/cn'; + +const b = formGeneratorCn('array-item-button'); + +interface ItemButtonProps { + onRemove: () => void; + onReorderUp: () => void; + disableReorderUp?: boolean; + onReorderDown: () => void; + disableReorderDown?: boolean; + className?: string; +} + +const ItemButton = ({ + className, + onRemove, + onReorderUp, + onReorderDown, + disableReorderUp = false, + disableReorderDown = false, +}: ItemButtonProps) => { + const buttonRef = React.useRef(null); + const [isOpen, setIsOpen] = React.useState(false); + + const onMenuItemClickWrapper = React.useCallback((callback: () => void) => { + return () => { + setIsOpen(false); + callback(); + }; + }, []); + + return ( + <React.Fragment> + <Button className={b(null, className)} ref={buttonRef} onClick={() => setIsOpen(true)}> + <Icon data={EllipsisVertical} /> + </Button> + <Popup + placement={'bottom-end'} + anchorRef={buttonRef} + open={isOpen} + onOutsideClick={() => setIsOpen(false)} + > + <Menu> + <Menu.Item + theme={'danger'} + onClick={onMenuItemClickWrapper(onRemove)} + iconStart={<Icon data={TrashBin} />} + > + Remove + </Menu.Item> + <Menu.Item + disabled={disableReorderUp} + onClick={onMenuItemClickWrapper(onReorderUp)} + iconStart={<Icon data={ArrowUp} />} + > + Reorder Up + </Menu.Item> + <Menu.Item + disabled={disableReorderDown} + onClick={onMenuItemClickWrapper(onReorderDown)} + iconStart={<Icon data={ArrowDown} />} + > + Reorder Down + </Menu.Item> + </Menu> + </Popup> + </React.Fragment> + ); +}; + +export default ItemButton; diff --git a/src/form-generator/components/Fields/Boolean/Boolean.scss b/src/form-generator/components/Fields/Boolean/Boolean.scss new file mode 100644 index 0000000000..26d495fc2c --- /dev/null +++ b/src/form-generator/components/Fields/Boolean/Boolean.scss @@ -0,0 +1,10 @@ +@import '../../../styles/variables.scss'; +@import '../../../styles/mixins.scss'; + +$block: '.#{$ns-form-generator}boolean-dynamic-field'; + +#{$block} { + &__switch { + margin-top: 5px; + } +} diff --git a/src/form-generator/components/Fields/Boolean/Boolean.tsx b/src/form-generator/components/Fields/Boolean/Boolean.tsx new file mode 100644 index 0000000000..870d80fe65 --- /dev/null +++ b/src/form-generator/components/Fields/Boolean/Boolean.tsx @@ -0,0 +1,24 @@ +import {Switch} from '@gravity-ui/uikit'; + +import {formGeneratorCn} from '../../../utils/cn'; +import FieldBase, {FieldBaseParams} from '../../FieldBase/FieldBase'; + +import './Boolean.scss'; + +const b = formGeneratorCn('boolean-dynamic-field'); + +interface BooleanProps extends FieldBaseParams { + value: string; + onUpdate: (value: boolean | undefined) => void; + className?: string; +} + +const BooleanDynamicField = ({title, value, onUpdate, className}: BooleanProps) => { + return ( + <FieldBase title={title} className={b(null, className)} onRefresh={onUpdate}> + <Switch className={b('switch')} checked={Boolean(value)} onUpdate={onUpdate} /> + </FieldBase> + ); +}; + +export default BooleanDynamicField; diff --git a/src/form-generator/components/Fields/Number/Number.tsx b/src/form-generator/components/Fields/Number/Number.tsx new file mode 100644 index 0000000000..1f56843bdc --- /dev/null +++ b/src/form-generator/components/Fields/Number/Number.tsx @@ -0,0 +1,26 @@ +import {TextInput} from '@gravity-ui/uikit'; + +import {formGeneratorCn} from '../../../utils/cn'; +import FieldBase, {FieldBaseParams} from '../../FieldBase/FieldBase'; + +const b = formGeneratorCn('number-dynamic-field'); + +interface NumberDynamicFieldProps extends FieldBaseParams { + value: string; + onUpdate: (value: number | undefined) => void; + className?: string; +} + +const NumberDynamicField = ({title, value, onUpdate, className}: NumberDynamicFieldProps) => { + const onUpdateFunc = (updateValue: string) => { + onUpdate(Number(updateValue)); + }; + + return ( + <FieldBase title={title} className={b(null, className)} onRefresh={onUpdate}> + <TextInput value={value || ''} onUpdate={onUpdateFunc} /> + </FieldBase> + ); +}; + +export default NumberDynamicField; diff --git a/src/form-generator/components/Fields/Object/Object.tsx b/src/form-generator/components/Fields/Object/Object.tsx new file mode 100644 index 0000000000..1a56d43db6 --- /dev/null +++ b/src/form-generator/components/Fields/Object/Object.tsx @@ -0,0 +1,31 @@ +import DynamicForm from '../../../FormGenerator'; +import {ConfigInput, DynamicFormValue} from '../../../types'; +import FieldBase, {FieldBaseParams} from '../../FieldBase/FieldBase'; + +interface ObjectDynamicFieldProps extends FieldBaseParams { + value: object; + onUpdate: (key: string, value: DynamicFormValue) => void; + blockConfig: Array<ConfigInput>; + className?: string; +} + +const ObjectDynamicField = ({ + title, + value, + onUpdate, + className, + blockConfig, +}: ObjectDynamicFieldProps) => { + return ( + <FieldBase + title={title} + className={className} + onRefresh={(updatedValue) => onUpdate('', updatedValue)} + expandable + > + <DynamicForm contentConfig={value} blockConfig={blockConfig} onUpdateByKey={onUpdate} /> + </FieldBase> + ); +}; + +export default ObjectDynamicField; diff --git a/src/form-generator/components/Fields/OneOf/OneOf.scss b/src/form-generator/components/Fields/OneOf/OneOf.scss new file mode 100644 index 0000000000..086fe149bc --- /dev/null +++ b/src/form-generator/components/Fields/OneOf/OneOf.scss @@ -0,0 +1,14 @@ +@import '../../../styles/variables.scss'; +@import '../../../styles/mixins.scss'; + +$block: '.#{$ns-form-generator}oneof-dynamic-field'; + +#{$block} { + &__radio { + margin-bottom: 12px; + } + + &__select { + min-width: 80px; + } +} diff --git a/src/form-generator/components/Fields/OneOf/OneOf.tsx b/src/form-generator/components/Fields/OneOf/OneOf.tsx new file mode 100644 index 0000000000..ce39ed1137 --- /dev/null +++ b/src/form-generator/components/Fields/OneOf/OneOf.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; + +import {SegmentedRadioGroup, Select} from '@gravity-ui/uikit'; + +import DynamicForm from '../../../FormGenerator'; +import {DynamicFormValue, OneOfInput} from '../../../types'; +import {formGeneratorCn} from '../../../utils/cn'; +import FieldBase from '../../FieldBase/FieldBase'; + +import './OneOf.scss'; + +const b = formGeneratorCn('oneof-dynamic-field'); + +interface OneOfDynamicFieldProps { + contentConfig: DynamicFormValue; + onUpdate: (key: string, value: DynamicFormValue) => void; + inputConfig: OneOfInput; + className?: string; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getOneOfContentConfig = (contentConfig: any, name: string) => { + if (name) { + return contentConfig ? contentConfig[name] : {}; + } + return contentConfig; +}; + +const OneOfDynamicField = ({ + contentConfig, + onUpdate, + className, + inputConfig, +}: OneOfDynamicFieldProps) => { + const defaultValue = inputConfig.options[0].value; + + const [oneOfMetaValue, setOneOfMetaValue] = React.useState(defaultValue); + + const oneOfContentConfig = getOneOfContentConfig(contentConfig, inputConfig.name); + const oneOfChosenOption = React.useMemo( + () => + inputConfig.options.find( + ({value: foundOneOfValue}) => foundOneOfValue === oneOfMetaValue, + ), + [inputConfig.options, oneOfMetaValue], + ); + + const onUpdateOneOf = React.useCallback((value: string) => { + setOneOfMetaValue(value); + }, []); + + return ( + <FieldBase + title={inputConfig.title} + className={b(null, className)} + onRefresh={(value) => onUpdate('', value)} + expandable + > + {inputConfig.options.length < 4 ? ( + <SegmentedRadioGroup + className={b('radio')} + options={inputConfig.options.map((option) => ({ + content: option.title, + value: option.value, + }))} + value={oneOfMetaValue} + onUpdate={onUpdateOneOf} + /> + ) : ( + <Select + options={inputConfig.options.map((option) => ({ + content: option.title, + value: option.value, + }))} + value={oneOfMetaValue ? [oneOfMetaValue] : []} + onUpdate={([selectValue]) => onUpdateOneOf(selectValue)} + className={b('select')} + /> + )} + {oneOfChosenOption && ( + <DynamicForm + blockConfig={oneOfChosenOption.properties} + contentConfig={oneOfContentConfig} + onUpdateByKey={onUpdate} + /> + )} + </FieldBase> + ); +}; + +export default OneOfDynamicField; diff --git a/src/form-generator/components/Fields/Select/Select.scss b/src/form-generator/components/Fields/Select/Select.scss new file mode 100644 index 0000000000..c131b73458 --- /dev/null +++ b/src/form-generator/components/Fields/Select/Select.scss @@ -0,0 +1,10 @@ +@import '../../../styles/variables.scss'; +@import '../../../styles/mixins.scss'; + +$block: '.#{$ns-form-generator}editor-select-field'; + +#{$block} { + &__select { + min-width: 80px; + } +} diff --git a/src/form-generator/components/Fields/Select/Select.tsx b/src/form-generator/components/Fields/Select/Select.tsx new file mode 100644 index 0000000000..d3a57e752f --- /dev/null +++ b/src/form-generator/components/Fields/Select/Select.tsx @@ -0,0 +1,50 @@ +import {SegmentedRadioGroup, Select} from '@gravity-ui/uikit'; + +import {SelectMultipleInput, SelectSingleInput} from '../../../types'; +import {formGeneratorCn} from '../../../utils/cn'; +import FieldBase, {FieldBaseParams} from '../../FieldBase/FieldBase'; + +import './Select.scss'; + +const b = formGeneratorCn('editor-select-field'); + +type SelectInput = SelectSingleInput | SelectMultipleInput; + +interface SelectDynamicFieldProps extends FieldBaseParams { + input: SelectInput; + value: string | string[]; + onUpdate: (value: string | string[] | undefined) => void; + className?: string; +} + +const SelectDynamicField = ({input, value, onUpdate, className}: SelectDynamicFieldProps) => { + const inputView = input.view || 'radiobutton'; + const isMultiple = input.mode === 'multiple'; + const currentValue = Array.isArray(value) ? value : [value]; + + return ( + <FieldBase title={input.title} className={b(null, className)} onRefresh={onUpdate}> + {(inputView === 'select' || isMultiple) && ( + <Select + placeholder={'Value'} + value={value ? currentValue : []} + onUpdate={(selectValues) => + isMultiple ? onUpdate(selectValues) : onUpdate(selectValues[0]) + } + options={input.enum} + className={b('select')} + multiple={isMultiple} + /> + )} + {inputView === 'radiobutton' && !isMultiple && ( + <SegmentedRadioGroup + options={input.enum} + value={Array.isArray(value) ? value[0] : value} + onUpdate={onUpdate} + /> + )} + </FieldBase> + ); +}; + +export default SelectDynamicField; diff --git a/src/form-generator/components/Fields/Text/Text.tsx b/src/form-generator/components/Fields/Text/Text.tsx new file mode 100644 index 0000000000..c3c6cf5eb0 --- /dev/null +++ b/src/form-generator/components/Fields/Text/Text.tsx @@ -0,0 +1,22 @@ +import {TextInput} from '@gravity-ui/uikit'; + +import {formGeneratorCn} from '../../../utils/cn'; +import FieldBase, {FieldBaseParams} from '../../FieldBase/FieldBase'; + +const b = formGeneratorCn('text-dynamic-field'); + +interface TextDynamicFieldProps extends FieldBaseParams { + value: string; + onUpdate: (value: string | undefined) => void; + className?: string; +} + +const TextDynamicField = ({title, value, onUpdate, className}: TextDynamicFieldProps) => { + return ( + <FieldBase title={title} className={b(null, className)} onRefresh={onUpdate}> + <TextInput value={value || ''} onUpdate={onUpdate} /> + </FieldBase> + ); +}; + +export default TextDynamicField; diff --git a/src/form-generator/components/Fields/TextArea/TextArea.tsx b/src/form-generator/components/Fields/TextArea/TextArea.tsx new file mode 100644 index 0000000000..7676ed8a48 --- /dev/null +++ b/src/form-generator/components/Fields/TextArea/TextArea.tsx @@ -0,0 +1,22 @@ +import {TextArea} from '@gravity-ui/uikit'; + +import {formGeneratorCn} from '../../../utils/cn'; +import FieldBase, {FieldBaseParams} from '../../FieldBase/FieldBase'; + +const b = formGeneratorCn('textarea-dynamic-field'); + +interface TextAreaDynamicFieldProps extends FieldBaseParams { + value: string; + onUpdate: (value: string | undefined) => void; + className?: string; +} + +const TextAreaDynamicField = ({title, value, onUpdate, className}: TextAreaDynamicFieldProps) => { + return ( + <FieldBase title={title} className={b(null, className)} onRefresh={onUpdate}> + <TextArea minRows={5} maxRows={20} value={value || ''} onUpdate={onUpdate} /> + </FieldBase> + ); +}; + +export default TextAreaDynamicField; diff --git a/src/form-generator/index.ts b/src/form-generator/index.ts new file mode 100644 index 0000000000..77eb31cf87 --- /dev/null +++ b/src/form-generator/index.ts @@ -0,0 +1,2 @@ +export * from './FormGenerator'; +export * from './types'; diff --git a/src/form-generator/styles/mixins.scss b/src/form-generator/styles/mixins.scss new file mode 100644 index 0000000000..bb482d8647 --- /dev/null +++ b/src/form-generator/styles/mixins.scss @@ -0,0 +1 @@ +@import '../../../styles/mixins.scss'; diff --git a/src/form-generator/styles/variables.scss b/src/form-generator/styles/variables.scss new file mode 100644 index 0000000000..9a94ae3044 --- /dev/null +++ b/src/form-generator/styles/variables.scss @@ -0,0 +1 @@ +$ns-form-generator: 'pcformgenerator-'; diff --git a/src/form-generator/types.ts b/src/form-generator/types.ts new file mode 100644 index 0000000000..68a2d81cc7 --- /dev/null +++ b/src/form-generator/types.ts @@ -0,0 +1,133 @@ +import {IconProps} from '@gravity-ui/uikit'; + +import {PageContent} from '../models'; + +export type DynamicFormValue = string | number | [] | object | boolean | PageContent | undefined; + +export interface BlockConfig { + name: string; + inputs?: Array<ConfigInput>; + group?: string; + hidden?: boolean; + default?: object; + previewImg?: string; + previewIcon?: IconProps; +} + +export interface TextInput { + type: 'text'; + name: string; + title: string; +} + +export interface BooleanInput { + type: 'boolean'; + name: string; + title: string; +} + +export interface NumberInput { + type: 'number'; + name: string; + title: string; +} + +export interface TextAreaInput { + type: 'textarea'; + name: string; + title: string; +} + +export interface SelectBaseInput { + type: 'select'; + name: string; + title: string; + view: 'select' | 'radiobutton'; + mode: 'single' | 'multiple'; + enum: Array<{content: string; value: string}>; +} + +export interface SelectSingleInput extends SelectBaseInput { + type: 'select'; + name: string; + title: string; + view: 'select' | 'radiobutton'; + mode: 'single'; + enum: Array<{content: string; value: string}>; +} + +export interface SelectMultipleInput extends SelectBaseInput { + type: 'select'; + name: string; + title: string; + view: 'select'; + mode: 'multiple'; + enum: Array<{content: string; value: string}>; +} + +export interface ObjectInput { + type: 'object'; + name: string; + title: string; + properties: Array<ConfigInput>; +} + +export interface ArrayBaseInput { + type: 'array'; + arrayType: 'object' | 'text'; + name: string; + title: string; + buttonText: string; +} + +export interface ArrayTextInput extends ArrayBaseInput { + arrayType: 'text'; +} + +export interface ArrayObjectInput extends ArrayBaseInput { + arrayType: 'object'; + properties: Array<ConfigInput>; +} + +export interface OneOfInput { + type: 'oneOf'; + name: string; + key?: string; + title: string; + options: { + value: string; + title: string; + properties: Array<ConfigInput>; + }[]; +} + +export interface AnyOfInput { + type: 'anyOf'; + name: string; + key?: string; + title: string; + options: { + value: string; + title: string; + properties: Array<ConfigInput>; + }[]; +} + +export interface GeneralProps { + showIf?: string; +} + +export type ConfigInput = ( + | TextInput + | BooleanInput + | NumberInput + | TextAreaInput + | SelectSingleInput + | SelectMultipleInput + | ObjectInput + | ArrayTextInput + | ArrayObjectInput + | OneOfInput + | AnyOfInput +) & + GeneralProps; diff --git a/src/form-generator/utils/cn.ts b/src/form-generator/utils/cn.ts new file mode 100644 index 0000000000..f8ff60f153 --- /dev/null +++ b/src/form-generator/utils/cn.ts @@ -0,0 +1,5 @@ +import {withNaming} from '@bem-react/classname'; + +export const FORM_GENERATOR_NAMESPACE = 'pcformgenerator-'; + +export const formGeneratorCn = withNaming({n: FORM_GENERATOR_NAMESPACE, e: '__', m: '_'}); diff --git a/src/form-generator/utils/common.ts b/src/form-generator/utils/common.ts new file mode 100644 index 0000000000..ac4a098bb4 --- /dev/null +++ b/src/form-generator/utils/common.ts @@ -0,0 +1,23 @@ +import _ from 'lodash'; + +import {DynamicFormValue} from '../types'; + +export const getFullPath = (path: string, name: string) => { + if (!path && !name) { + return ''; + } + + if (!path) { + return name; + } + + if (!name) { + return path; + } + + return path + '.' + name; +}; + +export const getContent = (contentConfig: DynamicFormValue, path: string) => { + return path ? _.get(contentConfig, path) : contentConfig; +}; diff --git a/src/Indents/__stories__/Indents.mdx b/src/gravity-blocks/Indents/__stories__/Indents.mdx similarity index 100% rename from src/Indents/__stories__/Indents.mdx rename to src/gravity-blocks/Indents/__stories__/Indents.mdx diff --git a/src/Indents/__stories__/Indents.stories.scss b/src/gravity-blocks/Indents/__stories__/Indents.stories.scss similarity index 95% rename from src/Indents/__stories__/Indents.stories.scss rename to src/gravity-blocks/Indents/__stories__/Indents.stories.scss index 784556ff8a..1936d7cc3b 100644 --- a/src/Indents/__stories__/Indents.stories.scss +++ b/src/gravity-blocks/Indents/__stories__/Indents.stories.scss @@ -1,5 +1,5 @@ -@import '../../../styles/variables.scss'; -@import '../../../styles/mixins'; +@import '../../../../styles/variables.scss'; +@import '../../../../styles/mixins'; @mixin pseudo($content) { background: repeating-linear-gradient( diff --git a/src/Indents/__stories__/Indents.stories.tsx b/src/gravity-blocks/Indents/__stories__/Indents.stories.tsx similarity index 90% rename from src/Indents/__stories__/Indents.stories.tsx rename to src/gravity-blocks/Indents/__stories__/Indents.stories.tsx index ea4932cce5..d98c57fbb0 100644 --- a/src/Indents/__stories__/Indents.stories.tsx +++ b/src/gravity-blocks/Indents/__stories__/Indents.stories.tsx @@ -1,9 +1,9 @@ import {Meta, StoryFn} from '@storybook/react'; -import {CardLayoutBlock} from '../../blocks'; -import {PageConstructor} from '../../containers/PageConstructor'; -import {CardLayoutBlockModel, CardLayoutBlockProps} from '../../models'; -import {block} from '../../utils'; +import {default as CardLayoutBlock} from '../../../blocks/CardLayout/CardLayout'; +import {PageConstructor} from '../../../containers/PageConstructor'; +import {CardLayoutBlockModel, CardLayoutBlockProps} from '../../../models'; +import {block} from '../../../utils'; import data from './data.json'; diff --git a/src/Indents/__stories__/data.json b/src/gravity-blocks/Indents/__stories__/data.json similarity index 100% rename from src/Indents/__stories__/data.json rename to src/gravity-blocks/Indents/__stories__/data.json diff --git a/src/gravity-blocks/context/GravityBlocksProvider.tsx b/src/gravity-blocks/context/GravityBlocksProvider.tsx new file mode 100644 index 0000000000..5a61f13700 --- /dev/null +++ b/src/gravity-blocks/context/GravityBlocksProvider.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; + +import {ThemeProvider} from '@gravity-ui/uikit'; + +import {DEFAULT_THEME} from '../../components/constants'; +import {Theme} from '../../models'; + +import {AnalyticsContext, AnalyticsContextProps} from './analyticsContext'; +import {AnimateContext} from './animateContext'; +import { + DEFAULT_FORMS_CONTEXT_VALUE, + FormsContext, + FormsContextProps, +} from './formsContext/FormsContext'; +import {ImageContext, ImageContextProps} from './imageContext'; +import {LocaleContext, LocaleContextProps} from './localeContext'; +import {LocationContext, LocationContextProps} from './locationContext'; +import {MapsContext, MapsContextType, initialMapValue} from './mapsContext/mapsContext'; +import {MicrodataContext, MicrodataContextProps} from './microdataContext'; +import {MobileContext} from './mobileContext'; +import {ProjectSettingsContext, ProjectSettingsContextProps} from './projectSettingsContext'; +import {SSRContext, SSRContextProps} from './ssrContext'; +import {ThemeContext} from './theme'; +import {WindowWidthProvider} from './windowWidthContext'; + +export interface GravityBlocksProviderProps { + isMobile?: boolean; + locale?: LocaleContextProps; + location?: LocationContextProps; + ssrConfig?: SSRContextProps; + theme?: Theme; + mapsContext?: MapsContextType; + projectSettings?: ProjectSettingsContextProps; + analytics?: AnalyticsContextProps; + forms?: FormsContextProps; + image?: ImageContextProps; + animated?: boolean; + microdata?: MicrodataContextProps; +} + +export const GravityBlocksProvider: React.FC< + React.PropsWithChildren<GravityBlocksProviderProps> +> = ({children, ...props}) => { + const { + isMobile, + mapsContext = initialMapValue, + locale = {}, + location = {}, + analytics = {}, + ssrConfig = {}, + projectSettings = {}, + theme = DEFAULT_THEME, + image = {}, + forms = DEFAULT_FORMS_CONTEXT_VALUE, + animated = true, + microdata = {}, + } = props; + + /* eslint-disable react/jsx-key */ + const context = [ + <ThemeProvider theme={theme} />, + <AnimateContext.Provider value={{animated}} />, + <ThemeContext.Provider value={{theme}} />, + <ProjectSettingsContext.Provider value={projectSettings} />, + <LocaleContext.Provider value={locale} />, + <ImageContext.Provider value={image} />, + <LocationContext.Provider value={location} />, + <MobileContext.Provider value={Boolean(isMobile)} />, + <MapsContext.Provider value={mapsContext} />, + <AnalyticsContext.Provider value={analytics} />, + <FormsContext.Provider value={forms} />, + <SSRContext.Provider value={{isServer: ssrConfig?.isServer}} />, + <MicrodataContext.Provider value={microdata} />, + <WindowWidthProvider />, + ].reduceRight((prev, provider) => React.cloneElement(provider, {}, prev), children); + /* eslint-enable react/jsx-key */ + + return <React.Fragment>{context}</React.Fragment>; +}; diff --git a/src/context/analyticsContext/analyticsContext.tsx b/src/gravity-blocks/context/analyticsContext/analyticsContext.tsx similarity index 83% rename from src/context/analyticsContext/analyticsContext.tsx rename to src/gravity-blocks/context/analyticsContext/analyticsContext.tsx index 1d1838e740..851a906778 100644 --- a/src/context/analyticsContext/analyticsContext.tsx +++ b/src/gravity-blocks/context/analyticsContext/analyticsContext.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {AnalyticsEvent} from '../../models'; +import {AnalyticsEvent} from '../../../models'; export interface AnalyticsContextProps { sendEvents?: (events: AnalyticsEvent[]) => void; diff --git a/src/context/analyticsContext/index.ts b/src/gravity-blocks/context/analyticsContext/index.ts similarity index 100% rename from src/context/analyticsContext/index.ts rename to src/gravity-blocks/context/analyticsContext/index.ts diff --git a/src/context/animateContext/AnimateContext.tsx b/src/gravity-blocks/context/animateContext/AnimateContext.tsx similarity index 100% rename from src/context/animateContext/AnimateContext.tsx rename to src/gravity-blocks/context/animateContext/AnimateContext.tsx diff --git a/src/context/animateContext/index.ts b/src/gravity-blocks/context/animateContext/index.ts similarity index 100% rename from src/context/animateContext/index.ts rename to src/gravity-blocks/context/animateContext/index.ts diff --git a/src/context/formsContext/FormsContext.ts b/src/gravity-blocks/context/formsContext/FormsContext.ts similarity index 77% rename from src/context/formsContext/FormsContext.ts rename to src/gravity-blocks/context/formsContext/FormsContext.ts index 1b71d016f2..7c1cfc2a12 100644 --- a/src/context/formsContext/FormsContext.ts +++ b/src/gravity-blocks/context/formsContext/FormsContext.ts @@ -1,7 +1,7 @@ import * as React from 'react'; -import {YandexFormProps} from '../../models/constructor-items/common'; -import {HubspotFormProps} from '../../models/constructor-items/sub-blocks'; +import {YandexFormProps} from '../../../models/constructor-items/common'; +import {HubspotFormProps} from '../../../models/constructor-items/sub-blocks'; export const DEFAULT_FORMS_CONTEXT_VALUE: FormsContextProps = {}; diff --git a/src/context/formsContext/index.ts b/src/gravity-blocks/context/formsContext/index.ts similarity index 100% rename from src/context/formsContext/index.ts rename to src/gravity-blocks/context/formsContext/index.ts diff --git a/src/context/imageContext/imageContext.ts b/src/gravity-blocks/context/imageContext/imageContext.ts similarity index 84% rename from src/context/imageContext/imageContext.ts rename to src/gravity-blocks/context/imageContext/imageContext.ts index 8a6240a3a9..bc58b0fc11 100644 --- a/src/context/imageContext/imageContext.ts +++ b/src/gravity-blocks/context/imageContext/imageContext.ts @@ -1,6 +1,6 @@ import * as React from 'react'; -import type {ImageBaseProps} from '../../components'; +import type {ImageBaseProps} from '../../../components'; export type ImageContextProps = { Image?: diff --git a/src/context/imageContext/index.ts b/src/gravity-blocks/context/imageContext/index.ts similarity index 100% rename from src/context/imageContext/index.ts rename to src/gravity-blocks/context/imageContext/index.ts diff --git a/src/context/localeContext/index.ts b/src/gravity-blocks/context/localeContext/index.ts similarity index 100% rename from src/context/localeContext/index.ts rename to src/gravity-blocks/context/localeContext/index.ts diff --git a/src/context/localeContext/localeContext.ts b/src/gravity-blocks/context/localeContext/localeContext.ts similarity index 100% rename from src/context/localeContext/localeContext.ts rename to src/gravity-blocks/context/localeContext/localeContext.ts diff --git a/src/context/locationContext/index.ts b/src/gravity-blocks/context/locationContext/index.ts similarity index 100% rename from src/context/locationContext/index.ts rename to src/gravity-blocks/context/locationContext/index.ts diff --git a/src/context/locationContext/locationContext.ts b/src/gravity-blocks/context/locationContext/locationContext.ts similarity index 89% rename from src/context/locationContext/locationContext.ts rename to src/gravity-blocks/context/locationContext/locationContext.ts index 9b40725ce3..56c9df1982 100644 --- a/src/context/locationContext/locationContext.ts +++ b/src/gravity-blocks/context/locationContext/locationContext.ts @@ -1,6 +1,6 @@ import * as React from 'react'; -import {RouterLinkProps} from '../../components/RouterLink/RouterLink'; +import {RouterLinkProps} from '../../../components/RouterLink/RouterLink'; export interface History { action: 'PUSH' | 'POP' | 'REPLACE' | ''; diff --git a/src/context/mapsContext/mapsContext.ts b/src/gravity-blocks/context/mapsContext/mapsContext.ts similarity index 100% rename from src/context/mapsContext/mapsContext.ts rename to src/gravity-blocks/context/mapsContext/mapsContext.ts diff --git a/src/context/mapsContext/mapsProvider.tsx b/src/gravity-blocks/context/mapsContext/mapsProvider.tsx similarity index 100% rename from src/context/mapsContext/mapsProvider.tsx rename to src/gravity-blocks/context/mapsContext/mapsProvider.tsx diff --git a/src/context/mapsContext/useMap.ts b/src/gravity-blocks/context/mapsContext/useMap.ts similarity index 100% rename from src/context/mapsContext/useMap.ts rename to src/gravity-blocks/context/mapsContext/useMap.ts diff --git a/src/gravity-blocks/context/microdataContext/MicrodataContext.ts b/src/gravity-blocks/context/microdataContext/MicrodataContext.ts new file mode 100644 index 0000000000..1b43ac19be --- /dev/null +++ b/src/gravity-blocks/context/microdataContext/MicrodataContext.ts @@ -0,0 +1,7 @@ +import * as React from 'react'; + +export interface MicrodataContextProps { + contentUpdatedDate?: string; +} + +export const MicrodataContext = React.createContext<MicrodataContextProps>({}); diff --git a/src/gravity-blocks/context/microdataContext/index.ts b/src/gravity-blocks/context/microdataContext/index.ts new file mode 100644 index 0000000000..8165d84a09 --- /dev/null +++ b/src/gravity-blocks/context/microdataContext/index.ts @@ -0,0 +1,2 @@ +export * from './MicrodataContext'; +export * from './useMicrodata'; diff --git a/src/gravity-blocks/context/microdataContext/useMicrodata.ts b/src/gravity-blocks/context/microdataContext/useMicrodata.ts new file mode 100644 index 0000000000..4266ee52f0 --- /dev/null +++ b/src/gravity-blocks/context/microdataContext/useMicrodata.ts @@ -0,0 +1,7 @@ +import * as React from 'react'; + +import {MicrodataContext, MicrodataContextProps} from './MicrodataContext'; + +export function useMicrodata(): MicrodataContextProps { + return React.useContext(MicrodataContext); +} diff --git a/src/context/mobileContext/MobileContext.ts b/src/gravity-blocks/context/mobileContext/MobileContext.ts similarity index 100% rename from src/context/mobileContext/MobileContext.ts rename to src/gravity-blocks/context/mobileContext/MobileContext.ts diff --git a/src/context/mobileContext/index.ts b/src/gravity-blocks/context/mobileContext/index.ts similarity index 100% rename from src/context/mobileContext/index.ts rename to src/gravity-blocks/context/mobileContext/index.ts diff --git a/src/context/projectSettingsContext/ProjectSettingsContext.ts b/src/gravity-blocks/context/projectSettingsContext/ProjectSettingsContext.ts similarity index 100% rename from src/context/projectSettingsContext/ProjectSettingsContext.ts rename to src/gravity-blocks/context/projectSettingsContext/ProjectSettingsContext.ts diff --git a/src/context/projectSettingsContext/index.ts b/src/gravity-blocks/context/projectSettingsContext/index.ts similarity index 100% rename from src/context/projectSettingsContext/index.ts rename to src/gravity-blocks/context/projectSettingsContext/index.ts diff --git a/src/context/ssrContext/SSRContext.ts b/src/gravity-blocks/context/ssrContext/SSRContext.ts similarity index 100% rename from src/context/ssrContext/SSRContext.ts rename to src/gravity-blocks/context/ssrContext/SSRContext.ts diff --git a/src/context/ssrContext/index.ts b/src/gravity-blocks/context/ssrContext/index.ts similarity index 100% rename from src/context/ssrContext/index.ts rename to src/gravity-blocks/context/ssrContext/index.ts diff --git a/src/context/stylesContext/StylesContext.ts b/src/gravity-blocks/context/stylesContext/StylesContext.ts similarity index 100% rename from src/context/stylesContext/StylesContext.ts rename to src/gravity-blocks/context/stylesContext/StylesContext.ts diff --git a/src/context/stylesContext/index.ts b/src/gravity-blocks/context/stylesContext/index.ts similarity index 100% rename from src/context/stylesContext/index.ts rename to src/gravity-blocks/context/stylesContext/index.ts diff --git a/src/context/theme/ThemeContext.ts b/src/gravity-blocks/context/theme/ThemeContext.ts similarity index 70% rename from src/context/theme/ThemeContext.ts rename to src/gravity-blocks/context/theme/ThemeContext.ts index 52d2ae1964..ffe54934a3 100644 --- a/src/context/theme/ThemeContext.ts +++ b/src/gravity-blocks/context/theme/ThemeContext.ts @@ -1,7 +1,7 @@ import * as React from 'react'; -import {DEFAULT_THEME} from '../../components/constants'; -import {Theme} from '../../models'; +import {DEFAULT_THEME} from '../../../components/constants'; +import {Theme} from '../../../models'; export interface ThemeContextProps { theme: Theme; diff --git a/src/context/theme/index.ts b/src/gravity-blocks/context/theme/index.ts similarity index 100% rename from src/context/theme/index.ts rename to src/gravity-blocks/context/theme/index.ts diff --git a/src/context/theme/useTheme.ts b/src/gravity-blocks/context/theme/useTheme.ts similarity index 100% rename from src/context/theme/useTheme.ts rename to src/gravity-blocks/context/theme/useTheme.ts diff --git a/src/context/theme/withTheme.tsx b/src/gravity-blocks/context/theme/withTheme.tsx similarity index 100% rename from src/context/theme/withTheme.tsx rename to src/gravity-blocks/context/theme/withTheme.tsx diff --git a/src/context/videoContext/VideoContext.ts b/src/gravity-blocks/context/videoContext/VideoContext.ts similarity index 100% rename from src/context/videoContext/VideoContext.ts rename to src/gravity-blocks/context/videoContext/VideoContext.ts diff --git a/src/context/videoContext/index.ts b/src/gravity-blocks/context/videoContext/index.ts similarity index 100% rename from src/context/videoContext/index.ts rename to src/gravity-blocks/context/videoContext/index.ts diff --git a/src/context/windowWidthContext/WindowWidthContext.tsx b/src/gravity-blocks/context/windowWidthContext/WindowWidthContext.tsx similarity index 96% rename from src/context/windowWidthContext/WindowWidthContext.tsx rename to src/gravity-blocks/context/windowWidthContext/WindowWidthContext.tsx index 388b76477e..a3581dfa89 100644 --- a/src/context/windowWidthContext/WindowWidthContext.tsx +++ b/src/gravity-blocks/context/windowWidthContext/WindowWidthContext.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import throttle from 'lodash/throttle'; -import {BREAKPOINTS} from '../../constants'; +import {BREAKPOINTS} from '../../../constants'; import {MobileContext} from '../mobileContext'; const DEFAULT_DESKTOP_WIDTH = BREAKPOINTS.xl; diff --git a/src/context/windowWidthContext/index.ts b/src/gravity-blocks/context/windowWidthContext/index.ts similarity index 100% rename from src/context/windowWidthContext/index.ts rename to src/gravity-blocks/context/windowWidthContext/index.ts diff --git a/src/gravity-blocks/extensions/BackgroundExtension.tsx b/src/gravity-blocks/extensions/BackgroundExtension.tsx new file mode 100644 index 0000000000..dff4b8f700 --- /dev/null +++ b/src/gravity-blocks/extensions/BackgroundExtension.tsx @@ -0,0 +1,166 @@ +import * as React from 'react'; + +import type {PageConstructorWrapperProps} from '../../common/types'; +import BackgroundMedia from '../../components/BackgroundMedia/BackgroundMedia'; +import type {PageConstructorExtension} from '../../containers/PageConstructor/PageConstructor'; +import {MediaProps} from '../../models'; +import {block} from '../../utils'; +import {useContent} from '../hooks'; + +const b = block('page-constructor'); + +export interface BackgroundExtensionWrapperProps {} + +export interface BackgroundExtensionGlobalConfig { + background?: MediaProps; +} + +export interface BackgroundPageContent extends BackgroundExtensionGlobalConfig {} + +export const BackgroundExtensionContentWrapper: React.FC< + BackgroundExtensionWrapperProps & PageConstructorWrapperProps +> = ({children}) => { + const {content} = useContent<BackgroundPageContent>(); + const {background} = content; + + return ( + <React.Fragment> + {background && <BackgroundMedia {...background} className={b('background')} />} + {children} + </React.Fragment> + ); +}; + +export const backgroundExtension = ({ + wrapperProps = {}, + globalDefaults = {}, +}: { + wrapperProps?: BackgroundExtensionWrapperProps; + globalDefaults?: BackgroundExtensionGlobalConfig; +}): PageConstructorExtension<BackgroundExtensionGlobalConfig, BackgroundExtensionWrapperProps> => { + return { + name: 'Background Extension', + id: '@gravity-ui/page-constructor/background-extension', + settings: { + ContentWrapper: BackgroundExtensionContentWrapper, + contentWrapperProps: wrapperProps, + globalInputs: [ + { + type: 'section', + title: 'Background', + opened: false, + fields: [ + { + type: 'segmentedRadioGroup', + title: 'Media type', + name: 'background._mediaType', + options: [ + {content: 'Image', value: 'image'}, + {content: 'Video', value: 'video'}, + {content: 'YouTube', value: 'youtube'}, + {content: 'Iframe', value: 'iframe'}, + ], + }, + { + type: 'text', + text: 'Light theme', + when: [ + {field: 'background._mediaType', operator: '!==', value: undefined}, + ], + }, + { + type: 'textInput', + title: 'Desktop image URL', + name: 'background.image.light.desktop', + when: [ + {field: 'background._mediaType', operator: '===', value: 'image'}, + ], + }, + { + type: 'textInput', + title: 'Tablet image URL', + name: 'background.image.light.tablet', + when: [ + {field: 'background._mediaType', operator: '===', value: 'image'}, + ], + }, + { + type: 'textInput', + title: 'Mobile image URL', + name: 'background.image.light.mobile', + when: [ + {field: 'background._mediaType', operator: '===', value: 'image'}, + ], + }, + { + type: 'textInput', + title: 'Video URL', + name: 'background.video.src', + when: [ + {field: 'background._mediaType', operator: '===', value: 'video'}, + ], + }, + { + type: 'textInput', + title: 'YouTube URL', + name: 'background.youtube', + when: [ + {field: 'background._mediaType', operator: '===', value: 'youtube'}, + ], + }, + { + type: 'textInput', + title: 'Iframe URL', + name: 'background.iframe.src', + when: [ + {field: 'background._mediaType', operator: '===', value: 'iframe'}, + ], + }, + { + type: 'text', + text: 'Dark theme', + when: [ + {field: 'background._mediaType', operator: '!==', value: undefined}, + ], + }, + { + type: 'textInput', + title: 'Desktop image URL', + name: 'background.image.dark.desktop', + when: [ + {field: 'background._mediaType', operator: '===', value: 'image'}, + ], + }, + { + type: 'textInput', + title: 'Tablet image URL', + name: 'background.image.dark.tablet', + when: [ + {field: 'background._mediaType', operator: '===', value: 'image'}, + ], + }, + { + type: 'textInput', + title: 'Mobile image URL', + name: 'background.image.dark.mobile', + when: [ + {field: 'background._mediaType', operator: '===', value: 'image'}, + ], + }, + { + type: 'colorInput', + title: 'Color overlay', + name: 'background.color', + }, + { + type: 'switch', + title: 'Parallax effect', + name: 'background.parallax', + }, + ], + }, + ], + globalDefaults, + }, + }; +}; diff --git a/src/gravity-blocks/extensions/BlockBaseExtension.tsx b/src/gravity-blocks/extensions/BlockBaseExtension.tsx new file mode 100644 index 0000000000..4861d44a9a --- /dev/null +++ b/src/gravity-blocks/extensions/BlockBaseExtension.tsx @@ -0,0 +1,98 @@ +import * as React from 'react'; + +import BlockBase from '../../components/BlockBase/BlockBase'; +import type {PageConstructorExtension} from '../../containers/PageConstructor/PageConstructor'; +import type {BlockBaseProps, BlockWrapperDataProps} from '../../models'; +import {IndentValue} from '../grid'; + +export const BlockBaseExtensionBlockWrapper: React.FC< + BlockWrapperDataProps<BlockBaseProps> & React.PropsWithChildren +> = ({props, content, children, index}) => { + let defaultIndent; + + if (index === 0) { + defaultIndent = {top: '0' as IndentValue, bottom: 'l' as IndentValue}; + } + + return ( + <BlockBase + anchor={content?.anchor ?? props?.anchor} + indent={content?.indent ?? props?.indent ?? defaultIndent} + visible={content?.visible ?? props?.visible} + resetPaddings={content?.resetPaddings ?? props?.resetPaddings} + qa={content?.qa ?? props?.qa} + > + {children} + </BlockBase> + ); +}; + +export const blockBaseExtension = (): PageConstructorExtension<{}, {}, BlockBaseProps> => ({ + name: 'Block Base', + id: '@gravity-ui/page-constructor/block-base', + settings: { + blockWrapper: BlockBaseExtensionBlockWrapper, + blockInputs: [ + { + type: 'section', + title: 'Block Base', + fields: [ + { + type: 'textInput', + title: 'Anchor', + name: 'anchor.url', + placeholder: '#', + }, + { + type: 'textInput', + title: 'Anchor text', + name: 'anchor.text', + placeholder: 'Text', + }, + { + type: 'select', + title: 'Top indent', + name: 'indent.top', + hasClear: true, + options: [ + {value: '0'}, + {value: 'xs', content: 'XS'}, + {value: 's', content: 'S'}, + {value: 'm', content: 'M'}, + {value: 'l', content: 'L'}, + {value: 'xl', content: 'XL'}, + ], + defaultValue: 'l', + }, + { + type: 'select', + title: 'Bottom indent', + name: 'indent.bottom', + hasClear: true, + options: [ + {value: '0'}, + {value: 'xs', content: 'XS'}, + {value: 's', content: 'S'}, + {value: 'm', content: 'M'}, + {value: 'l', content: 'L'}, + {value: 'xl', content: 'XL'}, + ], + defaultValue: 'l', + }, + { + type: 'select', + title: 'Hide on breakpoint', + name: 'visible', + hasClear: true, + options: [ + {value: 'sm', content: 'SM only'}, + {value: 'md', content: 'MD and down'}, + {value: 'lg', content: 'LG and down'}, + {value: 'xl', content: 'XL and down'}, + ], + }, + ], + }, + ], + }, +}); diff --git a/src/gravity-blocks/extensions/GeneralExtension.tsx b/src/gravity-blocks/extensions/GeneralExtension.tsx new file mode 100644 index 0000000000..615f00eff7 --- /dev/null +++ b/src/gravity-blocks/extensions/GeneralExtension.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; + +import type {PageConstructorWrapperProps} from '../../common/types'; +import BrandFooter from '../../components/BrandFooter/BrandFooter'; +import type {PageConstructorExtension} from '../../containers/PageConstructor/PageConstructor'; +import {block} from '../../utils'; +import {GravityBlocksProvider, GravityBlocksProviderProps} from '../context/GravityBlocksProvider'; +import {MicrodataContextProps} from '../context/microdataContext'; +import {useContent} from '../hooks'; + +const b = block('page-constructor'); + +export interface GeneralExtensionGlobalConfig extends GravityBlocksProviderProps { + isBranded?: boolean; +} + +export interface GeneralExtensionWrapperProps extends GeneralExtensionGlobalConfig { + microdata?: MicrodataContextProps; +} + +export interface GeneralPageContent extends GeneralExtensionGlobalConfig {} + +export const GeneralExtensionContentWrapper: React.FC< + GeneralExtensionWrapperProps & PageConstructorWrapperProps +> = (props) => { + const {children, isBranded: isBrandedProp, ...rest} = props; + const {content} = useContent<GeneralPageContent>(); + const {isBranded = isBrandedProp, animated = rest.animated} = content; + + return ( + <div className={b('wrapper')}> + <GravityBlocksProvider animated={animated} {...rest}> + {children} + {isBranded && <BrandFooter />} + </GravityBlocksProvider> + </div> + ); +}; + +export const generalExtension = ({ + wrapperProps = {}, + globalDefaults = {}, +}: { + wrapperProps?: GeneralExtensionWrapperProps; + globalDefaults?: GeneralExtensionGlobalConfig; +}): PageConstructorExtension<GeneralExtensionGlobalConfig, GeneralExtensionWrapperProps> => { + return { + name: 'General Extension', + id: '@gravity-ui/page-constructor/general-extension', + settings: { + ContentWrapper: GeneralExtensionContentWrapper, + contentWrapperProps: wrapperProps, + globalInputs: [ + { + type: 'switch', + title: 'Is branded', + name: 'isBranded', + }, + { + type: 'switch', + title: 'Animated', + name: 'animated', + }, + ], + globalDefaults, + }, + }; +}; diff --git a/src/gravity-blocks/extensions/GravityBlocksExtension.tsx b/src/gravity-blocks/extensions/GravityBlocksExtension.tsx new file mode 100644 index 0000000000..e87696fdef --- /dev/null +++ b/src/gravity-blocks/extensions/GravityBlocksExtension.tsx @@ -0,0 +1,49 @@ +import type {PageConstructorExtension} from '../../containers/PageConstructor/PageConstructor'; +import {MediaProps, NavigationData, PageContent} from '../../models'; + +export { + GravityBlocksProvider, + type GravityBlocksProviderProps, +} from '../context/GravityBlocksProvider'; + +import {GeneralExtensionGlobalConfig, GeneralExtensionWrapperProps} from './GeneralExtension'; + +import {backgroundExtension, blockBaseExtension, generalExtension, navigationExtension} from '.'; + +export interface GravityBlocksWrapperProps extends GeneralExtensionWrapperProps { + renderMenu?: () => React.ReactNode; +} + +export interface GravityBlocksGlobalConfig extends GeneralExtensionGlobalConfig { + background?: MediaProps; + navigation?: NavigationData; +} + +export interface GravityPageContent extends PageContent, GravityBlocksGlobalConfig {} + +/** @returns Array of PageConstructorExtension instances for gravity-blocks. */ +export const gravityBlocksExtension = ({ + wrapperProps = {}, + globalDefaults = {}, +}: { + wrapperProps?: GravityBlocksWrapperProps; + globalDefaults?: GravityBlocksGlobalConfig; +}): PageConstructorExtension<GravityBlocksGlobalConfig, GravityBlocksWrapperProps>[] => { + const {background, navigation, ...generalGlobalDefaults} = globalDefaults; + const {renderMenu, ...generalWrapperProps} = wrapperProps; + + return [ + generalExtension({ + wrapperProps: generalWrapperProps, + globalDefaults: generalGlobalDefaults, + }), + backgroundExtension({ + globalDefaults: {background}, + }), + navigationExtension({ + wrapperProps: {renderMenu}, + globalDefaults: {navigation}, + }), + blockBaseExtension(), + ]; +}; diff --git a/src/gravity-blocks/extensions/NavigationExtension.tsx b/src/gravity-blocks/extensions/NavigationExtension.tsx new file mode 100644 index 0000000000..c356f978d2 --- /dev/null +++ b/src/gravity-blocks/extensions/NavigationExtension.tsx @@ -0,0 +1,327 @@ +import * as React from 'react'; + +import type {PageConstructorWrapperProps} from '../../common/types'; +import type {PageConstructorExtension} from '../../containers/PageConstructor/PageConstructor'; +import {NavigationData} from '../../models'; +import {useContent} from '../hooks'; +import Layout from '../navigation/containers/Layout/Layout'; + +export interface NavigationExtensionWrapperProps { + renderMenu?: () => React.ReactNode; +} + +export interface NavigationExtensionGlobalConfig { + navigation?: NavigationData; +} + +export interface NavigationPageContent extends NavigationExtensionGlobalConfig {} + +export const NavigationExtensionContentWrapper: React.FC< + NavigationExtensionWrapperProps & PageConstructorWrapperProps +> = ({children, renderMenu}) => { + const {content} = useContent<NavigationPageContent>(); + const {navigation} = content; + + return ( + <Layout navigation={navigation}> + {renderMenu?.()} + {children} + </Layout> + ); +}; + +export const navigationExtension = ({ + wrapperProps = {}, + globalDefaults = {}, +}: { + wrapperProps?: NavigationExtensionWrapperProps; + globalDefaults?: NavigationExtensionGlobalConfig; +}): PageConstructorExtension<NavigationExtensionGlobalConfig, NavigationExtensionWrapperProps> => { + return { + name: 'Navigation Extension', + id: '@gravity-ui/page-constructor/navigation-extension', + settings: { + ContentWrapper: NavigationExtensionContentWrapper, + contentWrapperProps: wrapperProps, + globalInputs: [ + { + type: 'section', + title: 'Navigation', + opened: false, + fields: [ + { + type: 'text', + text: 'Logo', + }, + { + type: 'textInput', + title: 'Light theme desktop logo URL', + name: 'navigation.logo.light.icon.default', + }, + { + type: 'textInput', + title: 'Light theme mobile logo URL', + name: 'navigation.logo.light.icon.mobile', + }, + { + type: 'textInput', + title: 'Dark theme desktop logo URL', + name: 'navigation.logo.dark.icon.default', + }, + { + type: 'textInput', + title: 'Dark theme mobile logo URL', + name: 'navigation.logo.dark.icon.mobile', + }, + { + type: 'textInput', + title: 'Alt text', + name: 'navigation.logo.alt', + }, + { + type: 'textInput', + title: 'Logo URL', + name: 'navigation.logo.url', + }, + { + type: 'text', + text: 'Header', + }, + { + type: 'switch', + title: 'Compact', + name: 'navigation.header.compact', + }, + { + type: 'switch', + title: 'Hide logo', + name: 'navigation.header.hideLogo', + }, + { + type: 'switch', + title: 'Dark theme', + name: 'navigation.header.isDarkTheme', + }, + { + type: 'textInput', + title: 'Icon size', + name: 'navigation.header.iconSize', + }, + { + type: 'switch', + title: 'With border', + name: 'navigation.header.withBorder', + }, + { + type: 'switch', + title: 'With border on scroll', + name: 'navigation.header.withBorderOnScroll', + }, + { + type: 'section', + title: 'Left menu item {{index}}', + index: 'index', + withAddButton: true, + fields: [ + { + type: 'textInput', + title: 'Text', + name: 'navigation.header.leftItems[{{index}}].text', + }, + { + type: 'textInput', + title: 'URL', + name: 'navigation.header.leftItems[{{index}}].url', + }, + { + type: 'select', + title: 'Target', + name: 'navigation.header.leftItems[{{index}}].target', + options: [ + {value: '_blank'}, + {value: '_self'}, + {value: '_parent'}, + {value: '_top'}, + ], + hasClear: true, + }, + { + type: 'select', + title: 'Type', + name: 'navigation.header.leftItems[{{index}}].type', + options: [ + {value: 'link', content: 'Link'}, + {value: 'dropdown', content: 'Dropdown'}, + {value: 'button', content: 'Button'}, + ], + }, + ], + }, + { + type: 'section', + title: 'Right button {{index2}}', + index: 'index2', + withAddButton: true, + fields: [ + { + type: 'textInput', + title: 'Text', + name: 'navigation.header.rightItems[{{index2}}].text', + }, + { + type: 'textInput', + title: 'URL', + name: 'navigation.header.rightItems[{{index2}}].url', + }, + { + type: 'select', + title: 'Theme', + name: 'navigation.header.rightItems[{{index2}}].theme', + options: [ + {value: 'action', content: 'Action'}, + {value: 'outlined', content: 'Outlined'}, + {value: 'normal', content: 'Normal'}, + {value: 'monochrome', content: 'Monochrome'}, + { + value: 'outlined-contrast', + content: 'Outlined-contrast', + }, + { + value: 'normal-contrast', + content: 'Normal-contrast', + }, + ], + }, + { + type: 'select', + title: 'Type', + name: 'navigation.header.rightItems[{{index2}}].type', + options: [ + {value: 'link', content: 'Link'}, + {value: 'button', content: 'Button'}, + ], + }, + ], + }, + { + type: 'text', + text: 'Footer', + }, + { + type: 'section', + title: 'Column {{index3}}', + index: 'index3', + withAddButton: true, + fields: [ + { + type: 'textInput', + title: 'Title', + name: 'navigation.footer.columns[{{index3}}].title', + }, + { + type: 'section', + title: 'Link {{index4}}', + index: 'index4', + withAddButton: true, + fields: [ + { + type: 'textInput', + title: 'Text', + name: 'navigation.footer.columns[{{index3}}].links[{{index4}}].text', + }, + { + type: 'textInput', + title: 'URL', + name: 'navigation.footer.columns[{{index3}}].links[{{index4}}].url', + }, + { + type: 'select', + title: 'Target', + name: 'navigation.footer.columns[{{index3}}].links[{{index4}}].target', + options: [ + {value: '_blank'}, + {value: '_self'}, + {value: '_parent'}, + {value: '_top'}, + ], + hasClear: true, + }, + { + type: 'select', + title: 'Type', + name: 'navigation.footer.columns[{{index3}}].links[{{index4}}].type', + options: [ + {value: 'link', content: 'Link'}, + {value: 'button', content: 'Button'}, + ], + }, + ], + }, + ], + }, + { + type: 'section', + title: 'Social link {{index5}}', + index: 'index5', + withAddButton: true, + fields: [ + { + type: 'textInput', + title: 'Icon URL', + name: 'navigation.footer.social[{{index5}}].icon.src', + }, + { + type: 'textInput', + title: 'URL', + name: 'navigation.footer.social[{{index5}}].url', + }, + { + type: 'textInput', + title: 'Alt text', + name: 'navigation.footer.social[{{index5}}].urlTitle', + }, + ], + }, + { + type: 'text', + text: 'Underline', + }, + { + type: 'textInput', + title: 'Copyright text', + name: 'navigation.footer.underline.copyright', + }, + { + type: 'section', + title: 'Link {{index6}}', + index: 'index6', + withAddButton: true, + fields: [ + { + type: 'textInput', + title: 'Text', + name: 'navigation.footer.underline.links[{{index6}}].text', + }, + { + type: 'textInput', + title: 'URL', + name: 'navigation.footer.underline.links[{{index6}}].url', + }, + { + type: 'select', + title: 'Type', + name: 'navigation.footer.underline.links[{{index6}}].type', + options: [ + {value: 'link', content: 'Link'}, + {value: 'button', content: 'Button'}, + ], + }, + ], + }, + ], + }, + ], + globalDefaults, + }, + }; +}; diff --git a/src/gravity-blocks/extensions/index.ts b/src/gravity-blocks/extensions/index.ts new file mode 100644 index 0000000000..b8aa82023c --- /dev/null +++ b/src/gravity-blocks/extensions/index.ts @@ -0,0 +1,32 @@ +export {blockBaseExtension, BlockBaseExtensionBlockWrapper} from './BlockBaseExtension'; + +export { + generalExtension, + GeneralExtensionContentWrapper, + type GeneralExtensionGlobalConfig, + type GeneralExtensionWrapperProps, + type GeneralPageContent, +} from './GeneralExtension'; + +export { + backgroundExtension, + BackgroundExtensionContentWrapper, + type BackgroundExtensionGlobalConfig, + type BackgroundExtensionWrapperProps, + type BackgroundPageContent, +} from './BackgroundExtension'; + +export { + navigationExtension, + NavigationExtensionContentWrapper, + type NavigationExtensionGlobalConfig, + type NavigationExtensionWrapperProps, + type NavigationPageContent, +} from './NavigationExtension'; + +export { + gravityBlocksExtension, + type GravityBlocksGlobalConfig, + type GravityBlocksWrapperProps, + type GravityPageContent, +} from './GravityBlocksExtension'; diff --git a/src/grid/Break/Break.tsx b/src/gravity-blocks/grid/Break/Break.tsx similarity index 100% rename from src/grid/Break/Break.tsx rename to src/gravity-blocks/grid/Break/Break.tsx diff --git a/src/grid/Col/Col.tsx b/src/gravity-blocks/grid/Col/Col.tsx similarity index 92% rename from src/grid/Col/Col.tsx rename to src/gravity-blocks/grid/Col/Col.tsx index a630c8ec3b..714f42967b 100644 --- a/src/grid/Col/Col.tsx +++ b/src/gravity-blocks/grid/Col/Col.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {AriaProps, QAProps, Refable, Roleable} from '../../models'; +import {AriaProps, QAProps, Refable, Roleable} from '../../../models'; import {GridColumnClassParams} from '../types'; import {getColClass} from '../utils'; diff --git a/src/grid/Grid/Grid.scss b/src/gravity-blocks/grid/Grid/Grid.scss similarity index 90% rename from src/grid/Grid/Grid.scss rename to src/gravity-blocks/grid/Grid/Grid.scss index 5c172d42f8..01949f1a6d 100644 --- a/src/grid/Grid/Grid.scss +++ b/src/gravity-blocks/grid/Grid/Grid.scss @@ -1,4 +1,4 @@ -@import '../../../styles/variables.scss'; +@import '../../../../styles/variables.scss'; $block: '.#{$ns}Grid'; @@ -15,8 +15,8 @@ $block: '.#{$ns}Grid'; } .row { - margin-right: 0; - margin-left: 0; + //margin-right: 0; + //margin-left: 0; } #{$block} { @@ -26,7 +26,7 @@ $block: '.#{$ns}Grid'; } } - .row .row { + .row { margin: 0 -#{$gutter}; } @@ -58,7 +58,7 @@ $block: '.#{$ns}Grid'; } } - .row .row { + .row { margin: 0 -#{$gutterMobile}; } } diff --git a/src/grid/Grid/Grid.tsx b/src/gravity-blocks/grid/Grid/Grid.tsx similarity index 93% rename from src/grid/Grid/Grid.tsx rename to src/gravity-blocks/grid/Grid/Grid.tsx index d7c29e2977..4a14700529 100644 --- a/src/grid/Grid/Grid.tsx +++ b/src/gravity-blocks/grid/Grid/Grid.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {block} from '../../utils'; +import {block} from '../../../utils'; import './Grid.scss'; import '../styles/bootstrap.scss'; diff --git a/src/grid/Row/Row.tsx b/src/gravity-blocks/grid/Row/Row.tsx similarity index 89% rename from src/grid/Row/Row.tsx rename to src/gravity-blocks/grid/Row/Row.tsx index 7288aad228..d3978f77a7 100644 --- a/src/grid/Row/Row.tsx +++ b/src/gravity-blocks/grid/Row/Row.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import {AriaProps, Roleable} from '../../models'; -import {ClassNameProps, Refable} from '../../models/common'; +import {AriaProps, Roleable} from '../../../models'; +import {ClassNameProps, Refable} from '../../../models/common'; import {GridAlignItems, GridJustifyContent} from '../types'; export interface RowProps extends ClassNameProps, Refable<HTMLDivElement>, Roleable, AriaProps { diff --git a/src/grid/index.ts b/src/gravity-blocks/grid/index.ts similarity index 100% rename from src/grid/index.ts rename to src/gravity-blocks/grid/index.ts diff --git a/src/grid/styles/bootstrap.scss b/src/gravity-blocks/grid/styles/bootstrap.scss similarity index 99% rename from src/grid/styles/bootstrap.scss rename to src/gravity-blocks/grid/styles/bootstrap.scss index 09b9e61c9a..11758e7cad 100644 --- a/src/grid/styles/bootstrap.scss +++ b/src/gravity-blocks/grid/styles/bootstrap.scss @@ -7,7 +7,7 @@ /* stylelint-disable declaration-no-important */ -@import '../../../styles/variables.scss'; +@import '../../../../styles/variables.scss'; html { box-sizing: border-box; diff --git a/src/grid/types.ts b/src/gravity-blocks/grid/types.ts similarity index 100% rename from src/grid/types.ts rename to src/gravity-blocks/grid/types.ts diff --git a/src/grid/utils.ts b/src/gravity-blocks/grid/utils.ts similarity index 100% rename from src/grid/utils.ts rename to src/gravity-blocks/grid/utils.ts diff --git a/src/hooks/hubspot.ts b/src/gravity-blocks/hooks/hubspot.ts similarity index 90% rename from src/hooks/hubspot.ts rename to src/gravity-blocks/hooks/hubspot.ts index 2a05de598e..b1a5a3198d 100644 --- a/src/hooks/hubspot.ts +++ b/src/gravity-blocks/hooks/hubspot.ts @@ -1,6 +1,10 @@ import * as React from 'react'; -import {HubspotEventHandlers, handleHubspotEvents, loopBackHabspotEvents} from '../utils/hubspot'; +import { + HubspotEventHandlers, + handleHubspotEvents, + loopBackHabspotEvents, +} from '../../utils/hubspot'; /** * @param {string} formId diff --git a/src/hooks/index.ts b/src/gravity-blocks/hooks/index.ts similarity index 80% rename from src/hooks/index.ts rename to src/gravity-blocks/hooks/index.ts index 9430de593a..19687977dc 100644 --- a/src/hooks/index.ts +++ b/src/gravity-blocks/hooks/index.ts @@ -1,8 +1,8 @@ export {default as useFocus} from './useFocus'; -export {default as useWindowBreakpoint} from './useWindowBreakpoint'; export {default as useMount} from './useMount'; export {default as useHeightCalculator} from './useHeightCalculator'; export * from './useImageSize'; export * from './useAnalytics'; export * from './hubspot'; export * from './useDeviceValue'; +export * from './useContent'; diff --git a/src/hooks/useAnalytics.ts b/src/gravity-blocks/hooks/useAnalytics.ts similarity index 90% rename from src/hooks/useAnalytics.ts rename to src/gravity-blocks/hooks/useAnalytics.ts index 5eb1768d9d..b11b9569fe 100644 --- a/src/hooks/useAnalytics.ts +++ b/src/gravity-blocks/hooks/useAnalytics.ts @@ -1,8 +1,8 @@ import * as React from 'react'; +import {BlockIdContext} from '../../context/blockIdContext'; +import {AnalyticsEvent, AnalyticsEventsProp, PredefinedEventTypes} from '../../models'; import {AnalyticsContext} from '../context/analyticsContext'; -import {BlockIdContext} from '../context/blockIdContext'; -import {AnalyticsEvent, AnalyticsEventsProp, PredefinedEventTypes} from '../models'; export const useAnalytics = (name = '', target?: string) => { const {sendEvents, autoEvents} = React.useContext(AnalyticsContext); @@ -12,7 +12,7 @@ export const useAnalytics = (name = '', target?: string) => { name ? { name, - context, + context: String(context), type: PredefinedEventTypes.Default, target: target, } diff --git a/src/gravity-blocks/hooks/useContent.ts b/src/gravity-blocks/hooks/useContent.ts new file mode 100644 index 0000000000..cd9fddd88d --- /dev/null +++ b/src/gravity-blocks/hooks/useContent.ts @@ -0,0 +1,12 @@ +import * as React from 'react'; + +import {InnerContext} from '../../context/innerContext'; +import {PageContent} from '../../models'; + +export function useContent<T extends object = object>() { + const {content, setContent} = React.useContext(InnerContext); + return { + content: content as T & PageContent, + setContent: setContent as React.Dispatch<React.SetStateAction<T>>, + }; +} diff --git a/src/hooks/useDeviceValue.ts b/src/gravity-blocks/hooks/useDeviceValue.ts similarity index 87% rename from src/hooks/useDeviceValue.ts rename to src/gravity-blocks/hooks/useDeviceValue.ts index b5b93ad544..a3fec54768 100644 --- a/src/hooks/useDeviceValue.ts +++ b/src/gravity-blocks/hooks/useDeviceValue.ts @@ -1,9 +1,9 @@ import * as React from 'react'; -import {BREAKPOINTS} from '../constants'; +import {BREAKPOINTS} from '../../constants'; +import {Device} from '../../models'; +import {DeviceSupporting, isDeviceValue} from '../../utils'; import {useWindowWidth} from '../context/windowWidthContext'; -import {Device} from '../models'; -import {DeviceSupporting, isDeviceValue} from '../utils'; const getDeviceBreakpoints = (inclusive?: boolean): [tablet: number, mobile: number] => { const shift = inclusive ? 0 : -1; diff --git a/src/hooks/useFocus.ts b/src/gravity-blocks/hooks/useFocus.ts similarity index 100% rename from src/hooks/useFocus.ts rename to src/gravity-blocks/hooks/useFocus.ts diff --git a/src/hooks/useHeightCalculator.ts b/src/gravity-blocks/hooks/useHeightCalculator.ts similarity index 100% rename from src/hooks/useHeightCalculator.ts rename to src/gravity-blocks/hooks/useHeightCalculator.ts diff --git a/src/hooks/useImageSize.ts b/src/gravity-blocks/hooks/useImageSize.ts similarity index 97% rename from src/hooks/useImageSize.ts rename to src/gravity-blocks/hooks/useImageSize.ts index f9d3dd8295..9a918d0eea 100644 --- a/src/hooks/useImageSize.ts +++ b/src/gravity-blocks/hooks/useImageSize.ts @@ -1,6 +1,6 @@ import * as React from 'react'; -import {MediaAllProps} from '../components/Media/Media'; +import {MediaAllProps} from '../../components/Media/Media'; import {useWindowWidth} from '../context/windowWidthContext'; export const useImageSize = ({ diff --git a/src/gravity-blocks/hooks/useMetrika.ts b/src/gravity-blocks/hooks/useMetrika.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/hooks/useMount.ts b/src/gravity-blocks/hooks/useMount.ts similarity index 100% rename from src/hooks/useMount.ts rename to src/gravity-blocks/hooks/useMount.ts diff --git a/src/icons/BrandIconDark.tsx b/src/gravity-blocks/icons/BrandIconDark.tsx similarity index 98% rename from src/icons/BrandIconDark.tsx rename to src/gravity-blocks/icons/BrandIconDark.tsx index 1441a38eb6..fddddcb381 100644 --- a/src/icons/BrandIconDark.tsx +++ b/src/gravity-blocks/icons/BrandIconDark.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {a11yHiddenSvgProps} from '../utils/svg'; +import {a11yHiddenSvgProps} from '../../utils/svg'; export const BrandIconDark = (props: React.SVGProps<SVGSVGElement>) => ( <svg diff --git a/src/icons/BrandIconLight.tsx b/src/gravity-blocks/icons/BrandIconLight.tsx similarity index 98% rename from src/icons/BrandIconLight.tsx rename to src/gravity-blocks/icons/BrandIconLight.tsx index f276808926..72ecc77397 100644 --- a/src/icons/BrandIconLight.tsx +++ b/src/gravity-blocks/icons/BrandIconLight.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {a11yHiddenSvgProps} from '../utils/svg'; +import {a11yHiddenSvgProps} from '../../utils/svg'; export const BrandIconLight = (props: React.SVGProps<SVGSVGElement>) => ( <svg diff --git a/src/icons/BrandName.tsx b/src/gravity-blocks/icons/BrandName.tsx similarity index 98% rename from src/icons/BrandName.tsx rename to src/gravity-blocks/icons/BrandName.tsx index 76384ba363..aeb1d0bae4 100644 --- a/src/icons/BrandName.tsx +++ b/src/gravity-blocks/icons/BrandName.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {a11yHiddenSvgProps} from '../utils/svg'; +import {a11yHiddenSvgProps} from '../../utils/svg'; export const BRAND_NAME_RATIO = 72 / 16; diff --git a/src/icons/Chevron.tsx b/src/gravity-blocks/icons/Chevron.tsx similarity index 87% rename from src/icons/Chevron.tsx rename to src/gravity-blocks/icons/Chevron.tsx index f3ada929ba..89254025cf 100644 --- a/src/icons/Chevron.tsx +++ b/src/gravity-blocks/icons/Chevron.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {a11yHiddenSvgProps} from '../utils/svg'; +import {a11yHiddenSvgProps} from '../../utils/svg'; export const Chevron = (props: React.SVGProps<SVGSVGElement>) => ( <svg diff --git a/src/icons/Facebook.tsx b/src/gravity-blocks/icons/Facebook.tsx similarity index 90% rename from src/icons/Facebook.tsx rename to src/gravity-blocks/icons/Facebook.tsx index 0069f498fd..84ded6633c 100644 --- a/src/icons/Facebook.tsx +++ b/src/gravity-blocks/icons/Facebook.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {a11yHiddenSvgProps} from '../utils/svg'; +import {a11yHiddenSvgProps} from '../../utils/svg'; export const Facebook = (props: React.SVGProps<SVGSVGElement>) => ( <svg diff --git a/src/icons/Github.tsx b/src/gravity-blocks/icons/Github.tsx similarity index 97% rename from src/icons/Github.tsx rename to src/gravity-blocks/icons/Github.tsx index 2d71fa2854..7e84295680 100644 --- a/src/icons/Github.tsx +++ b/src/gravity-blocks/icons/Github.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {a11yHiddenSvgProps} from '../utils/svg'; +import {a11yHiddenSvgProps} from '../../utils/svg'; export const Github = (props: React.SVGProps<SVGSVGElement>) => ( <svg diff --git a/src/icons/Linkedin.tsx b/src/gravity-blocks/icons/Linkedin.tsx similarity index 96% rename from src/icons/Linkedin.tsx rename to src/gravity-blocks/icons/Linkedin.tsx index f6d64e98ef..af68b9b711 100644 --- a/src/icons/Linkedin.tsx +++ b/src/gravity-blocks/icons/Linkedin.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {a11yHiddenSvgProps} from '../utils/svg'; +import {a11yHiddenSvgProps} from '../../utils/svg'; export const Linkedin = (props: React.SVGProps<SVGSVGElement>) => ( <svg diff --git a/src/icons/NavigationArrow.tsx b/src/gravity-blocks/icons/NavigationArrow.tsx similarity index 90% rename from src/icons/NavigationArrow.tsx rename to src/gravity-blocks/icons/NavigationArrow.tsx index 9819ab1dce..d0d0a26df1 100644 --- a/src/icons/NavigationArrow.tsx +++ b/src/gravity-blocks/icons/NavigationArrow.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {a11yHiddenSvgProps} from '../utils/svg'; +import {a11yHiddenSvgProps} from '../../utils/svg'; export const NavigationArrow = (props: React.SVGProps<SVGSVGElement>) => ( <svg diff --git a/src/icons/NavigationChevron.tsx b/src/gravity-blocks/icons/NavigationChevron.tsx similarity index 90% rename from src/icons/NavigationChevron.tsx rename to src/gravity-blocks/icons/NavigationChevron.tsx index dab7d2f4d1..b588decf2c 100644 --- a/src/icons/NavigationChevron.tsx +++ b/src/gravity-blocks/icons/NavigationChevron.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {a11yHiddenSvgProps} from '../utils/svg'; +import {a11yHiddenSvgProps} from '../../utils/svg'; export const NavigationChevron = (props: React.SVGProps<SVGSVGElement>) => ( <svg diff --git a/src/icons/Telegram.tsx b/src/gravity-blocks/icons/Telegram.tsx similarity index 93% rename from src/icons/Telegram.tsx rename to src/gravity-blocks/icons/Telegram.tsx index 2d8809dfc0..3c312b3ea5 100644 --- a/src/icons/Telegram.tsx +++ b/src/gravity-blocks/icons/Telegram.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {a11yHiddenSvgProps} from '../utils/svg'; +import {a11yHiddenSvgProps} from '../../utils/svg'; export const Telegram = (props: React.SVGProps<SVGSVGElement>) => ( <svg diff --git a/src/icons/Twitter.tsx b/src/gravity-blocks/icons/Twitter.tsx similarity index 94% rename from src/icons/Twitter.tsx rename to src/gravity-blocks/icons/Twitter.tsx index 01c2146ad1..6c2abeef84 100644 --- a/src/icons/Twitter.tsx +++ b/src/gravity-blocks/icons/Twitter.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {a11yHiddenSvgProps} from '../utils/svg'; +import {a11yHiddenSvgProps} from '../../utils/svg'; export const Twitter = (props: React.SVGProps<SVGSVGElement>) => ( <svg diff --git a/src/icons/Vk.tsx b/src/gravity-blocks/icons/Vk.tsx similarity index 96% rename from src/icons/Vk.tsx rename to src/gravity-blocks/icons/Vk.tsx index 92bbc49448..f1baf3766c 100644 --- a/src/icons/Vk.tsx +++ b/src/gravity-blocks/icons/Vk.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {a11yHiddenSvgProps} from '../utils/svg'; +import {a11yHiddenSvgProps} from '../../utils/svg'; export const Vk = (props: React.SVGProps<SVGSVGElement>) => ( <svg diff --git a/src/icons/index.ts b/src/gravity-blocks/icons/index.ts similarity index 100% rename from src/icons/index.ts rename to src/gravity-blocks/icons/index.ts diff --git a/src/gravity-blocks/index.ts b/src/gravity-blocks/index.ts new file mode 100644 index 0000000000..cd98cdcb97 --- /dev/null +++ b/src/gravity-blocks/index.ts @@ -0,0 +1,7 @@ +export * from './extensions'; +export * from './schema'; +export * from './hooks'; +export * from './navigation'; +export * from './grid'; +export * from '../blocks'; +export * from '../utils/svg'; diff --git a/src/navigation/__stories__/CustomButton/CustomButton.scss b/src/gravity-blocks/navigation/__stories__/CustomButton/CustomButton.scss similarity index 100% rename from src/navigation/__stories__/CustomButton/CustomButton.scss rename to src/gravity-blocks/navigation/__stories__/CustomButton/CustomButton.scss diff --git a/src/navigation/__stories__/CustomButton/CustomButton.tsx b/src/gravity-blocks/navigation/__stories__/CustomButton/CustomButton.tsx similarity index 94% rename from src/navigation/__stories__/CustomButton/CustomButton.tsx rename to src/gravity-blocks/navigation/__stories__/CustomButton/CustomButton.tsx index 5335df4072..aa1444c9f3 100644 --- a/src/navigation/__stories__/CustomButton/CustomButton.tsx +++ b/src/gravity-blocks/navigation/__stories__/CustomButton/CustomButton.tsx @@ -1,7 +1,7 @@ import {TriangleUp} from '@gravity-ui/icons'; import {Button} from '@gravity-ui/uikit'; -import {cn} from '../../../utils'; +import {cn} from '../../../../utils'; import {NavigationItemProps} from '../../models'; import './CustomButton.scss'; diff --git a/src/navigation/__stories__/CustomComponent/CustomComponent.scss b/src/gravity-blocks/navigation/__stories__/CustomComponent/CustomComponent.scss similarity index 100% rename from src/navigation/__stories__/CustomComponent/CustomComponent.scss rename to src/gravity-blocks/navigation/__stories__/CustomComponent/CustomComponent.scss diff --git a/src/navigation/__stories__/CustomComponent/CustomComponent.tsx b/src/gravity-blocks/navigation/__stories__/CustomComponent/CustomComponent.tsx similarity index 95% rename from src/navigation/__stories__/CustomComponent/CustomComponent.tsx rename to src/gravity-blocks/navigation/__stories__/CustomComponent/CustomComponent.tsx index 08ca681f36..9dd765b4e7 100644 --- a/src/navigation/__stories__/CustomComponent/CustomComponent.tsx +++ b/src/gravity-blocks/navigation/__stories__/CustomComponent/CustomComponent.tsx @@ -1,6 +1,6 @@ import {useActionHandlers} from '@gravity-ui/uikit'; -import {cn} from '../../../utils'; +import {cn} from '../../../../utils'; import {NavigationItemProps} from '../../models'; import './CustomComponent.scss'; diff --git a/src/navigation/__stories__/Navigation.stories.tsx b/src/gravity-blocks/navigation/__stories__/Navigation.stories.tsx similarity index 80% rename from src/navigation/__stories__/Navigation.stories.tsx rename to src/gravity-blocks/navigation/__stories__/Navigation.stories.tsx index 7b32d3880d..9e55a0de63 100644 --- a/src/navigation/__stories__/Navigation.stories.tsx +++ b/src/gravity-blocks/navigation/__stories__/Navigation.stories.tsx @@ -1,7 +1,8 @@ import {Meta, StoryFn} from '@storybook/react'; -import {PageConstructor} from '../../containers/PageConstructor'; -import {CustomConfig, NavigationData} from '../../models'; +import {PageConstructor} from '../../../containers/PageConstructor'; +import {CustomConfig, NavigationData} from '../../../models'; +import {gravityBlocksExtension} from '../../extensions'; import {CustomButton} from './CustomButton/CustomButton'; import {CustomComponent} from './CustomComponent/CustomComponent'; @@ -16,7 +17,17 @@ export default { const DefaultTemplate: StoryFn<{ navigation: NavigationData; custom?: CustomConfig; -}> = ({navigation, custom = {}}) => <PageConstructor navigation={navigation} custom={custom} />; +}> = ({navigation, custom = {}}) => ( + <PageConstructor + content={{blocks: []}} + custom={custom} + extensions={gravityBlocksExtension({ + globalDefaults: { + navigation, + }, + })} + /> +); export const DefaultNavigation = DefaultTemplate.bind({}); export const NavigationWithBorder = DefaultTemplate.bind({}); export const NavigationWithCustomItems = DefaultTemplate.bind({}); diff --git a/src/navigation/__stories__/data.json b/src/gravity-blocks/navigation/__stories__/data.json similarity index 100% rename from src/navigation/__stories__/data.json rename to src/gravity-blocks/navigation/__stories__/data.json diff --git a/src/navigation/components/DesktopNavigation/DesktopNavigation.scss b/src/gravity-blocks/navigation/components/DesktopNavigation/DesktopNavigation.scss similarity index 97% rename from src/navigation/components/DesktopNavigation/DesktopNavigation.scss rename to src/gravity-blocks/navigation/components/DesktopNavigation/DesktopNavigation.scss index 60b312c0f8..a963237e1e 100644 --- a/src/navigation/components/DesktopNavigation/DesktopNavigation.scss +++ b/src/gravity-blocks/navigation/components/DesktopNavigation/DesktopNavigation.scss @@ -1,5 +1,5 @@ -@import '../../../../styles/variables'; -@import '../../../../styles/mixins'; +@import '../../../../../styles/variables'; +@import '../../../../../styles/mixins'; $block: '.#{$ns}desktop-navigation'; diff --git a/src/navigation/components/DesktopNavigation/DesktopNavigation.tsx b/src/gravity-blocks/navigation/components/DesktopNavigation/DesktopNavigation.tsx similarity index 94% rename from src/navigation/components/DesktopNavigation/DesktopNavigation.tsx rename to src/gravity-blocks/navigation/components/DesktopNavigation/DesktopNavigation.tsx index ccc88837d3..0d5f43b37d 100644 --- a/src/navigation/components/DesktopNavigation/DesktopNavigation.tsx +++ b/src/gravity-blocks/navigation/components/DesktopNavigation/DesktopNavigation.tsx @@ -1,5 +1,5 @@ -import OverflowScroller from '../../../components/OverflowScroller/OverflowScroller'; -import {block} from '../../../utils'; +import OverflowScroller from '../../../../components/OverflowScroller/OverflowScroller'; +import {block, isLogoSet} from '../../../../utils'; import {DesktopNavigationProps, ItemColumnName, NavigationLayout} from '../../models'; import Logo from '../Logo/Logo'; import {MobileMenuButton} from '../MobileMenuButton/MobileMenuButton'; @@ -20,7 +20,7 @@ export const DesktopNavigation = ({ activeItemId, }: DesktopNavigationProps) => ( <div className={b('wrapper')}> - {logo && ( + {isLogoSet(logo) && ( <div className={b('left')}> <Logo {...logo} className={b('logo')} /> </div> diff --git a/src/navigation/components/Logo/Logo.scss b/src/gravity-blocks/navigation/components/Logo/Logo.scss similarity index 78% rename from src/navigation/components/Logo/Logo.scss rename to src/gravity-blocks/navigation/components/Logo/Logo.scss index 0735204753..6e85b9863f 100644 --- a/src/navigation/components/Logo/Logo.scss +++ b/src/gravity-blocks/navigation/components/Logo/Logo.scss @@ -1,5 +1,5 @@ -@import '../../../../styles/variables'; -@import '../../../../styles/mixins'; +@import '../../../../../styles/variables'; +@import '../../../../../styles/mixins'; $block: '.#{$ns}logo'; diff --git a/src/navigation/components/Logo/Logo.tsx b/src/gravity-blocks/navigation/components/Logo/Logo.tsx similarity index 83% rename from src/navigation/components/Logo/Logo.tsx rename to src/gravity-blocks/navigation/components/Logo/Logo.tsx index c6e474f909..2f5ed2844c 100644 --- a/src/navigation/components/Logo/Logo.tsx +++ b/src/gravity-blocks/navigation/components/Logo/Logo.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; -import {Image} from '../../../components'; -import {getMediaImage} from '../../../components/Media/Image/utils'; -import RouterLink from '../../../components/RouterLink/RouterLink'; +import {Image} from '../../../../components'; +import {getMediaImage} from '../../../../components/Media/Image/utils'; +import RouterLink from '../../../../components/RouterLink/RouterLink'; +import {ThemedNavigationLogoData} from '../../../../models'; +import {block, getLinkProps, getThemedValue} from '../../../../utils'; import {LocationContext} from '../../../context/locationContext'; import {useTheme} from '../../../context/theme'; -import {ThemedNavigationLogoData} from '../../../models'; -import {block, getLinkProps, getThemedValue} from '../../../utils'; import {i18n} from './i18n'; diff --git a/src/navigation/components/Logo/i18n/en.json b/src/gravity-blocks/navigation/components/Logo/i18n/en.json similarity index 100% rename from src/navigation/components/Logo/i18n/en.json rename to src/gravity-blocks/navigation/components/Logo/i18n/en.json diff --git a/src/navigation/components/Logo/i18n/index.ts b/src/gravity-blocks/navigation/components/Logo/i18n/index.ts similarity index 78% rename from src/navigation/components/Logo/i18n/index.ts rename to src/gravity-blocks/navigation/components/Logo/i18n/index.ts index d01179801d..ea7f8ad8eb 100644 --- a/src/navigation/components/Logo/i18n/index.ts +++ b/src/gravity-blocks/navigation/components/Logo/i18n/index.ts @@ -1,6 +1,6 @@ import {addComponentKeysets} from '@gravity-ui/uikit/i18n'; -import {NAMESPACE} from '../../../../utils/cn'; +import {NAMESPACE} from '../../../../../utils/cn'; import en from './en.json'; import ru from './ru.json'; diff --git a/src/navigation/components/Logo/i18n/ru.json b/src/gravity-blocks/navigation/components/Logo/i18n/ru.json similarity index 100% rename from src/navigation/components/Logo/i18n/ru.json rename to src/gravity-blocks/navigation/components/Logo/i18n/ru.json diff --git a/src/navigation/components/MobileMenuButton/MobileMenuButton.scss b/src/gravity-blocks/navigation/components/MobileMenuButton/MobileMenuButton.scss similarity index 76% rename from src/navigation/components/MobileMenuButton/MobileMenuButton.scss rename to src/gravity-blocks/navigation/components/MobileMenuButton/MobileMenuButton.scss index 709aa469e6..5fc1dac58a 100644 --- a/src/navigation/components/MobileMenuButton/MobileMenuButton.scss +++ b/src/gravity-blocks/navigation/components/MobileMenuButton/MobileMenuButton.scss @@ -1,4 +1,4 @@ -@import '../../../../styles/mixins'; +@import '../../../../../styles/mixins'; $block: '.#{$ns}mobile-menu-button'; diff --git a/src/navigation/components/MobileMenuButton/MobileMenuButton.tsx b/src/gravity-blocks/navigation/components/MobileMenuButton/MobileMenuButton.tsx similarity index 88% rename from src/navigation/components/MobileMenuButton/MobileMenuButton.tsx rename to src/gravity-blocks/navigation/components/MobileMenuButton/MobileMenuButton.tsx index 3d912276b3..2a3b634031 100644 --- a/src/navigation/components/MobileMenuButton/MobileMenuButton.tsx +++ b/src/gravity-blocks/navigation/components/MobileMenuButton/MobileMenuButton.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import {Bars, Xmark} from '@gravity-ui/icons'; -import {Control} from '../../../components'; -import {block} from '../../../utils'; +import {Control} from '../../../../components'; +import {block} from '../../../../utils'; import {MobileMenuButtonProps} from '../../models'; import './MobileMenuButton.scss'; diff --git a/src/navigation/components/MobileNavigation/MobileNavigation.scss b/src/gravity-blocks/navigation/components/MobileNavigation/MobileNavigation.scss similarity index 94% rename from src/navigation/components/MobileNavigation/MobileNavigation.scss rename to src/gravity-blocks/navigation/components/MobileNavigation/MobileNavigation.scss index eb926a4d6c..02fb2e69f8 100644 --- a/src/navigation/components/MobileNavigation/MobileNavigation.scss +++ b/src/gravity-blocks/navigation/components/MobileNavigation/MobileNavigation.scss @@ -1,5 +1,5 @@ -@import '../../../../styles/variables'; -@import '../../../../styles/mixins'; +@import '../../../../../styles/variables'; +@import '../../../../../styles/mixins'; $block: '.#{$ns}mobile-navigation'; diff --git a/src/navigation/components/MobileNavigation/MobileNavigation.tsx b/src/gravity-blocks/navigation/components/MobileNavigation/MobileNavigation.tsx similarity index 94% rename from src/navigation/components/MobileNavigation/MobileNavigation.tsx rename to src/gravity-blocks/navigation/components/MobileNavigation/MobileNavigation.tsx index 6ed7609d61..3814f33c43 100644 --- a/src/navigation/components/MobileNavigation/MobileNavigation.tsx +++ b/src/gravity-blocks/navigation/components/MobileNavigation/MobileNavigation.tsx @@ -2,9 +2,9 @@ import * as React from 'react'; import {Portal} from '@gravity-ui/uikit'; -import Foldable from '../../../components/Foldable/Foldable'; +import Foldable from '../../../../components/Foldable/Foldable'; +import {block} from '../../../../utils'; import {useMount} from '../../../hooks'; -import {block} from '../../../utils'; import {ItemColumnName, MobileNavigationProps, NavigationLayout} from '../../models'; import {NavigationList} from '../NavigationList/NavigationList'; diff --git a/src/navigation/components/Navigation/Navigation.scss b/src/gravity-blocks/navigation/components/Navigation/Navigation.scss similarity index 78% rename from src/navigation/components/Navigation/Navigation.scss rename to src/gravity-blocks/navigation/components/Navigation/Navigation.scss index f7ec5d4cc3..3f2a421683 100644 --- a/src/navigation/components/Navigation/Navigation.scss +++ b/src/gravity-blocks/navigation/components/Navigation/Navigation.scss @@ -1,5 +1,5 @@ -@import '../../../../styles/variables'; -@import '../../../../styles/mixins'; +@import '../../../../../styles/variables'; +@import '../../../../../styles/mixins'; $block: '.#{$ns}navigation'; diff --git a/src/navigation/components/Navigation/Navigation.tsx b/src/gravity-blocks/navigation/components/Navigation/Navigation.tsx similarity index 91% rename from src/navigation/components/Navigation/Navigation.tsx rename to src/gravity-blocks/navigation/components/Navigation/Navigation.tsx index 6116f80ac5..a6332ea0b9 100644 --- a/src/navigation/components/Navigation/Navigation.tsx +++ b/src/gravity-blocks/navigation/components/Navigation/Navigation.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import OutsideClick from '../../../components/OutsideClick/OutsideClick'; +import OutsideClick from '../../../../components/OutsideClick/OutsideClick'; +import {ClassNameProps, HeaderData, ThemedNavigationLogoData} from '../../../../models'; +import {block} from '../../../../utils'; import {Col, Grid, Row} from '../../../grid'; -import {ClassNameProps, HeaderData, ThemedNavigationLogoData} from '../../../models'; -import {block} from '../../../utils'; import {useActiveNavItem, useShowBorder} from '../../hooks'; import DesktopNavigation from '../DesktopNavigation/DesktopNavigation'; import MobileNavigation from '../MobileNavigation/MobileNavigation'; @@ -13,8 +13,8 @@ import './Navigation.scss'; const b = block('navigation'); export interface NavigationComponentProps extends ClassNameProps { - logo: ThemedNavigationLogoData; - data: HeaderData; + logo?: ThemedNavigationLogoData; + data?: HeaderData; mobilePortalContainer?: React.RefObject<HTMLElement>; onSidebarChange?: (isOpen: boolean) => void; } @@ -27,13 +27,13 @@ export const Navigation = ({ onSidebarChange, }: NavigationComponentProps) => { const { - leftItems, + leftItems = [], rightItems, customMobileHeaderItems, iconSize = 20, withBorder = false, withBorderOnScroll = true, - } = data; + } = data || {}; const [isSidebarOpened, setIsSidebarOpened] = React.useState(false); const [showBorder] = useShowBorder(withBorder, withBorderOnScroll); diff --git a/src/navigation/components/NavigationItem/NavigationItem.scss b/src/gravity-blocks/navigation/components/NavigationItem/NavigationItem.scss similarity index 90% rename from src/navigation/components/NavigationItem/NavigationItem.scss rename to src/gravity-blocks/navigation/components/NavigationItem/NavigationItem.scss index 64cccd7f6d..1692d225d8 100644 --- a/src/navigation/components/NavigationItem/NavigationItem.scss +++ b/src/gravity-blocks/navigation/components/NavigationItem/NavigationItem.scss @@ -1,5 +1,5 @@ -@import '../../../../styles/variables'; -@import '../../../../styles/mixins'; +@import '../../../../../styles/variables'; +@import '../../../../../styles/mixins'; $block: '.#{$ns}navigation-item'; diff --git a/src/navigation/components/NavigationItem/NavigationItem.tsx b/src/gravity-blocks/navigation/components/NavigationItem/NavigationItem.tsx similarity index 84% rename from src/navigation/components/NavigationItem/NavigationItem.tsx rename to src/gravity-blocks/navigation/components/NavigationItem/NavigationItem.tsx index fb6db07fe1..42edc5bb36 100644 --- a/src/navigation/components/NavigationItem/NavigationItem.tsx +++ b/src/gravity-blocks/navigation/components/NavigationItem/NavigationItem.tsx @@ -2,9 +2,9 @@ import * as React from 'react'; import omit from 'lodash/omit'; -import {BlockIdContext} from '../../../context/blockIdContext'; -import {CustomItem, NavigationItemType, NavigationItemTypes} from '../../../models'; -import {block} from '../../../utils'; +import {BlockIdContext} from '../../../../context/blockIdContext'; +import {CustomItem, NavigationItemType, NavigationItemTypes} from '../../../../models'; +import {block} from '../../../../utils'; import {NavigationItemProps} from '../../models'; import {useNavigationItemMap} from './hooks/useNavigationItemMap'; @@ -43,7 +43,9 @@ export const NavigationItem = ({data, className, menuLayout, ...props}: Navigati }, [data, props, type, menuLayout]); return ( - <BlockIdContext.Provider value={ANALYTICS_ID}> + // TODO: fix any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + <BlockIdContext.Provider value={ANALYTICS_ID as any}> <li className={b({'menu-layout': menuLayout}, className)}> <Component {...componentProps} className={b('content', {type})} /> </li> diff --git a/src/navigation/components/NavigationItem/components/ContentWrapper/ContentWrapper.scss b/src/gravity-blocks/navigation/components/NavigationItem/components/ContentWrapper/ContentWrapper.scss similarity index 77% rename from src/navigation/components/NavigationItem/components/ContentWrapper/ContentWrapper.scss rename to src/gravity-blocks/navigation/components/NavigationItem/components/ContentWrapper/ContentWrapper.scss index 9905642278..0b8db7d282 100644 --- a/src/navigation/components/NavigationItem/components/ContentWrapper/ContentWrapper.scss +++ b/src/gravity-blocks/navigation/components/NavigationItem/components/ContentWrapper/ContentWrapper.scss @@ -1,4 +1,4 @@ -@import '../../../../../../styles/variables'; +@import '../../../../../../../styles/variables'; $block: '.#{$ns}content-wrapper'; diff --git a/src/navigation/components/NavigationItem/components/ContentWrapper/ContentWrapper.tsx b/src/gravity-blocks/navigation/components/NavigationItem/components/ContentWrapper/ContentWrapper.tsx similarity index 82% rename from src/navigation/components/NavigationItem/components/ContentWrapper/ContentWrapper.tsx rename to src/gravity-blocks/navigation/components/NavigationItem/components/ContentWrapper/ContentWrapper.tsx index a89f01f164..7be7a560a0 100644 --- a/src/navigation/components/NavigationItem/components/ContentWrapper/ContentWrapper.tsx +++ b/src/gravity-blocks/navigation/components/NavigationItem/components/ContentWrapper/ContentWrapper.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import {Image} from '../../../../../components'; -import {ImageProps} from '../../../../../models'; -import {block} from '../../../../../utils'; +import {Image} from '../../../../../../components'; +import {ImageProps} from '../../../../../../models'; +import {block} from '../../../../../../utils'; import './ContentWrapper.scss'; diff --git a/src/navigation/components/NavigationItem/components/GithubButton/GithubButton.scss b/src/gravity-blocks/navigation/components/NavigationItem/components/GithubButton/GithubButton.scss similarity index 85% rename from src/navigation/components/NavigationItem/components/GithubButton/GithubButton.scss rename to src/gravity-blocks/navigation/components/NavigationItem/components/GithubButton/GithubButton.scss index 40dbf27c52..64d2ff0d31 100644 --- a/src/navigation/components/NavigationItem/components/GithubButton/GithubButton.scss +++ b/src/gravity-blocks/navigation/components/NavigationItem/components/GithubButton/GithubButton.scss @@ -1,4 +1,4 @@ -@import '../../../../../../styles/variables'; +@import '../../../../../../../styles/variables'; @import '../../mixins'; $block: '.#{$ns}github-button'; diff --git a/src/navigation/components/NavigationItem/components/GithubButton/GithubButton.tsx b/src/gravity-blocks/navigation/components/NavigationItem/components/GithubButton/GithubButton.tsx similarity index 96% rename from src/navigation/components/NavigationItem/components/GithubButton/GithubButton.tsx rename to src/gravity-blocks/navigation/components/NavigationItem/components/GithubButton/GithubButton.tsx index 7d3a5e3cff..f2a4a14f7f 100644 --- a/src/navigation/components/NavigationItem/components/GithubButton/GithubButton.tsx +++ b/src/gravity-blocks/navigation/components/NavigationItem/components/GithubButton/GithubButton.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import {NavigationGithubButton, NavigationGithubButtonIcon} from '../../../../../models'; -import {block} from '../../../../../utils'; +import {NavigationGithubButton, NavigationGithubButtonIcon} from '../../../../../../models'; +import {block} from '../../../../../../utils'; import {NavigationItemProps} from '../../../../models'; import './GithubButton.scss'; diff --git a/src/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.scss b/src/gravity-blocks/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.scss similarity index 61% rename from src/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.scss rename to src/gravity-blocks/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.scss index e281802aa3..4cad5bad36 100644 --- a/src/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.scss +++ b/src/gravity-blocks/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.scss @@ -1,4 +1,4 @@ -@import '../../../../../../styles/variables'; +@import '../../../../../../../styles/variables'; $block: '.#{$ns}navigation-button'; diff --git a/src/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.tsx b/src/gravity-blocks/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.tsx similarity index 64% rename from src/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.tsx rename to src/gravity-blocks/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.tsx index 0eded43742..5c752434fc 100644 --- a/src/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.tsx +++ b/src/gravity-blocks/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.tsx @@ -1,7 +1,7 @@ -import {Button, RouterLink} from '../../../../../components'; -import {BlockIdContext} from '../../../../../context/blockIdContext'; -import {ButtonProps} from '../../../../../models'; -import {block} from '../../../../../utils'; +import {Button, RouterLink} from '../../../../../../components'; +import {BlockIdContext} from '../../../../../../context/blockIdContext'; +import {ButtonProps} from '../../../../../../models'; +import {block} from '../../../../../../utils'; import {NavigationItemProps} from '../../../../models'; import './NavigationButton.scss'; @@ -16,7 +16,9 @@ export const NavigationButton = (props: NavigationButtonProps) => { const {url, target, className} = props; const classes = b(null, className); return ( - <BlockIdContext.Provider value={ANALYTICS_ID}> + // TODO: fix any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + <BlockIdContext.Provider value={ANALYTICS_ID as any}> {target ? ( <Button className={classes} {...props} url={url} /> ) : ( diff --git a/src/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.scss b/src/gravity-blocks/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.scss similarity index 80% rename from src/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.scss rename to src/gravity-blocks/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.scss index 374a9a4635..9244dbf826 100644 --- a/src/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.scss +++ b/src/gravity-blocks/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.scss @@ -1,5 +1,5 @@ -@import '../../../../../../styles/variables'; -@import '../../../../../../styles/mixins'; +@import '../../../../../../../styles/variables'; +@import '../../../../../../../styles/mixins'; @import '../../mixins'; $block: '.#{$ns}navigation-dropdown'; diff --git a/src/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.tsx b/src/gravity-blocks/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.tsx similarity index 86% rename from src/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.tsx rename to src/gravity-blocks/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.tsx index 2556820e7b..df01720135 100644 --- a/src/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.tsx +++ b/src/gravity-blocks/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import {ToggleArrow} from '../../../../../components'; -import {getMediaImage} from '../../../../../components/Media/Image/utils'; -import {NavigationDropdownItem} from '../../../../../models'; -import {block} from '../../../../../utils'; +import {ToggleArrow} from '../../../../../../components'; +import {getMediaImage} from '../../../../../../components/Media/Image/utils'; +import {NavigationDropdownItem} from '../../../../../../models'; +import {block} from '../../../../../../utils'; import {NavigationItemProps} from '../../../../models'; import NavigationPopup from '../../../NavigationPopup/NavigationPopup'; import {ContentWrapper} from '../ContentWrapper/ContentWrapper'; diff --git a/src/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.scss b/src/gravity-blocks/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.scss similarity index 76% rename from src/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.scss rename to src/gravity-blocks/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.scss index bdcd935b7d..202d6e9b79 100644 --- a/src/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.scss +++ b/src/gravity-blocks/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.scss @@ -1,5 +1,5 @@ -@import '../../../../../../styles/variables'; -@import '../../../../../../styles/mixins'; +@import '../../../../../../../styles/variables'; +@import '../../../../../../../styles/mixins'; @import '../../mixins'; $block: '.#{$ns}navigation-link'; diff --git a/src/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.tsx b/src/gravity-blocks/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.tsx similarity index 80% rename from src/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.tsx rename to src/gravity-blocks/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.tsx index e47665c29a..daf0db3517 100644 --- a/src/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.tsx +++ b/src/gravity-blocks/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; -import {RouterLink} from '../../../../../components'; -import {getMediaImage} from '../../../../../components/Media/Image/utils'; -import {LocationContext} from '../../../../../context/locationContext'; -import {useAnalytics} from '../../../../../hooks'; -import {NavigationArrow} from '../../../../../icons'; -import {DefaultEventNames, NavigationLinkItem} from '../../../../../models'; -import {block, getLinkProps} from '../../../../../utils'; -import {NavigationItemProps} from '../../../../models'; +import {RouterLink} from '../../../../../../components'; +import {getMediaImage} from '../../../../../../components/Media/Image/utils'; +import {LocationContext} from '../../../../../../gravity-blocks/context/locationContext'; +import {useAnalytics} from '../../../../../../gravity-blocks/hooks'; +import {NavigationArrow} from '../../../../../../gravity-blocks/icons'; +import {DefaultEventNames, NavigationLinkItem} from '../../../../../../models'; +import {block, getLinkProps} from '../../../../../../utils'; +import type {NavigationItemProps} from '../../../../../navigation/models'; import {ContentWrapper} from '../ContentWrapper/ContentWrapper'; import './NavigationLink.scss'; diff --git a/src/navigation/components/NavigationItem/hooks/useNavigationItemMap.ts b/src/gravity-blocks/navigation/components/NavigationItem/hooks/useNavigationItemMap.ts similarity index 62% rename from src/navigation/components/NavigationItem/hooks/useNavigationItemMap.ts rename to src/gravity-blocks/navigation/components/NavigationItem/hooks/useNavigationItemMap.ts index da17c34e9e..8f954b3ed2 100644 --- a/src/navigation/components/NavigationItem/hooks/useNavigationItemMap.ts +++ b/src/gravity-blocks/navigation/components/NavigationItem/hooks/useNavigationItemMap.ts @@ -2,8 +2,8 @@ import * as React from 'react'; import isEmpty from 'lodash/isEmpty'; -import {navItemMap as NavItemMapDefault} from '../../../../constructor-items'; -import {InnerContext} from '../../../../context/innerContext'; +import {navItemMap as NavItemMapDefault} from '../../../../../constructor-items'; +import {InnerContext} from '../../../../../context/innerContext'; export const useNavigationItemMap = () => { const {navItemMap} = React.useContext(InnerContext); diff --git a/src/navigation/components/NavigationItem/index.ts b/src/gravity-blocks/navigation/components/NavigationItem/index.ts similarity index 100% rename from src/navigation/components/NavigationItem/index.ts rename to src/gravity-blocks/navigation/components/NavigationItem/index.ts diff --git a/src/navigation/components/NavigationItem/mixins.scss b/src/gravity-blocks/navigation/components/NavigationItem/mixins.scss similarity index 100% rename from src/navigation/components/NavigationItem/mixins.scss rename to src/gravity-blocks/navigation/components/NavigationItem/mixins.scss diff --git a/src/navigation/components/NavigationList/NavigationList.tsx b/src/gravity-blocks/navigation/components/NavigationList/NavigationList.tsx similarity index 100% rename from src/navigation/components/NavigationList/NavigationList.tsx rename to src/gravity-blocks/navigation/components/NavigationList/NavigationList.tsx diff --git a/src/navigation/components/NavigationListItem/NavigationListItem.tsx b/src/gravity-blocks/navigation/components/NavigationListItem/NavigationListItem.tsx similarity index 100% rename from src/navigation/components/NavigationListItem/NavigationListItem.tsx rename to src/gravity-blocks/navigation/components/NavigationListItem/NavigationListItem.tsx diff --git a/src/navigation/components/NavigationPopup/NavigationPopup.scss b/src/gravity-blocks/navigation/components/NavigationPopup/NavigationPopup.scss similarity index 85% rename from src/navigation/components/NavigationPopup/NavigationPopup.scss rename to src/gravity-blocks/navigation/components/NavigationPopup/NavigationPopup.scss index 46e53ba0ef..fc34071cd8 100644 --- a/src/navigation/components/NavigationPopup/NavigationPopup.scss +++ b/src/gravity-blocks/navigation/components/NavigationPopup/NavigationPopup.scss @@ -1,5 +1,5 @@ -@import '../../../../styles/variables'; -@import '../../../../styles/mixins'; +@import '../../../../../styles/variables'; +@import '../../../../../styles/mixins'; $block: '.#{$ns}navigation-popup'; diff --git a/src/navigation/components/NavigationPopup/NavigationPopup.tsx b/src/gravity-blocks/navigation/components/NavigationPopup/NavigationPopup.tsx similarity index 97% rename from src/navigation/components/NavigationPopup/NavigationPopup.tsx rename to src/gravity-blocks/navigation/components/NavigationPopup/NavigationPopup.tsx index 53d99f764a..589c771d6e 100644 --- a/src/navigation/components/NavigationPopup/NavigationPopup.tsx +++ b/src/gravity-blocks/navigation/components/NavigationPopup/NavigationPopup.tsx @@ -1,6 +1,6 @@ import {Popup} from '@gravity-ui/uikit'; -import {block} from '../../../utils'; +import {block} from '../../../../utils'; import {NavigationLayout, NavigationPopupProps} from '../../models'; import NavigationItem from '../NavigationItem'; diff --git a/src/navigation/components/SocialIcon/SocialIcon.scss b/src/gravity-blocks/navigation/components/SocialIcon/SocialIcon.scss similarity index 84% rename from src/navigation/components/SocialIcon/SocialIcon.scss rename to src/gravity-blocks/navigation/components/SocialIcon/SocialIcon.scss index c7619c79d8..8dd144bc17 100644 --- a/src/navigation/components/SocialIcon/SocialIcon.scss +++ b/src/gravity-blocks/navigation/components/SocialIcon/SocialIcon.scss @@ -1,5 +1,5 @@ -@import '../../../../styles/variables'; -@import '../../../../styles/mixins'; +@import '../../../../../styles/variables'; +@import '../../../../../styles/mixins'; $block: '.#{$ns}social-icon'; diff --git a/src/navigation/components/SocialIcon/SocialIcon.tsx b/src/gravity-blocks/navigation/components/SocialIcon/SocialIcon.tsx similarity index 75% rename from src/navigation/components/SocialIcon/SocialIcon.tsx rename to src/gravity-blocks/navigation/components/SocialIcon/SocialIcon.tsx index 8d39a99df1..29a8e5a480 100644 --- a/src/navigation/components/SocialIcon/SocialIcon.tsx +++ b/src/gravity-blocks/navigation/components/SocialIcon/SocialIcon.tsx @@ -1,7 +1,7 @@ -import {Image} from '../../../components'; -import {getMediaImage} from '../../../components/Media/Image/utils'; -import {NavigationSocialItem} from '../../../models'; -import {block} from '../../../utils'; +import {Image} from '../../../../components'; +import {getMediaImage} from '../../../../components/Media/Image/utils'; +import {NavigationSocialItem} from '../../../../models'; +import {block} from '../../../../utils'; import './SocialIcon.scss'; diff --git a/src/navigation/components/Standalone/index.tsx b/src/gravity-blocks/navigation/components/Standalone/index.tsx similarity index 82% rename from src/navigation/components/Standalone/index.tsx rename to src/gravity-blocks/navigation/components/Standalone/index.tsx index db20fa850b..64fcf6c38e 100644 --- a/src/navigation/components/Standalone/index.tsx +++ b/src/gravity-blocks/navigation/components/Standalone/index.tsx @@ -1,4 +1,4 @@ -import RootCn from '../../../components/RootCn'; +import RootCn from '../../../../components/RootCn'; import Navigation, {NavigationComponentProps} from './../../components/Navigation/Navigation'; diff --git a/src/navigation/containers/Layout/Layout.scss b/src/gravity-blocks/navigation/containers/Layout/Layout.scss similarity index 73% rename from src/navigation/containers/Layout/Layout.scss rename to src/gravity-blocks/navigation/containers/Layout/Layout.scss index 7d2a45b721..8a0d8b6518 100644 --- a/src/navigation/containers/Layout/Layout.scss +++ b/src/gravity-blocks/navigation/containers/Layout/Layout.scss @@ -1,4 +1,4 @@ -@import '../../../../styles/variables'; +@import '../../../../../styles/variables'; $block: '.#{$ns}layout'; @@ -6,7 +6,8 @@ $block: '.#{$ns}layout'; display: flex; flex-direction: column; - min-height: 100vh; + // TODO: It seems not okay + //min-height: 100vh; &__content { display: flex; diff --git a/src/navigation/containers/Layout/Layout.tsx b/src/gravity-blocks/navigation/containers/Layout/Layout.tsx similarity index 80% rename from src/navigation/containers/Layout/Layout.tsx rename to src/gravity-blocks/navigation/containers/Layout/Layout.tsx index 03a7c19fdc..2f47ff1daa 100644 --- a/src/navigation/containers/Layout/Layout.tsx +++ b/src/gravity-blocks/navigation/containers/Layout/Layout.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import {NavigationData} from '../../../models'; -import {block} from '../../../utils'; +import {NavigationData} from '../../../../models'; +import {block, isHeaderSet, isLogoSet} from '../../../../utils'; import Navigation from '../../components/Navigation/Navigation'; import './Layout.scss'; @@ -16,6 +16,7 @@ export interface LayoutProps { const Layout = ({children, navigation}: React.PropsWithChildren<LayoutProps>) => ( <div className={b()}> {navigation && + (isLogoSet(navigation.logo) || isHeaderSet(navigation.header)) && (navigation.renderNavigation ? ( navigation.renderNavigation() ) : ( diff --git a/src/navigation/hooks/index.ts b/src/gravity-blocks/navigation/hooks/index.ts similarity index 100% rename from src/navigation/hooks/index.ts rename to src/gravity-blocks/navigation/hooks/index.ts diff --git a/src/navigation/hooks/useActiveNavItem.ts b/src/gravity-blocks/navigation/hooks/useActiveNavItem.ts similarity index 94% rename from src/navigation/hooks/useActiveNavItem.ts rename to src/gravity-blocks/navigation/hooks/useActiveNavItem.ts index ce4001338d..5b391c638b 100644 --- a/src/navigation/hooks/useActiveNavItem.ts +++ b/src/gravity-blocks/navigation/hooks/useActiveNavItem.ts @@ -1,6 +1,6 @@ import * as React from 'react'; -import {NavigationItemModel} from '../../models'; +import {NavigationItemModel} from '../../../models'; import {getNavigationItemWithIconSize} from '../utils'; const useActiveNavItem = ( diff --git a/src/navigation/hooks/useShowBorder.ts b/src/gravity-blocks/navigation/hooks/useShowBorder.ts similarity index 100% rename from src/navigation/hooks/useShowBorder.ts rename to src/gravity-blocks/navigation/hooks/useShowBorder.ts diff --git a/src/navigation/index.ts b/src/gravity-blocks/navigation/index.ts similarity index 100% rename from src/navigation/index.ts rename to src/gravity-blocks/navigation/index.ts diff --git a/src/navigation/models.ts b/src/gravity-blocks/navigation/models.ts similarity index 97% rename from src/navigation/models.ts rename to src/gravity-blocks/navigation/models.ts index 010db51bee..9088bf1807 100644 --- a/src/navigation/models.ts +++ b/src/gravity-blocks/navigation/models.ts @@ -6,7 +6,7 @@ import { NavigationItemModel, NavigationLinkItem, ThemedNavigationLogoData, -} from '../models'; +} from '../../models'; export interface MobileMenuButtonProps { isSidebarOpened: boolean; @@ -63,7 +63,7 @@ export interface ItemsWrapperProps ClassNameProps {} export interface DesktopNavigationProps extends MobileMenuButtonProps, ActiveItemProps { - logo: ThemedNavigationLogoData; + logo?: ThemedNavigationLogoData; leftItemsWithIconSize: NavigationItemModel[]; rightItemsWithIconSize?: NavigationItemModel[]; customMobileHeaderItems?: NavigationItemModel[]; diff --git a/src/navigation/schema.ts b/src/gravity-blocks/navigation/schema.ts similarity index 83% rename from src/navigation/schema.ts rename to src/gravity-blocks/navigation/schema.ts index 5c809d633e..b1ef3e4da5 100644 --- a/src/navigation/schema.ts +++ b/src/gravity-blocks/navigation/schema.ts @@ -1,6 +1,6 @@ import omit from 'lodash/omit'; -import {ImageProps, imageUrlPattern} from '../components/Image/schema'; +import {ImageProps, imageUrlPattern} from '../../components/Image/schema'; import {ButtonProps} from '../schema/validators/common'; import {filteredArray} from '../schema/validators/utils'; @@ -86,9 +86,18 @@ const NavigationDropdownItemProps = { const NavigationItemProps = { oneOf: [ - filteredArray(NavigationLinkItemProps), - filteredArray(NavigationButtonItemProps), - filteredArray(NavigationDropdownItemProps), + { + optionName: 'link', + ...filteredArray(NavigationLinkItemProps), + }, + { + optionName: 'button', + ...filteredArray(NavigationButtonItemProps), + }, + { + optionName: 'dropdown', + ...filteredArray(NavigationDropdownItemProps), + }, ], }; diff --git a/src/navigation/utils.ts b/src/gravity-blocks/navigation/utils.ts similarity index 98% rename from src/navigation/utils.ts rename to src/gravity-blocks/navigation/utils.ts index 751b023733..43575e3332 100644 --- a/src/navigation/utils.ts +++ b/src/gravity-blocks/navigation/utils.ts @@ -7,7 +7,7 @@ import { NavigationItemModel, NavigationItemType, NavigationLinkItem, -} from '../models'; +} from '../../models'; import {ItemColumnName} from './models'; diff --git a/src/schema/constants.ts b/src/gravity-blocks/schema/constants.ts similarity index 98% rename from src/schema/constants.ts rename to src/gravity-blocks/schema/constants.ts index c16ba4b98c..1552a53e29 100644 --- a/src/schema/constants.ts +++ b/src/gravity-blocks/schema/constants.ts @@ -1,4 +1,4 @@ -import {BlockType} from '../models'; +import {BlockType} from '../../models'; import { BannerBlock, diff --git a/src/schema/index.ts b/src/gravity-blocks/schema/index.ts similarity index 89% rename from src/schema/index.ts rename to src/gravity-blocks/schema/index.ts index c7f90d8b9a..42f16a0b76 100644 --- a/src/schema/index.ts +++ b/src/gravity-blocks/schema/index.ts @@ -33,6 +33,17 @@ export const getBlocksCases = (blocks: Schema) => { ); }; +export const defaultComponentsConfigurationSchema = { + type: 'object', + properties: { + ...AnimatableProps, + logo: withTheme(LogoProps), + header: NavigationHeaderProps, + menu: MenuProps, + background: withTheme(BackgroundProps), + }, +}; + export function generateDefaultSchema(config?: SchemaCustomConfig) { const {cards = {}, blocks = {}, extensions = {}} = config ?? {}; @@ -82,17 +93,13 @@ export function generateDefaultSchema(config?: SchemaCustomConfig) { additionalProperties: false, required: ['blocks'], properties: { - ...AnimatableProps, - logo: withTheme(LogoProps), - header: NavigationHeaderProps, + ...defaultComponentsConfigurationSchema.properties, blocks: { type: 'array', items: { $ref: '#/definitions/children', }, }, - menu: MenuProps, - background: withTheme(BackgroundProps), ...extensions, }, } as Schema; diff --git a/src/gravity-blocks/schema/validators/blocks.ts b/src/gravity-blocks/schema/validators/blocks.ts new file mode 100644 index 0000000000..396c09da5b --- /dev/null +++ b/src/gravity-blocks/schema/validators/blocks.ts @@ -0,0 +1,23 @@ +export * from '../../../blocks/Banner/schema'; +export * from '../../../blocks/Companies/schema'; +export * from '../../../blocks/ExtendedFeatures/schema'; +export * from '../../../blocks/PromoFeaturesBlock/schema'; +export * from '../../../blocks/Header/schema'; +export * from '../../../blocks/Hero/schema'; +export * from '../../../blocks/Info/schema'; +export * from '../../../blocks/Media/schema'; +export * from '../../../blocks/Map/schema'; +export * from '../../../blocks/Questions/schema'; +export * from '../../../blocks/FoldableList/schema'; +export * from '../../../blocks/Slider/schema'; +export * from '../../../blocks/SliderOld/schema'; +export * from '../../../blocks/Table/schema'; +export * from '../../../blocks/Tabs/schema'; +export * from '../../../blocks/HeaderSlider/schema'; +export * from '../../../blocks/Icons/schema'; +export * from '../../../blocks/CardLayout/schema'; +export * from '../../../blocks/ContentLayout/schema'; +export * from '../../../blocks/Share/schema'; +export * from '../../../blocks/FilterBlock/schema'; +export * from '../../../blocks/Form/schema'; +export * from '../../../blocks/Slider/schema'; diff --git a/src/schema/validators/common.ts b/src/gravity-blocks/schema/validators/common.ts similarity index 99% rename from src/schema/validators/common.ts rename to src/gravity-blocks/schema/validators/common.ts index 64cd066a68..a2b3aa3cdb 100644 --- a/src/schema/validators/common.ts +++ b/src/gravity-blocks/schema/validators/common.ts @@ -1,4 +1,4 @@ -import {ImageProps} from '../../components/Image/schema'; +import {ImageProps} from '../../../components/Image/schema'; import { CustomControlsButtonPositioning, CustomControlsType, @@ -6,7 +6,7 @@ import { MediaVideoControlsType, QuoteType, Theme, -} from '../../models'; +} from '../../../models'; import {AnalyticsEventSchema} from './event'; diff --git a/src/gravity-blocks/schema/validators/components.ts b/src/gravity-blocks/schema/validators/components.ts new file mode 100644 index 0000000000..94e62ae7c1 --- /dev/null +++ b/src/gravity-blocks/schema/validators/components.ts @@ -0,0 +1,3 @@ +export * from '../../../components/Author/schema'; +export * from '../../../components/Image/schema'; +export * from '../../../components/YandexForm/schema'; diff --git a/src/schema/validators/event.ts b/src/gravity-blocks/schema/validators/event.ts similarity index 100% rename from src/schema/validators/event.ts rename to src/gravity-blocks/schema/validators/event.ts diff --git a/src/schema/validators/index.ts b/src/gravity-blocks/schema/validators/index.ts similarity index 100% rename from src/schema/validators/index.ts rename to src/gravity-blocks/schema/validators/index.ts diff --git a/src/schema/validators/navigation.ts b/src/gravity-blocks/schema/validators/navigation.ts similarity index 100% rename from src/schema/validators/navigation.ts rename to src/gravity-blocks/schema/validators/navigation.ts diff --git a/src/gravity-blocks/schema/validators/sub-blocks.ts b/src/gravity-blocks/schema/validators/sub-blocks.ts new file mode 100644 index 0000000000..9d698bd316 --- /dev/null +++ b/src/gravity-blocks/schema/validators/sub-blocks.ts @@ -0,0 +1,11 @@ +export * from '../../../sub-blocks/PriceDetailed/schema'; +export * from '../../../sub-blocks/BackgroundCard/schema'; +export * from '../../../sub-blocks/Content/schema'; +export * from '../../../sub-blocks/MediaCard/schema'; +export * from '../../../sub-blocks/LayoutItem/schema'; +export * from '../../../sub-blocks/Quote/schema'; +export * from '../../../sub-blocks/Divider/schema'; +export * from '../../../sub-blocks/BasicCard/schema'; +export * from '../../../sub-blocks/PriceCard/schema'; +export * from '../../../sub-blocks/HubspotForm/schema'; +export * from '../../../sub-blocks/ImageCard/schema'; diff --git a/src/schema/validators/utils.ts b/src/gravity-blocks/schema/validators/utils.ts similarity index 100% rename from src/schema/validators/utils.ts rename to src/gravity-blocks/schema/validators/utils.ts diff --git a/src/text-transform/common.ts b/src/gravity-blocks/text-transform/common.ts similarity index 100% rename from src/text-transform/common.ts rename to src/gravity-blocks/text-transform/common.ts diff --git a/src/text-transform/config.ts b/src/gravity-blocks/text-transform/config.ts similarity index 99% rename from src/text-transform/config.ts rename to src/gravity-blocks/text-transform/config.ts index e7948a2ae7..4140ba1afe 100644 --- a/src/text-transform/config.ts +++ b/src/gravity-blocks/text-transform/config.ts @@ -12,7 +12,7 @@ import { SubBlockType, TableProps, TitleItemProps, -} from '../models'; +} from '../../models'; import { Parser, diff --git a/src/text-transform/filter.ts b/src/gravity-blocks/text-transform/filter.ts similarity index 100% rename from src/text-transform/filter.ts rename to src/gravity-blocks/text-transform/filter.ts diff --git a/src/text-transform/index.ts b/src/gravity-blocks/text-transform/index.ts similarity index 100% rename from src/text-transform/index.ts rename to src/gravity-blocks/text-transform/index.ts diff --git a/src/text-transform/transformers.ts b/src/gravity-blocks/text-transform/transformers.ts similarity index 97% rename from src/text-transform/transformers.ts rename to src/gravity-blocks/text-transform/transformers.ts index 4026ecba85..0e32478dbd 100644 --- a/src/text-transform/transformers.ts +++ b/src/gravity-blocks/text-transform/transformers.ts @@ -4,7 +4,7 @@ import {MarkdownItPluginCb} from '@diplodoc/transform/lib/plugins/typings'; import cloneDeep from 'lodash/cloneDeep'; import shuffle from 'lodash/shuffle'; -import {ConstructorBlock, PageContent} from '../models/constructor'; +import {ConstructorBlock, PageContent} from '../../models/constructor'; import {Transformer} from './common'; import {BlocksConfig, config} from './config'; diff --git a/src/text-transform/types.ts b/src/gravity-blocks/text-transform/types.ts similarity index 100% rename from src/text-transform/types.ts rename to src/gravity-blocks/text-transform/types.ts diff --git a/src/text-transform/utils.ts b/src/gravity-blocks/text-transform/utils.ts similarity index 100% rename from src/text-transform/utils.ts rename to src/gravity-blocks/text-transform/utils.ts diff --git a/src/hooks/usePCEditorBlockRegister.ts b/src/hooks/usePCEditorBlockRegister.ts new file mode 100644 index 0000000000..4d395508eb --- /dev/null +++ b/src/hooks/usePCEditorBlockRegister.ts @@ -0,0 +1,41 @@ +import * as React from 'react'; + +import {BlockRegistryContext, pathKey} from '../context/blockRegistryContext'; + +export function usePCEditorBlockRegister(path: number[], dropZone?: boolean) { + const registry = React.useContext(BlockRegistryContext); + const elementRef = React.useRef<HTMLElement | null>(null); + const observerRef = React.useRef<ResizeObserver | null>(null); + const key = React.useMemo(() => pathKey(path), [path]); + + const blockRef = React.useCallback( + (node: HTMLElement | null) => { + if (elementRef.current === node) { + return; + } + + if (elementRef.current && registry) { + registry.unregister(key); + } + + observerRef.current?.disconnect(); + observerRef.current = null; + elementRef.current = node; + + if (node && registry) { + registry.register(key, path, node, dropZone); + + const observer = new ResizeObserver(() => { + registry.register(key, path, node, dropZone); + }); + observer.observe(node); + observerRef.current = observer; + } + }, + // `path` is stable per render but identity may change; `key` captures content. + // eslint-disable-next-line react-hooks/exhaustive-deps + [registry, key, dropZone], + ); + + return blockRef; +} diff --git a/src/hooks/usePCEditorChildrenItemWrap.ts b/src/hooks/usePCEditorChildrenItemWrap.ts new file mode 100644 index 0000000000..1064e384c7 --- /dev/null +++ b/src/hooks/usePCEditorChildrenItemWrap.ts @@ -0,0 +1,13 @@ +import * as React from 'react'; + +import {BlockIdContext} from '../context/blockIdContext'; + +import {usePCEditorBlockRegister} from './usePCEditorBlockRegister'; + +export function usePCEditorChildrenItemWrap(index = 0) { + const parentBlockId = React.useContext(BlockIdContext); + const path = React.useMemo(() => [...parentBlockId, index], [parentBlockId, index]); + const blockRef = usePCEditorBlockRegister(path); + + return {blockRef, path}; +} diff --git a/src/hooks/usePCEditorInitializeEvents.ts b/src/hooks/usePCEditorInitializeEvents.ts new file mode 100644 index 0000000000..3cd4c58329 --- /dev/null +++ b/src/hooks/usePCEditorInitializeEvents.ts @@ -0,0 +1,125 @@ +import * as React from 'react'; + +import _ from 'lodash'; + +import {toSerializableRect} from '../common/types/rect'; +import {BlockData} from '../constructor-items'; +import {BlockRegistry} from '../context/blockRegistryContext'; +import {Fields} from '../form-generator-v2/types'; +import {PageContent} from '../models'; + +import {usePCEditorStore} from './usePCEditorStore'; +import {sendEventPostMessage, useInternalPostMessageAPIListener} from './usePostMessageAPI'; + +interface UseEditorInitializeProps { + initialContent: PageContent; + setContent: (content: PageContent) => void; + blocks: Array<BlockData>; + global?: Fields; + blockInputs?: Fields; + registry: BlockRegistry | null; +} + +function collectRectMap(registry: BlockRegistry) { + return registry.getEntries().map(({path, element, dropZone}) => { + const rect = element.getClientRects().item(0) ?? element.getBoundingClientRect(); + return {path, rect: toSerializableRect(rect), dropZone}; + }); +} + +export const usePCEditorInitializeEvents = ({ + initialContent, + setContent, + blocks, + global, + blockInputs, + registry, +}: UseEditorInitializeProps) => { + const {initialized, content} = usePCEditorStore(); + + React.useEffect(() => { + if (initialized) { + setContent(content); + } + }, [content, initialized, setContent]); + + useInternalPostMessageAPIListener('GET_INITIAL_CONTENT', () => { + sendEventPostMessage('ON_INITIAL_CONTENT', initialContent); + }); + + useInternalPostMessageAPIListener('GET_SUPPORTED_BLOCKS', () => { + sendEventPostMessage('ON_SUPPORTED_BLOCKS', { + blocks: blocks.map((block) => ({ + type: block.type, + schema: blockInputs?.length + ? { + ...block.schema, + inputs: [...blockInputs, ...(block.schema?.inputs || [])], + } + : block.schema, + })), + subBlocks: [], + global: global || [], + }); + }); + + const onResize = React.useCallback(() => { + const height = document.body.scrollHeight; + sendEventPostMessage('ON_RESIZE', {height}); + }, []); + + React.useEffect(() => { + if (!registry) { + return undefined; + } + + let frame: number | null = null; + + const sendRectMap = () => { + frame = null; + sendEventPostMessage('ON_UPDATE_RECT_MAP', {rects: collectRectMap(registry)}); + }; + + const scheduleSend = () => { + if (frame !== null) { + return; + } + frame = requestAnimationFrame(sendRectMap); + }; + + const throttledSchedule = _.throttle(scheduleSend, 100, {leading: true, trailing: true}); + + const unsubscribe = registry.subscribe(throttledSchedule); + const observer = new ResizeObserver(throttledSchedule); + observer.observe(document.body); + + // Initial push once registry is ready. + throttledSchedule(); + + return () => { + throttledSchedule.cancel(); + unsubscribe(); + observer.disconnect(); + if (frame !== null) { + cancelAnimationFrame(frame); + } + }; + }, [registry]); + + React.useEffect(() => { + window.addEventListener('resize', onResize); + const observer = new ResizeObserver(onResize); + observer.observe(document.documentElement); + observer.observe(document.body); + + return () => { + window.removeEventListener('resize', onResize); + observer.disconnect(); + }; + }, [onResize]); + + React.useEffect(() => { + const height = document.body.scrollHeight; + sendEventPostMessage('ON_INIT', {height}); + }, []); +}; diff --git a/src/hooks/usePCEditorStore.ts b/src/hooks/usePCEditorStore.ts new file mode 100644 index 0000000000..6e70148c94 --- /dev/null +++ b/src/hooks/usePCEditorStore.ts @@ -0,0 +1,10 @@ +import * as React from 'react'; + +import {useStore} from 'zustand'; + +import {PCEditorStoreContext} from '../context/editorStoreContext'; + +export const usePCEditorStore = () => { + const {state} = React.useContext(PCEditorStoreContext); + return useStore(state); +}; diff --git a/src/hooks/usePostMessageAPI.ts b/src/hooks/usePostMessageAPI.ts new file mode 100644 index 0000000000..efb3621ab8 --- /dev/null +++ b/src/hooks/usePostMessageAPI.ts @@ -0,0 +1,44 @@ +import * as React from 'react'; + +import {POST_MESSAGE_SOURCE} from '../common/constants'; +import {isValidPostMessage} from '../common/postMessage'; +import {PostMessageAPIMessage} from '../common/types'; +import {ActionMessageTypes, EventMessageTypes} from '../common/types/actions'; + +export function sendEventPostMessage<K extends keyof EventMessageTypes>( + action: K, + data: EventMessageTypes[K], +) { + const message = {action, data, source: POST_MESSAGE_SOURCE} as PostMessageAPIMessage<K>; + window.parent.postMessage(message, '*'); +} + +export function listenPostMessageActions<K extends keyof ActionMessageTypes>( + action: K, + callback: (data: ActionMessageTypes[K]) => void, +) { + const onMessage = (e: MessageEvent) => { + if (!isValidPostMessage(e.data)) { + return undefined; + } + + const message = e.data as PostMessageAPIMessage<K>; + if (message.action === action) { + return callback(message.data); + } + + return undefined; + }; + + window.addEventListener('message', onMessage); + return () => window.removeEventListener('message', onMessage); +} + +export function useInternalPostMessageAPIListener<K extends keyof ActionMessageTypes>( + action: K, + callback: (data: ActionMessageTypes[K]) => void, +) { + React.useEffect(() => { + return listenPostMessageActions(action, callback); + }, [action, callback]); +} diff --git a/src/hooks/useWindowBreakpoint.ts b/src/hooks/useWindowBreakpoint.ts deleted file mode 100644 index 6a1e8d4a40..0000000000 --- a/src/hooks/useWindowBreakpoint.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from 'react'; - -import debounce from 'lodash/debounce'; - -import {BREAKPOINTS} from '../constants'; - -function calculate(windowWidth: number) { - const breakpointsSorted = Object.values(BREAKPOINTS).sort((b1, b2) => b1 - b2); - - let result = breakpointsSorted[0]; - - for (const breakpoint of breakpointsSorted) { - if (windowWidth >= breakpoint) { - result = breakpoint; - } else { - return result; - } - } - - return result; -} - -export default function useWindowBreakpoint() { - const [breakpoint, setBreakpoint] = React.useState(BREAKPOINTS.sm); - - React.useEffect(() => { - setBreakpoint(calculate(window.innerWidth)); - - const detect = debounce(() => { - setBreakpoint(calculate(window.innerWidth)); - }, 100); - - detect(); - - window.addEventListener('resize', detect, {passive: true}); - - return () => window.removeEventListener('resize', detect); - }, []); - - return breakpoint; -} diff --git a/src/index.ts b/src/index.ts index 10becb250d..4af8263629 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,16 @@ //storybook -export * from './context/theme'; -export * from './context/analyticsContext'; +export * from './gravity-blocks/context/theme'; +export * from './gravity-blocks/context/analyticsContext'; export * from './containers/PageConstructor'; -export * from './grid'; +export * from './gravity-blocks/grid'; export * from './blocks'; export * from './sub-blocks'; export * from './components'; export * from './models'; export * from './utils'; -export * from './schema'; -export * from './hooks'; -export * from './navigation'; +export * from './gravity-blocks/schema'; +export * from './gravity-blocks/hooks'; +export * from './gravity-blocks/navigation'; export {BREAKPOINTS} from './constants'; diff --git a/src/models/constructor-items/blocks.ts b/src/models/constructor-items/blocks.ts index 574867b639..19c60a1125 100644 --- a/src/models/constructor-items/blocks.ts +++ b/src/models/constructor-items/blocks.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import {ButtonSize} from '@gravity-ui/uikit'; -import {GridColumnSize, GridColumnSizesType, IndentValue} from '../../grid/types'; +import {GridColumnSize, GridColumnSizesType, IndentValue} from '../../gravity-blocks/grid/types'; import {ThemeSupporting} from '../../utils'; import {DeviceSupporting} from '../../utils/breakpoint'; import {AnalyticsEventsBase, AnalyticsEventsProp} from '../common'; diff --git a/src/models/constructor.ts b/src/models/constructor.ts index 68d1cca120..41b1ac980f 100644 --- a/src/models/constructor.ts +++ b/src/models/constructor.ts @@ -2,23 +2,23 @@ import * as React from 'react'; import type {BlockBaseProps} from './constructor-items/blocks'; -import {Animatable, BlockDecorationProps, ConstructorItem, ThemedMediaProps} from './'; +import {Animatable, ConstructorItem} from './'; export interface PageData { content: PageContent; } -export interface Menu { - title: string; -} - export type ConstructorBlock = (ConstructorItem | CustomBlock) & Partial<Pick<BlockBaseProps, 'visible'>>; +/** + * Core PageContent type - minimal fields that the engine needs. + * Plugins can extend this with their own fields using the index signature. + */ export interface PageContent extends Animatable { blocks: ConstructorBlock[]; - menu?: Menu; - background?: ThemedMediaProps; + // Allow plugin-specific fields to pass through + [key: string]: unknown; } export interface InitConstrucorState { @@ -62,7 +62,4 @@ export interface CustomConfig { headers?: CustomItems; navigation?: CustomItems; loadable?: LoadableConfig; - decorators?: { - block?: ((props: BlockDecorationProps) => React.ReactElement)[]; - }; } diff --git a/src/models/customization.ts b/src/models/customization.ts index 3c7fbfe2d6..a18b913325 100644 --- a/src/models/customization.ts +++ b/src/models/customization.ts @@ -1,9 +1,25 @@ import * as React from 'react'; -import {BlockBaseProps, BlockType} from './constructor-items'; +import {BlockType, ConstructorItem} from './constructor-items'; -export interface BlockDecorationProps extends React.PropsWithChildren, BlockBaseProps { - type: BlockType | string; +export interface BlockWrapperDataProps<T = object> { + type: string; index?: number; + props?: T; + content?: ConstructorItem & T; } + +/** + * @deprecated Use BlockWrapperDataProps instead. + * BlockDecorationProps will be removed in the next major version. + */ +export interface BlockDecorationProps + extends React.PropsWithChildren, + Omit<BlockWrapperDataProps, 'type'> { + type: BlockType | string; +} + +/** + * @deprecated Use PageConstructorExtension with blockWrapper instead. + */ export type BlockDecorator = (props: BlockDecorationProps) => React.ReactElement; diff --git a/src/models/navigation.ts b/src/models/navigation.ts index 72f51da19b..613855bf71 100644 --- a/src/models/navigation.ts +++ b/src/models/navigation.ts @@ -121,8 +121,8 @@ export interface FooterData { } export interface NavigationData { - logo: ThemedNavigationLogoData; - header: HeaderData; + logo?: ThemedNavigationLogoData; + header?: HeaderData; footer?: FooterData; renderNavigation?: () => React.ReactNode; } diff --git a/src/schema/validators/blocks.ts b/src/schema/validators/blocks.ts deleted file mode 100644 index d5ef4fc095..0000000000 --- a/src/schema/validators/blocks.ts +++ /dev/null @@ -1,23 +0,0 @@ -export * from '../../blocks/Banner/schema'; -export * from '../../blocks/Companies/schema'; -export * from '../../blocks/ExtendedFeatures/schema'; -export * from '../../blocks/PromoFeaturesBlock/schema'; -export * from '../../blocks/Header/schema'; -export * from '../../blocks/Hero/schema'; -export * from '../../blocks/Info/schema'; -export * from '../../blocks/Media/schema'; -export * from '../../blocks/Map/schema'; -export * from '../../blocks/Questions/schema'; -export * from '../../blocks/FoldableList/schema'; -export * from '../../blocks/Slider/schema'; -export * from '../../blocks/SliderOld/schema'; -export * from '../../blocks/Table/schema'; -export * from '../../blocks/Tabs/schema'; -export * from '../../blocks/HeaderSlider/schema'; -export * from '../../blocks/Icons/schema'; -export * from '../../blocks/CardLayout/schema'; -export * from '../../blocks/ContentLayout/schema'; -export * from '../../blocks/Share/schema'; -export * from '../../blocks/FilterBlock/schema'; -export * from '../../blocks/Form/schema'; -export * from '../../blocks/Slider/schema'; diff --git a/src/schema/validators/components.ts b/src/schema/validators/components.ts deleted file mode 100644 index e92915cf5c..0000000000 --- a/src/schema/validators/components.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from '../../components/Author/schema'; -export * from '../../components/Image/schema'; -export * from '../../components/YandexForm/schema'; diff --git a/src/schema/validators/sub-blocks.ts b/src/schema/validators/sub-blocks.ts deleted file mode 100644 index b9177959e9..0000000000 --- a/src/schema/validators/sub-blocks.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from '../../sub-blocks/PriceDetailed/schema'; -export * from '../../sub-blocks/BackgroundCard/schema'; -export * from '../../sub-blocks/Content/schema'; -export * from '../../sub-blocks/MediaCard/schema'; -export * from '../../sub-blocks/LayoutItem/schema'; -export * from '../../sub-blocks/Quote/schema'; -export * from '../../sub-blocks/Divider/schema'; -export * from '../../sub-blocks/BasicCard/schema'; -export * from '../../sub-blocks/PriceCard/schema'; -export * from '../../sub-blocks/HubspotForm/schema'; -export * from '../../sub-blocks/ImageCard/schema'; diff --git a/src/server.ts b/src/server.ts index 18b3f182b7..5da6c6ba31 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1 +1 @@ -export * from './text-transform'; +export * from './gravity-blocks/text-transform'; diff --git a/src/sub-blocks/BackgroundCard/BackgroundCard.tsx b/src/sub-blocks/BackgroundCard/BackgroundCard.tsx index 209cd9b4a8..46989beb87 100644 --- a/src/sub-blocks/BackgroundCard/BackgroundCard.tsx +++ b/src/sub-blocks/BackgroundCard/BackgroundCard.tsx @@ -1,7 +1,7 @@ import {useUniqId} from '@gravity-ui/uikit'; import {BackgroundImage, CardBase} from '../../components/'; -import {useTheme} from '../../context/theme'; +import {useTheme} from '../../gravity-blocks/context/theme'; import {BackgroundCardProps} from '../../models'; import {block, getThemedValue} from '../../utils'; import Content from '../Content/Content'; diff --git a/src/sub-blocks/BackgroundCard/__stories__/BackgroundCard.stories.tsx b/src/sub-blocks/BackgroundCard/__stories__/BackgroundCard.stories.tsx index 3908a788c1..2694697234 100644 --- a/src/sub-blocks/BackgroundCard/__stories__/BackgroundCard.stories.tsx +++ b/src/sub-blocks/BackgroundCard/__stories__/BackgroundCard.stories.tsx @@ -4,9 +4,10 @@ import {blockTransform} from '../../../../.storybook/utils'; import CardLayout, {CardLayoutBlockProps} from '../../../blocks/CardLayout/CardLayout'; import {BlockBase} from '../../../components'; import {ConstructorRow} from '../../../containers/PageConstructor/components/ConstructorRow'; -import {Grid} from '../../../grid'; +import {Grid} from '../../../gravity-blocks/grid'; import {BackgroundCardModel, BackgroundCardProps, CardLayoutBlockModel} from '../../../models'; import BackgroundCard from '../BackgroundCard'; +import {form} from '../form'; import data from './data.json'; @@ -22,6 +23,9 @@ export default { options: [undefined, 'dark', 'light'], }, }, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<BackgroundCardModel> = (args) => ( diff --git a/src/sub-blocks/BackgroundCard/form.ts b/src/sub-blocks/BackgroundCard/form.ts new file mode 100644 index 0000000000..7fb09f813c --- /dev/null +++ b/src/sub-blocks/BackgroundCard/form.ts @@ -0,0 +1,17 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {BackgroundCard as BackgroundCardSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + BackgroundCardSchema['background-card'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + title: 'Background Card', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + additionalInfo: 'Additional info', + backgroundColor: '#F0F0F0', +}; diff --git a/src/sub-blocks/BackgroundCard/icon.ts b/src/sub-blocks/BackgroundCard/icon.ts new file mode 100644 index 0000000000..e8fdcdc859 --- /dev/null +++ b/src/sub-blocks/BackgroundCard/icon.ts @@ -0,0 +1,11 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<rect x="37" y="10.2906" width="26" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="37" y="10.2906" width="26" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="40.3488" y="28.593" width="19.3023" height="2" rx="0.55814" fill="#262626"/> +<rect x="40.3488" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +</svg>`, +); diff --git a/src/sub-blocks/BackgroundCard/index.tsx b/src/sub-blocks/BackgroundCard/index.tsx new file mode 100644 index 0000000000..96fa539f21 --- /dev/null +++ b/src/sub-blocks/BackgroundCard/index.tsx @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import BackgroundCard from './BackgroundCard'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const BackgroundCardConfig: BlockData = { + type: '@gravity-ui/page-constructor/background-card', + component: BackgroundCard, + schema: { + name: 'Background Card', + group: '@gravity-ui/page-constructor/Cards', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default BackgroundCardConfig; diff --git a/src/sub-blocks/BackgroundCard/index_deprecated.tsx b/src/sub-blocks/BackgroundCard/index_deprecated.tsx new file mode 100644 index 0000000000..26cf3f87f9 --- /dev/null +++ b/src/sub-blocks/BackgroundCard/index_deprecated.tsx @@ -0,0 +1,20 @@ +import {BlockData} from '../../constructor-items'; + +import BackgroundCard from './BackgroundCard'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const BackgroundCardConfig: BlockData = { + type: 'background-card', + component: BackgroundCard, + schema: { + name: 'Background Card', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default BackgroundCardConfig; diff --git a/src/sub-blocks/BackgroundCard/schema.ts b/src/sub-blocks/BackgroundCard/schema.ts index b3b2557925..05d7a7195a 100644 --- a/src/sub-blocks/BackgroundCard/schema.ts +++ b/src/sub-blocks/BackgroundCard/schema.ts @@ -7,8 +7,8 @@ import { CardLayoutProps, containerSizesObject, withTheme, -} from '../../schema/validators/common'; -import {AnalyticsEventSchema} from '../../schema/validators/event'; +} from '../../gravity-blocks/schema/validators/common'; +import {AnalyticsEventSchema} from '../../gravity-blocks/schema/validators/event'; import {ContentBase} from '../Content/schema'; const BackgroundCardContentProps = omit(ContentBase, ['controlPosition']); diff --git a/src/sub-blocks/BannerCard/BannerCard.tsx b/src/sub-blocks/BannerCard/BannerCard.tsx index 75da986611..cc9fe8f4d9 100644 --- a/src/sub-blocks/BannerCard/BannerCard.tsx +++ b/src/sub-blocks/BannerCard/BannerCard.tsx @@ -1,5 +1,5 @@ import {BackgroundImage, Button, RouterLink, YFMWrapper} from '../../components'; -import {useTheme} from '../../context/theme'; +import {useTheme} from '../../gravity-blocks/context/theme'; import {BannerCardProps} from '../../models'; import {block, getThemedValue} from '../../utils'; @@ -11,7 +11,7 @@ export const BannerCard = (props: BannerCardProps) => { const { title, subtitle, - button: {url, text, target, theme: buttonTheme = 'raised'} = {}, + button, color, theme: textTheme = 'light', image, @@ -21,6 +21,8 @@ export const BannerCard = (props: BannerCardProps) => { const theme = useTheme(); const contentStyle: Record<string, string> = {}; + const {url, text, target, theme: buttonTheme = 'raised'} = button || {}; + if (color) { contentStyle.backgroundColor = getThemedValue(color, theme); } diff --git a/src/sub-blocks/BannerCard/form.ts b/src/sub-blocks/BannerCard/form.ts new file mode 100644 index 0000000000..ad5cfc5177 --- /dev/null +++ b/src/sub-blocks/BannerCard/form.ts @@ -0,0 +1,18 @@ +import {JSONSchemaType} from 'ajv'; + +import {BannerCardProps} from '../../blocks/Banner/schema'; +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + BannerCardProps as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + color: 'rgba(54, 151, 241, 0.4)', + title: 'Banner Card', + subtitle: 'Some sort of description.', + button: { + text: 'Read more', + }, +}; diff --git a/src/sub-blocks/BannerCard/icon.ts b/src/sub-blocks/BannerCard/icon.ts new file mode 100644 index 0000000000..e8fdcdc859 --- /dev/null +++ b/src/sub-blocks/BannerCard/icon.ts @@ -0,0 +1,11 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<rect x="37" y="10.2906" width="26" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="37" y="10.2906" width="26" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="40.3488" y="28.593" width="19.3023" height="2" rx="0.55814" fill="#262626"/> +<rect x="40.3488" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +</svg>`, +); diff --git a/src/sub-blocks/BannerCard/index.tsx b/src/sub-blocks/BannerCard/index.tsx new file mode 100644 index 0000000000..764dbfd683 --- /dev/null +++ b/src/sub-blocks/BannerCard/index.tsx @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import BannerCard from './BannerCard'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const BannerCardConfig: BlockData = { + type: '@gravity-ui/page-constructor/banner-card', + component: BannerCard, + schema: { + name: 'Banner Card', + group: '@gravity-ui/page-constructor/Cards', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default BannerCardConfig; diff --git a/src/sub-blocks/BannerCard/index_deprecated.ts b/src/sub-blocks/BannerCard/index_deprecated.ts new file mode 100644 index 0000000000..7a81593e1a --- /dev/null +++ b/src/sub-blocks/BannerCard/index_deprecated.ts @@ -0,0 +1,20 @@ +import {BlockData} from '../../constructor-items'; + +import BannerCard from './BannerCard'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const BannerCardConfig: BlockData = { + type: 'banner-card', + component: BannerCard, + schema: { + name: 'Banner Card', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default BannerCardConfig; diff --git a/src/sub-blocks/BasicCard/BasicCard.tsx b/src/sub-blocks/BasicCard/BasicCard.tsx index 9125cb6dcc..fc74cb6f2b 100644 --- a/src/sub-blocks/BasicCard/BasicCard.tsx +++ b/src/sub-blocks/BasicCard/BasicCard.tsx @@ -1,7 +1,7 @@ import {useUniqId} from '@gravity-ui/uikit'; import {CardBase, IconWrapper} from '../../components'; -import {useTheme} from '../../context/theme'; +import {useTheme} from '../../gravity-blocks/context/theme'; import {BasicCardProps} from '../../models'; import {IconPosition} from '../../models/constructor-items/sub-blocks'; import {block, getThemedValue} from '../../utils'; diff --git a/src/sub-blocks/BasicCard/__stories__/BasicCard.stories.tsx b/src/sub-blocks/BasicCard/__stories__/BasicCard.stories.tsx index 5fc35e0c58..2f65ab80d0 100644 --- a/src/sub-blocks/BasicCard/__stories__/BasicCard.stories.tsx +++ b/src/sub-blocks/BasicCard/__stories__/BasicCard.stories.tsx @@ -4,10 +4,11 @@ import {blockListTransform, blockTransform} from '../../../../.storybook/utils'; import CardLayout from '../../../blocks/CardLayout/CardLayout'; import {BlockBase} from '../../../components'; import {ConstructorRow} from '../../../containers/PageConstructor/components/ConstructorRow'; -import {Grid} from '../../../grid'; +import {Grid} from '../../../gravity-blocks/grid'; import {BasicCardModel, BasicCardProps, CardLayoutBlockModel} from '../../../models'; import {IconPosition} from '../../../models/constructor-items/sub-blocks'; import BasicCard from '../BasicCard'; +import {form} from '../form'; import data from './data.json'; @@ -20,6 +21,9 @@ const getCardWithIconTitle = (border: string) => export default { component: BasicCard, title: 'Components/Cards/BasicCard', + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<BasicCardModel> = (args) => { diff --git a/src/sub-blocks/BasicCard/form.ts b/src/sub-blocks/BasicCard/form.ts new file mode 100644 index 0000000000..94cda0f77d --- /dev/null +++ b/src/sub-blocks/BasicCard/form.ts @@ -0,0 +1,15 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {BasicCard as BasicCardSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + BasicCardSchema['basic-card'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + title: 'Basic Card', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', +}; diff --git a/src/sub-blocks/BasicCard/icon.ts b/src/sub-blocks/BasicCard/icon.ts new file mode 100644 index 0000000000..e8fdcdc859 --- /dev/null +++ b/src/sub-blocks/BasicCard/icon.ts @@ -0,0 +1,11 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<rect x="37" y="10.2906" width="26" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="37" y="10.2906" width="26" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="40.3488" y="28.593" width="19.3023" height="2" rx="0.55814" fill="#262626"/> +<rect x="40.3488" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +</svg>`, +); diff --git a/src/sub-blocks/BasicCard/index.tsx b/src/sub-blocks/BasicCard/index.tsx new file mode 100644 index 0000000000..6e60c42d7b --- /dev/null +++ b/src/sub-blocks/BasicCard/index.tsx @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import BasicCard from './BasicCard'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const BasicCardConfig: BlockData = { + type: '@gravity-ui/page-constructor/basic-card', + component: BasicCard, + schema: { + name: 'Basic Card', + group: '@gravity-ui/page-constructor/Cards', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default BasicCardConfig; diff --git a/src/sub-blocks/BasicCard/index_deprecated.ts b/src/sub-blocks/BasicCard/index_deprecated.ts new file mode 100644 index 0000000000..0a0bdbe299 --- /dev/null +++ b/src/sub-blocks/BasicCard/index_deprecated.ts @@ -0,0 +1,20 @@ +import {BlockData} from '../../constructor-items'; + +import BasicCard from './BasicCard'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const BasicCardConfig: BlockData = { + type: 'basic-card', + component: BasicCard, + schema: { + name: 'Basic Card', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default BasicCardConfig; diff --git a/src/sub-blocks/BasicCard/schema.ts b/src/sub-blocks/BasicCard/schema.ts index f796bab016..f9a45999ea 100644 --- a/src/sub-blocks/BasicCard/schema.ts +++ b/src/sub-blocks/BasicCard/schema.ts @@ -6,7 +6,7 @@ import { CardBase, CardLayoutProps, GravityIconProps, -} from '../../schema/validators/common'; +} from '../../gravity-blocks/schema/validators/common'; import {ContentBase} from '../Content/schema'; const BasicCardContentProps = omit(ContentBase, ['theme', 'controlPosition']); diff --git a/src/sub-blocks/Content/Content.tsx b/src/sub-blocks/Content/Content.tsx index 9b9238c24e..ea7b812d39 100644 --- a/src/sub-blocks/Content/Content.tsx +++ b/src/sub-blocks/Content/Content.tsx @@ -1,7 +1,7 @@ import {useUniqId} from '@gravity-ui/uikit'; import {Buttons, ContentList, Links, Title, YFMWrapper} from '../../components'; -import {Col} from '../../grid'; +import {Col} from '../../gravity-blocks/grid'; import { ClassNameProps, ContentBlockProps, diff --git a/src/sub-blocks/Content/__stories__/Content.stories.tsx b/src/sub-blocks/Content/__stories__/Content.stories.tsx index 6b16d8df48..0f388b561a 100644 --- a/src/sub-blocks/Content/__stories__/Content.stories.tsx +++ b/src/sub-blocks/Content/__stories__/Content.stories.tsx @@ -5,12 +5,16 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {ContentBlockProps, CustomBlock} from '../../../models'; import Content from '../Content'; +import {form} from '../form'; import data from './data.json'; export default { component: Content, title: 'Components/Content', + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<ContentBlockProps> = (args) => ( diff --git a/src/sub-blocks/Content/__tests__/Content.test.tsx b/src/sub-blocks/Content/__tests__/Content.test.tsx index fd7a1ebfc8..2f2d1bf7d6 100644 --- a/src/sub-blocks/Content/__tests__/Content.test.tsx +++ b/src/sub-blocks/Content/__tests__/Content.test.tsx @@ -12,7 +12,7 @@ import { testContentWithTheme, testContentWithTitle, } from '../../../../test-utils/shared/content'; -import {GridColumnSizesType} from '../../../grid/types'; +import {GridColumnSizesType} from '../../../gravity-blocks/grid/types'; import {ContentSize, ContentTheme} from '../../../models/constructor-items/common'; import {getQaAttrubutes} from '../../../utils/blocks'; import Content, {ContentProps} from '../Content'; diff --git a/src/sub-blocks/Content/form.ts b/src/sub-blocks/Content/form.ts new file mode 100644 index 0000000000..b7ada9879c --- /dev/null +++ b/src/sub-blocks/Content/form.ts @@ -0,0 +1,15 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {ContentBlock} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + ContentBlock['content'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + title: 'Content', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', +}; diff --git a/src/sub-blocks/Content/icon.ts b/src/sub-blocks/Content/icon.ts new file mode 100644 index 0000000000..e8fdcdc859 --- /dev/null +++ b/src/sub-blocks/Content/icon.ts @@ -0,0 +1,11 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<rect x="37" y="10.2906" width="26" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="37" y="10.2906" width="26" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="40.3488" y="28.593" width="19.3023" height="2" rx="0.55814" fill="#262626"/> +<rect x="40.3488" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +</svg>`, +); diff --git a/src/sub-blocks/Content/index.tsx b/src/sub-blocks/Content/index.tsx new file mode 100644 index 0000000000..62878ac996 --- /dev/null +++ b/src/sub-blocks/Content/index.tsx @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import Content from './Content'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const ContentConfig: BlockData = { + type: '@gravity-ui/page-constructor/content', + component: Content, + schema: { + name: 'Content', + group: '@gravity-ui/page-constructor/Cards', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default ContentConfig; diff --git a/src/sub-blocks/Content/index_deprecated.ts b/src/sub-blocks/Content/index_deprecated.ts new file mode 100644 index 0000000000..92b99b90bf --- /dev/null +++ b/src/sub-blocks/Content/index_deprecated.ts @@ -0,0 +1,20 @@ +import {BlockData} from '../../constructor-items'; + +import Content from './Content'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const ContentConfig: BlockData = { + type: 'content', + component: Content, + schema: { + name: 'Content', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default ContentConfig; diff --git a/src/sub-blocks/Content/schema.ts b/src/sub-blocks/Content/schema.ts index e50dbb6a8d..2ed1f68176 100644 --- a/src/sub-blocks/Content/schema.ts +++ b/src/sub-blocks/Content/schema.ts @@ -9,8 +9,8 @@ import { contentThemes, sizeNumber, withTheme, -} from '../../schema/validators/common'; -import {filteredArray} from '../../schema/validators/utils'; +} from '../../gravity-blocks/schema/validators/common'; +import {filteredArray} from '../../gravity-blocks/schema/validators/utils'; export const ContentItem = { additionalProperties: false, @@ -78,6 +78,7 @@ export const ContentBase = { export const ContentBlock = { content: { + type: 'object', additionalProperties: false, properties: { ...ContentBase, diff --git a/src/sub-blocks/Divider/__stories__/Divider.stories.tsx b/src/sub-blocks/Divider/__stories__/Divider.stories.tsx index 3c936088b2..0fd55a7c95 100644 --- a/src/sub-blocks/Divider/__stories__/Divider.stories.tsx +++ b/src/sub-blocks/Divider/__stories__/Divider.stories.tsx @@ -2,6 +2,7 @@ import {Meta, StoryFn} from '@storybook/react'; import {DividerProps} from '../../../models'; import Divider from '../Divider'; +import {form} from '../form'; import data from './data.json'; @@ -10,6 +11,9 @@ import './styles.scss'; export default { component: Divider, title: 'Components/Divider', + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<DividerProps> = (args) => ( diff --git a/src/sub-blocks/Divider/form.ts b/src/sub-blocks/Divider/form.ts new file mode 100644 index 0000000000..599e2b5797 --- /dev/null +++ b/src/sub-blocks/Divider/form.ts @@ -0,0 +1,12 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {Divider as DividerSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + DividerSchema['divider'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = {}; diff --git a/src/sub-blocks/Divider/icon.ts b/src/sub-blocks/Divider/icon.ts new file mode 100644 index 0000000000..e8fdcdc859 --- /dev/null +++ b/src/sub-blocks/Divider/icon.ts @@ -0,0 +1,11 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<rect x="37" y="10.2906" width="26" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="37" y="10.2906" width="26" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="40.3488" y="28.593" width="19.3023" height="2" rx="0.55814" fill="#262626"/> +<rect x="40.3488" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +</svg>`, +); diff --git a/src/sub-blocks/Divider/index.tsx b/src/sub-blocks/Divider/index.tsx new file mode 100644 index 0000000000..8f7a5e4cf3 --- /dev/null +++ b/src/sub-blocks/Divider/index.tsx @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import Divider from './Divider'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const DividerConfig: BlockData = { + type: '@gravity-ui/page-constructor/divider', + component: Divider, + schema: { + name: 'Divider', + group: '@gravity-ui/page-constructor/Cards', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default DividerConfig; diff --git a/src/sub-blocks/Divider/index_deprecated.ts b/src/sub-blocks/Divider/index_deprecated.ts new file mode 100644 index 0000000000..b7db3f6530 --- /dev/null +++ b/src/sub-blocks/Divider/index_deprecated.ts @@ -0,0 +1,20 @@ +import {BlockData} from '../../constructor-items'; + +import Divider from './Divider'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const DividerConfig: BlockData = { + type: 'divider', + component: Divider, + schema: { + name: 'Divider', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default DividerConfig; diff --git a/src/sub-blocks/Divider/schema.ts b/src/sub-blocks/Divider/schema.ts index 44aeb5d430..b0087369c2 100644 --- a/src/sub-blocks/Divider/schema.ts +++ b/src/sub-blocks/Divider/schema.ts @@ -1,4 +1,4 @@ -import {BaseProps, dividerEnum} from '../../schema/validators/common'; +import {BaseProps, dividerEnum} from '../../gravity-blocks/schema/validators/common'; export const Divider = { divider: { diff --git a/src/sub-blocks/HubspotForm/HubspotFormContainer.tsx b/src/sub-blocks/HubspotForm/HubspotFormContainer.tsx index 084e46730f..fd75228353 100644 --- a/src/sub-blocks/HubspotForm/HubspotFormContainer.tsx +++ b/src/sub-blocks/HubspotForm/HubspotFormContainer.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import {useMount} from '../../hooks'; +import {useMount} from '../../gravity-blocks/hooks'; import {HubspotFormProps} from '../../models'; import loadHubspotScript from './loadHubspotScript'; diff --git a/src/sub-blocks/HubspotForm/index.tsx b/src/sub-blocks/HubspotForm/index.tsx index 54a94e431c..f765019313 100644 --- a/src/sub-blocks/HubspotForm/index.tsx +++ b/src/sub-blocks/HubspotForm/index.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import {MobileContext} from '../../context/mobileContext'; -import {useTheme} from '../../context/theme'; -import {useAnalytics, useHandleHubspotEvents} from '../../hooks'; +import {MobileContext} from '../../gravity-blocks/context/mobileContext'; +import {useTheme} from '../../gravity-blocks/context/theme'; +import {useAnalytics, useHandleHubspotEvents} from '../../gravity-blocks/hooks'; import {DefaultEventNames, HubspotFormProps} from '../../models'; import {HubspotEventHandlers, block} from '../../utils'; diff --git a/src/sub-blocks/HubspotForm/schema.ts b/src/sub-blocks/HubspotForm/schema.ts index 9293d7c6e3..46817f931e 100644 --- a/src/sub-blocks/HubspotForm/schema.ts +++ b/src/sub-blocks/HubspotForm/schema.ts @@ -1,4 +1,4 @@ -import {BaseProps} from '../../schema/validators/common'; +import {BaseProps} from '../../gravity-blocks/schema/validators/common'; export const HubspotFormProps = { type: 'object', diff --git a/src/sub-blocks/ImageCard/ImageCard.tsx b/src/sub-blocks/ImageCard/ImageCard.tsx index e47285f181..7e4d85edae 100644 --- a/src/sub-blocks/ImageCard/ImageCard.tsx +++ b/src/sub-blocks/ImageCard/ImageCard.tsx @@ -4,9 +4,9 @@ import {Link, useUniqId} from '@gravity-ui/uikit'; import {Image} from '../../components'; import {getMediaImage} from '../../components/Media/Image/utils'; -import {useTheme} from '../../context/theme'; -import {GridColumnSizesType} from '../../grid'; -import {useAnalytics} from '../../hooks'; +import {useTheme} from '../../gravity-blocks/context/theme'; +import {GridColumnSizesType} from '../../gravity-blocks/grid'; +import {useAnalytics} from '../../gravity-blocks/hooks'; import {DefaultEventNames, ImageCardDirection, ImageCardProps} from '../../models'; import {block, getThemedValue} from '../../utils'; import Content from '../Content/Content'; @@ -50,12 +50,14 @@ const ImageCard = (props: ImageCardProps) => { const cardContent = ( <React.Fragment> - <div className={b('image', {margins})}> - <Image - className={b('image_inner', {radius: enableImageBorderRadius})} - {...imageProps} - /> - </div> + {image && ( + <div className={b('image', {margins})}> + <Image + className={b('image_inner', {radius: enableImageBorderRadius})} + {...imageProps} + /> + </div> + )} {hasContent && ( <div className={b('content')}> <Content diff --git a/src/sub-blocks/ImageCard/__stories__/ImageCard.stories.tsx b/src/sub-blocks/ImageCard/__stories__/ImageCard.stories.tsx index d94eb3d051..179b680f4c 100644 --- a/src/sub-blocks/ImageCard/__stories__/ImageCard.stories.tsx +++ b/src/sub-blocks/ImageCard/__stories__/ImageCard.stories.tsx @@ -4,7 +4,7 @@ import {blockTransform} from '../../../../.storybook/utils'; import CardLayout from '../../../blocks/CardLayout/CardLayout'; import {BlockBase} from '../../../components'; import {ConstructorRow} from '../../../containers/PageConstructor/components/ConstructorRow'; -import {Grid} from '../../../grid'; +import {Grid} from '../../../gravity-blocks/grid'; import { CardLayoutBlockModel, CardLayoutBlockProps, @@ -12,6 +12,7 @@ import { ImageCardProps, } from '../../../models'; import ImageCard from '../ImageCard'; +import {form} from '../form'; import data from './data.json'; @@ -23,6 +24,9 @@ export default { control: {type: 'color'}, }, }, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<ImageCardModel> = (args) => ( diff --git a/src/sub-blocks/ImageCard/form.ts b/src/sub-blocks/ImageCard/form.ts new file mode 100644 index 0000000000..c17f8f2216 --- /dev/null +++ b/src/sub-blocks/ImageCard/form.ts @@ -0,0 +1,15 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {ImageCard as ImageCardSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + ImageCardSchema['image-card'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + title: 'Image Card', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', +}; diff --git a/src/sub-blocks/ImageCard/icon.ts b/src/sub-blocks/ImageCard/icon.ts new file mode 100644 index 0000000000..e8fdcdc859 --- /dev/null +++ b/src/sub-blocks/ImageCard/icon.ts @@ -0,0 +1,11 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<rect x="37" y="10.2906" width="26" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="37" y="10.2906" width="26" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="40.3488" y="28.593" width="19.3023" height="2" rx="0.55814" fill="#262626"/> +<rect x="40.3488" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +</svg>`, +); diff --git a/src/sub-blocks/ImageCard/index.tsx b/src/sub-blocks/ImageCard/index.tsx new file mode 100644 index 0000000000..d234267c65 --- /dev/null +++ b/src/sub-blocks/ImageCard/index.tsx @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import ImageCard from './ImageCard'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const ImageCardConfig: BlockData = { + type: '@gravity-ui/page-constructor/image-card', + component: ImageCard, + schema: { + name: 'Image Card', + group: '@gravity-ui/page-constructor/Cards', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default ImageCardConfig; diff --git a/src/sub-blocks/ImageCard/index_deprecated.ts b/src/sub-blocks/ImageCard/index_deprecated.ts new file mode 100644 index 0000000000..ca8cbe536f --- /dev/null +++ b/src/sub-blocks/ImageCard/index_deprecated.ts @@ -0,0 +1,20 @@ +import {BlockData} from '../../constructor-items'; + +import ImageCard from './ImageCard'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const ImageCardConfig: BlockData = { + type: 'image-card', + component: ImageCard, + schema: { + name: 'Image Card', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default ImageCardConfig; diff --git a/src/sub-blocks/ImageCard/schema.ts b/src/sub-blocks/ImageCard/schema.ts index a3ef6c0876..7105ba35c6 100644 --- a/src/sub-blocks/ImageCard/schema.ts +++ b/src/sub-blocks/ImageCard/schema.ts @@ -1,8 +1,8 @@ import omit from 'lodash/omit'; -import {BaseProps, CardBase, CardLayoutProps} from '../../schema/validators/common'; -import {ImageProps} from '../../schema/validators/components'; -import {AnalyticsEventSchema} from '../../schema/validators/event'; +import {BaseProps, CardBase, CardLayoutProps} from '../../gravity-blocks/schema/validators/common'; +import {ImageProps} from '../../gravity-blocks/schema/validators/components'; +import {AnalyticsEventSchema} from '../../gravity-blocks/schema/validators/event'; import {ContentBase} from '../Content/schema'; const ImageCardBlockContentProps = omit(ContentBase, ['centered', 'colSizes', 'controlPosition']); diff --git a/src/sub-blocks/LayoutItem/LayoutItem.tsx b/src/sub-blocks/LayoutItem/LayoutItem.tsx index 8bd78976a7..e7c669c846 100644 --- a/src/sub-blocks/LayoutItem/LayoutItem.tsx +++ b/src/sub-blocks/LayoutItem/LayoutItem.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import {useUniqId} from '@gravity-ui/uikit'; import {FullscreenMedia, IconWrapper, Media, MetaInfo} from '../../components'; -import {useTheme} from '../../context/theme'; +import {useTheme} from '../../gravity-blocks/context/theme'; import {ContentBlockProps, LayoutItemProps} from '../../models'; import {block, getThemedValue} from '../../utils'; import {mergeVideoMicrodata} from '../../utils/microdata'; @@ -16,7 +16,7 @@ import './LayoutItem.scss'; const b = block('layout-item'); const LayoutItem = ({ - content: {links, ...content}, + content: {links = [], ...content} = {}, contentMargin = 'm', metaInfo, media, diff --git a/src/sub-blocks/LayoutItem/__stories__/LayoutItem.stories.tsx b/src/sub-blocks/LayoutItem/__stories__/LayoutItem.stories.tsx index 39abbd9eca..4962f18bf2 100644 --- a/src/sub-blocks/LayoutItem/__stories__/LayoutItem.stories.tsx +++ b/src/sub-blocks/LayoutItem/__stories__/LayoutItem.stories.tsx @@ -4,15 +4,19 @@ import {blockTransform} from '../../../../.storybook/utils'; import CardLayout from '../../../blocks/CardLayout/CardLayout'; import {BlockBase} from '../../../components'; import {ConstructorRow} from '../../../containers/PageConstructor/components/ConstructorRow'; -import {Grid} from '../../../grid'; +import {Grid} from '../../../gravity-blocks/grid'; import {LayoutItemModel, LayoutItemProps} from '../../../models'; import LayoutItem from '../LayoutItem'; +import {form} from '../form'; import data from './data.json'; export default { title: 'Components/Cards/LayoutItem', component: LayoutItem, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<LayoutItemModel> = (args) => ( diff --git a/src/sub-blocks/LayoutItem/form.ts b/src/sub-blocks/LayoutItem/form.ts new file mode 100644 index 0000000000..55a33b11f7 --- /dev/null +++ b/src/sub-blocks/LayoutItem/form.ts @@ -0,0 +1,21 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {LayoutItem as LayoutItemSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + LayoutItemSchema as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + type: 'layout-item', + content: { + title: 'Lorem ipsum', + text: 'Dolor sit amet', + }, + media: { + image: 'https://storage.yandexcloud.net/yc-www-community-images/event_ecaf1ef1-bc3a-40fa-adef-827b0959e6c3.jpg', + }, +}; diff --git a/src/sub-blocks/LayoutItem/icon.ts b/src/sub-blocks/LayoutItem/icon.ts new file mode 100644 index 0000000000..e8fdcdc859 --- /dev/null +++ b/src/sub-blocks/LayoutItem/icon.ts @@ -0,0 +1,11 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<rect x="37" y="10.2906" width="26" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="37" y="10.2906" width="26" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="40.3488" y="28.593" width="19.3023" height="2" rx="0.55814" fill="#262626"/> +<rect x="40.3488" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +</svg>`, +); diff --git a/src/sub-blocks/LayoutItem/index.tsx b/src/sub-blocks/LayoutItem/index.tsx new file mode 100644 index 0000000000..17de8d7dd8 --- /dev/null +++ b/src/sub-blocks/LayoutItem/index.tsx @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import LayoutItem from './LayoutItem'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const LayoutItemConfig: BlockData = { + type: '@gravity-ui/page-constructor/layout-item', + component: LayoutItem, + schema: { + name: 'Layout Item', + group: '@gravity-ui/page-constructor/Cards', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default LayoutItemConfig; diff --git a/src/sub-blocks/LayoutItem/index_deprecated.ts b/src/sub-blocks/LayoutItem/index_deprecated.ts new file mode 100644 index 0000000000..f55d3bd988 --- /dev/null +++ b/src/sub-blocks/LayoutItem/index_deprecated.ts @@ -0,0 +1,20 @@ +import {BlockData} from '../../constructor-items'; + +import LayoutItem from './LayoutItem'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const LayoutItemConfig: BlockData = { + type: 'layout-item', + component: LayoutItem, + schema: { + name: 'Layout Item', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default LayoutItemConfig; diff --git a/src/sub-blocks/LayoutItem/schema.ts b/src/sub-blocks/LayoutItem/schema.ts index cbd2c10649..302ba948e8 100644 --- a/src/sub-blocks/LayoutItem/schema.ts +++ b/src/sub-blocks/LayoutItem/schema.ts @@ -1,8 +1,12 @@ import omit from 'lodash/omit'; import metaInfo from '../../components/MetaInfo/schema'; -import {BaseProps, CardLayoutProps, MediaProps} from '../../schema/validators/common'; -import {AnalyticsEventSchema} from '../../schema/validators/event'; +import { + BaseProps, + CardLayoutProps, + MediaProps, +} from '../../gravity-blocks/schema/validators/common'; +import {AnalyticsEventSchema} from '../../gravity-blocks/schema/validators/event'; import {ContentBase} from '../../sub-blocks/Content/schema'; export const LayoutItem = { diff --git a/src/sub-blocks/MediaCard/__stories__/MediaCard.stories.tsx b/src/sub-blocks/MediaCard/__stories__/MediaCard.stories.tsx index e330cc3a62..aa861dd3bb 100644 --- a/src/sub-blocks/MediaCard/__stories__/MediaCard.stories.tsx +++ b/src/sub-blocks/MediaCard/__stories__/MediaCard.stories.tsx @@ -3,6 +3,7 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {MediaCardModel, MediaCardProps} from '../../../models'; import MediaCard from '../MediaCard'; +import {form} from '../form'; import data from './data.json'; @@ -14,6 +15,9 @@ export default { control: {type: 'color'}, }, }, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<MediaCardModel> = (args) => { diff --git a/src/sub-blocks/MediaCard/form.ts b/src/sub-blocks/MediaCard/form.ts new file mode 100644 index 0000000000..5e61be7c63 --- /dev/null +++ b/src/sub-blocks/MediaCard/form.ts @@ -0,0 +1,18 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {MediaCardBlock as MediaCardSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + MediaCardSchema['media-card'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + content: { + title: 'Media Card', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, + image: 'https://storage.yandexcloud.net/yc-www-community-images/event_ecaf1ef1-bc3a-40fa-adef-827b0959e6c3.jpg', +}; diff --git a/src/sub-blocks/MediaCard/icon.ts b/src/sub-blocks/MediaCard/icon.ts new file mode 100644 index 0000000000..e8fdcdc859 --- /dev/null +++ b/src/sub-blocks/MediaCard/icon.ts @@ -0,0 +1,11 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<rect x="37" y="10.2906" width="26" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="37" y="10.2906" width="26" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="40.3488" y="28.593" width="19.3023" height="2" rx="0.55814" fill="#262626"/> +<rect x="40.3488" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +</svg>`, +); diff --git a/src/sub-blocks/MediaCard/index.tsx b/src/sub-blocks/MediaCard/index.tsx new file mode 100644 index 0000000000..827f3899b0 --- /dev/null +++ b/src/sub-blocks/MediaCard/index.tsx @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import MediaCard from './MediaCard'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const MediaCardConfig: BlockData = { + type: '@gravity-ui/page-constructor/media-card', + component: MediaCard, + schema: { + name: 'Media Card', + group: '@gravity-ui/page-constructor/Cards', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default MediaCardConfig; diff --git a/src/sub-blocks/MediaCard/index_deprecated.ts b/src/sub-blocks/MediaCard/index_deprecated.ts new file mode 100644 index 0000000000..22ddec7ad6 --- /dev/null +++ b/src/sub-blocks/MediaCard/index_deprecated.ts @@ -0,0 +1,20 @@ +import {BlockData} from '../../constructor-items'; + +import MediaCard from './MediaCard'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const MediaCardConfig: BlockData = { + type: 'media-card', + component: MediaCard, + schema: { + name: 'Media Card', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default MediaCardConfig; diff --git a/src/sub-blocks/MediaCard/schema.ts b/src/sub-blocks/MediaCard/schema.ts index 6ec5232403..0049a3de5c 100644 --- a/src/sub-blocks/MediaCard/schema.ts +++ b/src/sub-blocks/MediaCard/schema.ts @@ -1,5 +1,10 @@ -import {AnimatableProps, BaseProps, CardBase, MediaProps} from '../../schema/validators/common'; -import {AnalyticsEventSchema} from '../../schema/validators/event'; +import { + AnimatableProps, + BaseProps, + CardBase, + MediaProps, +} from '../../gravity-blocks/schema/validators/common'; +import {AnalyticsEventSchema} from '../../gravity-blocks/schema/validators/event'; export const MediaCardBlock = { 'media-card': { diff --git a/src/sub-blocks/PriceCard/__stories__/PriceCard.stories.tsx b/src/sub-blocks/PriceCard/__stories__/PriceCard.stories.tsx index 952b20db3d..d01bf86890 100644 --- a/src/sub-blocks/PriceCard/__stories__/PriceCard.stories.tsx +++ b/src/sub-blocks/PriceCard/__stories__/PriceCard.stories.tsx @@ -4,9 +4,10 @@ import {blockListTransform, blockTransform} from '../../../../.storybook/utils'; import CardLayout from '../../../blocks/CardLayout/CardLayout'; import {BlockBase} from '../../../components'; import {ConstructorRow} from '../../../containers/PageConstructor/components/ConstructorRow'; -import {Grid} from '../../../grid'; +import {Grid} from '../../../gravity-blocks/grid'; import {PriceCardModel, PriceCardProps} from '../../../models'; import PriceCard from '../PriceCard'; +import {form} from '../form'; import data from './data.json'; @@ -18,6 +19,9 @@ export default { control: {type: 'color'}, }, }, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<PriceCardModel> = (args) => { diff --git a/src/sub-blocks/PriceCard/form.ts b/src/sub-blocks/PriceCard/form.ts new file mode 100644 index 0000000000..ee1b1ae751 --- /dev/null +++ b/src/sub-blocks/PriceCard/form.ts @@ -0,0 +1,28 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {PriceCardBlock as PriceCardSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + PriceCardSchema['price-card'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + type: 'price-card', + border: 'line', + title: 'Basic', + price: '100 $', + pricePeriod: 'month', + priceDetails: '+ 5% from check', + description: 'For any purposes', + buttons: [ + { + url: '/', + text: 'Read More', + width: 'max', + theme: 'action', + }, + ], +}; diff --git a/src/sub-blocks/PriceCard/icon.ts b/src/sub-blocks/PriceCard/icon.ts new file mode 100644 index 0000000000..e8fdcdc859 --- /dev/null +++ b/src/sub-blocks/PriceCard/icon.ts @@ -0,0 +1,11 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<rect x="37" y="10.2906" width="26" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="37" y="10.2906" width="26" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="40.3488" y="28.593" width="19.3023" height="2" rx="0.55814" fill="#262626"/> +<rect x="40.3488" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +</svg>`, +); diff --git a/src/sub-blocks/PriceCard/index.tsx b/src/sub-blocks/PriceCard/index.tsx new file mode 100644 index 0000000000..8697af40ee --- /dev/null +++ b/src/sub-blocks/PriceCard/index.tsx @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import PriceCard from './PriceCard'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const PriceCardConfig: BlockData = { + type: '@gravity-ui/page-constructor/price-card', + component: PriceCard, + schema: { + name: 'Price Card', + group: '@gravity-ui/page-constructor/Cards', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default PriceCardConfig; diff --git a/src/sub-blocks/PriceCard/index_deprecated.ts b/src/sub-blocks/PriceCard/index_deprecated.ts new file mode 100644 index 0000000000..338e3267b8 --- /dev/null +++ b/src/sub-blocks/PriceCard/index_deprecated.ts @@ -0,0 +1,20 @@ +import {BlockData} from '../../constructor-items'; + +import PriceCard from './PriceCard'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const PriceCardConfig: BlockData = { + type: 'price-card', + component: PriceCard, + schema: { + name: 'Price Card', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default PriceCardConfig; diff --git a/src/sub-blocks/PriceCard/schema.ts b/src/sub-blocks/PriceCard/schema.ts index 260ce83df8..eea3646ea4 100644 --- a/src/sub-blocks/PriceCard/schema.ts +++ b/src/sub-blocks/PriceCard/schema.ts @@ -1,4 +1,9 @@ -import {BaseProps, ButtonBlock, CardBase, LinkProps} from '../../schema/validators/common'; +import { + BaseProps, + ButtonBlock, + CardBase, + LinkProps, +} from '../../gravity-blocks/schema/validators/common'; import {ContentBase} from '../Content/schema'; export const PriceCardBlock = { diff --git a/src/sub-blocks/PriceDetailed/CombinedPriceDetailed/CombinedPriceDetailed.tsx b/src/sub-blocks/PriceDetailed/CombinedPriceDetailed/CombinedPriceDetailed.tsx index 787a3668ce..249bfdc7ce 100644 --- a/src/sub-blocks/PriceDetailed/CombinedPriceDetailed/CombinedPriceDetailed.tsx +++ b/src/sub-blocks/PriceDetailed/CombinedPriceDetailed/CombinedPriceDetailed.tsx @@ -4,7 +4,7 @@ import chunk from 'lodash/chunk'; import {CardBase} from '../../../components'; import {BREAKPOINTS} from '../../../constants'; -import {Col, Grid, GridColumnSize, Row} from '../../../grid'; +import {Col, Grid, GridColumnSize, Row} from '../../../gravity-blocks/grid'; import { AnalyticsEventsBase, CardBorder, diff --git a/src/sub-blocks/PriceDetailed/PriceDescription/PriceDescription.tsx b/src/sub-blocks/PriceDetailed/PriceDescription/PriceDescription.tsx index 5e56fbf63a..ab935d4532 100644 --- a/src/sub-blocks/PriceDetailed/PriceDescription/PriceDescription.tsx +++ b/src/sub-blocks/PriceDetailed/PriceDescription/PriceDescription.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import {Label, LabelProps} from '@gravity-ui/uikit'; import {YFMWrapper} from '../../../components'; -import {StylesContext} from '../../../context/stylesContext'; +import {StylesContext} from '../../../gravity-blocks/context/stylesContext'; import { PriceDescriptionColor, PriceDescriptionProps, diff --git a/src/sub-blocks/PriceDetailed/PriceDetailed.tsx b/src/sub-blocks/PriceDetailed/PriceDetailed.tsx index e1fb017cbf..e94bb6c7fc 100644 --- a/src/sub-blocks/PriceDetailed/PriceDetailed.tsx +++ b/src/sub-blocks/PriceDetailed/PriceDetailed.tsx @@ -18,7 +18,7 @@ import SeparatePriceDetailed from './SeparatePriceDetailed/SeparatePriceDetailed const PriceDetailed = (props: PriceDetailedProps) => { const { priceType = PriceDetailsType.SETTINGS, - items, + items = [], numberGroupItems = 1, description, details, diff --git a/src/sub-blocks/PriceDetailed/__stories__/PriceDetailed.stories.tsx b/src/sub-blocks/PriceDetailed/__stories__/PriceDetailed.stories.tsx index 46f8c06b23..e3f8523205 100644 --- a/src/sub-blocks/PriceDetailed/__stories__/PriceDetailed.stories.tsx +++ b/src/sub-blocks/PriceDetailed/__stories__/PriceDetailed.stories.tsx @@ -2,12 +2,16 @@ import {Meta, StoryFn} from '@storybook/react'; import {PriceDetailedProps} from '../../../models'; import PriceDetailed from '../PriceDetailed'; +import {form} from '../form'; import data from './data.json'; export default { component: PriceDetailed, title: 'Components/Cards/PriceDetailed (deprecated)', + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<PriceDetailedProps> = (args) => ( diff --git a/src/sub-blocks/PriceDetailed/form.ts b/src/sub-blocks/PriceDetailed/form.ts new file mode 100644 index 0000000000..f79a3d2005 --- /dev/null +++ b/src/sub-blocks/PriceDetailed/form.ts @@ -0,0 +1,29 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {PriceDetailedBlock as PriceDetailedSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + PriceDetailedSchema['price-detailed'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + priceType: 'marked-list', + items: [ + { + title: '100$', + description: 'Basic edition', + detailedTitle: 'per year', + items: [ + { + text: 'First item', + }, + { + text: 'Second item', + }, + ], + }, + ], +}; diff --git a/src/sub-blocks/PriceDetailed/icon.ts b/src/sub-blocks/PriceDetailed/icon.ts new file mode 100644 index 0000000000..e8fdcdc859 --- /dev/null +++ b/src/sub-blocks/PriceDetailed/icon.ts @@ -0,0 +1,11 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<rect x="37" y="10.2906" width="26" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="37" y="10.2906" width="26" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="40.3488" y="28.593" width="19.3023" height="2" rx="0.55814" fill="#262626"/> +<rect x="40.3488" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +</svg>`, +); diff --git a/src/sub-blocks/PriceDetailed/index.tsx b/src/sub-blocks/PriceDetailed/index.tsx new file mode 100644 index 0000000000..eb0b8a5dcf --- /dev/null +++ b/src/sub-blocks/PriceDetailed/index.tsx @@ -0,0 +1,20 @@ +import {BlockData} from '../../constructor-items'; + +import PriceDetailed from './PriceDetailed'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +/** @deprecated */ +const PriceDetailedConfig: BlockData = { + type: '@gravity-ui/page-constructor/price-detailed', + component: PriceDetailed, + schema: { + name: 'Price Detailed', + group: '@gravity-ui/page-constructor/Cards', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default PriceDetailedConfig; diff --git a/src/sub-blocks/PriceDetailed/index_deprecated.ts b/src/sub-blocks/PriceDetailed/index_deprecated.ts new file mode 100644 index 0000000000..97143c68c5 --- /dev/null +++ b/src/sub-blocks/PriceDetailed/index_deprecated.ts @@ -0,0 +1,18 @@ +import {BlockData} from '../../constructor-items'; + +import PriceDetailed from './PriceDetailed'; +import {defaultValue, form} from './form'; + +const PriceDetailedConfig: BlockData = { + type: 'price-detailed', + component: PriceDetailed, + schema: { + name: 'Price Detailed', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + }, +}; + +export default PriceDetailedConfig; diff --git a/src/sub-blocks/PriceDetailed/schema.ts b/src/sub-blocks/PriceDetailed/schema.ts index 37e2dafd03..30cae831cc 100644 --- a/src/sub-blocks/PriceDetailed/schema.ts +++ b/src/sub-blocks/PriceDetailed/schema.ts @@ -1,6 +1,6 @@ -import {AnimatableProps, BaseProps, textSize} from '../../schema/validators/common'; -import {AnalyticsEventSchema} from '../../schema/validators/event'; -import {filteredArray} from '../../schema/validators/utils'; +import {AnimatableProps, BaseProps, textSize} from '../../gravity-blocks/schema/validators/common'; +import {AnalyticsEventSchema} from '../../gravity-blocks/schema/validators/event'; +import {filteredArray} from '../../gravity-blocks/schema/validators/utils'; const PriceDetailedDetailsType = ['marked-list', 'settings']; const PriceDetailedDescriptionColor = ['cornflower', 'black']; diff --git a/src/sub-blocks/Quote/Quote.tsx b/src/sub-blocks/Quote/Quote.tsx index 38a65cfeb2..ab1273cef5 100644 --- a/src/sub-blocks/Quote/Quote.tsx +++ b/src/sub-blocks/Quote/Quote.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import {Author, Button, Image, YFMWrapper} from '../../components'; import {getMediaImage} from '../../components/Media/Image/utils'; -import {useTheme} from '../../context/theme'; -import {useAnalytics} from '../../hooks'; +import {useTheme} from '../../gravity-blocks/context/theme'; +import {useAnalytics} from '../../gravity-blocks/hooks'; import {AuthorItem, AuthorType, DefaultEventNames, QuoteProps, QuoteType} from '../../models'; import {block, getThemedValue} from '../../utils'; diff --git a/src/sub-blocks/Quote/__stories__/Quote.stories.tsx b/src/sub-blocks/Quote/__stories__/Quote.stories.tsx index 7016676a79..e49829078b 100644 --- a/src/sub-blocks/Quote/__stories__/Quote.stories.tsx +++ b/src/sub-blocks/Quote/__stories__/Quote.stories.tsx @@ -3,6 +3,7 @@ import {Meta, StoryFn} from '@storybook/react'; import {blockTransform} from '../../../../.storybook/utils'; import {QuoteModel, QuoteProps, QuoteType} from '../../../models'; import Quote from '../Quote'; +import {form} from '../form'; import data from './data.json'; @@ -12,6 +13,9 @@ export default { argTypes: { color: {control: 'color'}, }, + parameters: { + inputs: form, + }, } as Meta; const DefaultTemplate: StoryFn<QuoteModel> = (args) => ( diff --git a/src/sub-blocks/Quote/form.ts b/src/sub-blocks/Quote/form.ts new file mode 100644 index 0000000000..9267b0cbee --- /dev/null +++ b/src/sub-blocks/Quote/form.ts @@ -0,0 +1,18 @@ +import {JSONSchemaType} from 'ajv'; + +import {generateFormFieldsFromAjvSchema} from '../../form-generator-v2/utils/generateFormFieldsFromAjv'; + +import {Quote as QuoteSchema} from './schema'; + +// TODO: change to custom block schema +export const form = generateFormFieldsFromAjvSchema( + QuoteSchema['quote'] as unknown as JSONSchemaType<{}>, +); + +export const defaultValue = { + text: 'A good decision is based on knowledge and not on numbers.', + author: { + firstName: ' Plato', + description: 'Greek philosopher', + }, +}; diff --git a/src/sub-blocks/Quote/icon.ts b/src/sub-blocks/Quote/icon.ts new file mode 100644 index 0000000000..e8fdcdc859 --- /dev/null +++ b/src/sub-blocks/Quote/icon.ts @@ -0,0 +1,11 @@ +import {svgToDataUri} from '../../utils/svg'; + +export default svgToDataUri( + `<svg width="100" height="50" viewBox="0 0 100 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="100" height="50" rx="7.81395" fill="white"/> +<rect x="37" y="10.2906" width="26" height="29.4186" rx="3.34884" fill="#C0C8DB"/> +<rect x="37" y="10.2906" width="26" height="14.5116" rx="3.34884" fill="#262626"/> +<rect x="40.3488" y="28.593" width="19.3023" height="2" rx="0.55814" fill="#262626"/> +<rect x="40.3488" y="31.7094" width="9" height="2" rx="0.55814" fill="#262626"/> +</svg>`, +); diff --git a/src/sub-blocks/Quote/index.tsx b/src/sub-blocks/Quote/index.tsx new file mode 100644 index 0000000000..026128924d --- /dev/null +++ b/src/sub-blocks/Quote/index.tsx @@ -0,0 +1,19 @@ +import {BlockData} from '../../constructor-items'; + +import Quote from './Quote'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const QuoteConfig: BlockData = { + type: '@gravity-ui/page-constructor/quote', + component: Quote, + schema: { + name: 'Quote', + group: '@gravity-ui/page-constructor/Cards', + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default QuoteConfig; diff --git a/src/sub-blocks/Quote/index_deprecated.ts b/src/sub-blocks/Quote/index_deprecated.ts new file mode 100644 index 0000000000..7291f5205e --- /dev/null +++ b/src/sub-blocks/Quote/index_deprecated.ts @@ -0,0 +1,20 @@ +import {BlockData} from '../../constructor-items'; + +import Quote from './Quote'; +import {defaultValue, form} from './form'; +import icon from './icon'; + +const QuoteConfig: BlockData = { + type: 'quote', + component: Quote, + schema: { + name: 'Quote', + group: '@deprecated', + hidden: true, + inputs: form, + default: defaultValue, + previewImg: icon, + }, +}; + +export default QuoteConfig; diff --git a/src/sub-blocks/Quote/schema.ts b/src/sub-blocks/Quote/schema.ts index da65346a4a..5ce6cfbefb 100644 --- a/src/sub-blocks/Quote/schema.ts +++ b/src/sub-blocks/Quote/schema.ts @@ -5,7 +5,7 @@ import { authorItem, quoteTypes, withTheme, -} from '../../schema/validators/common'; +} from '../../gravity-blocks/schema/validators/common'; export const Quote = { quote: { diff --git a/src/utils/editor.ts b/src/utils/editor.ts new file mode 100644 index 0000000000..38d603c098 --- /dev/null +++ b/src/utils/editor.ts @@ -0,0 +1,23 @@ +import * as React from 'react'; + +export const getCursorPositionOverElement = ( + elementRect: DOMRect, + mouseEvent: React.MouseEvent, +) => { + const cursorPositionY = elementRect.height - (mouseEvent.clientY - elementRect.y); + const cursorPositionX = elementRect.width - (mouseEvent.clientX - elementRect.x); + const cursorRatioY = elementRect.height / 2 / cursorPositionY; + const cursorRatioX = elementRect.width / 2 / cursorPositionX; + + if (cursorRatioY > cursorRatioX) { + if (cursorRatioY >= 1) { + return 'bottom'; + } else { + return 'top'; + } + } else if (cursorRatioX >= 1) { + return 'right'; + } else { + return 'left'; + } +}; diff --git a/src/utils/form-generator.ts b/src/utils/form-generator.ts new file mode 100644 index 0000000000..61e7fe302b --- /dev/null +++ b/src/utils/form-generator.ts @@ -0,0 +1,173 @@ +import {JSONSchemaType} from 'ajv'; + +import { + ArrayBaseInput, + BooleanInput, + ConfigInput, + NumberInput, + ObjectInput, + SelectBaseInput, + TextAreaInput, + TextInput, +} from '../form-generator'; + +export const generateFromAJV = (schema: JSONSchemaType<{}>): ConfigInput[] => { + if (schema && schema.properties) { + const obj = Object.entries(schema.properties).map(([key, value]) => { + const innerSchema = value as JSONSchemaType<{}>; + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return generateSingleEntity(key, innerSchema); + }); + + return obj.filter(Boolean) as ConfigInput[]; + } + return []; +}; + +export const generateSingleEntity = (key: string, schema: JSONSchemaType<{}>) => { + const type = schema.type; + + if (!type && schema.enum) { + return { + type: 'select', + view: 'select', + name: key, + title: key, + enum: schema.enum.map((enumValue: string) => ({ + content: enumValue, + value: enumValue, + })), + } as SelectBaseInput; + } + + if (schema.oneOf) { + return { + type: 'oneOf', + name: key, + title: key, + options: schema.oneOf.map((item: JSONSchemaType<{}>) => { + let properties; + if (item.properties) { + properties = generateFromAJV(item); + } else { + properties = [ + generateSingleEntity('', { + ...item, + name: '', + title: item.optionName, + }), + ]; + } + + return { + value: item.optionName, + title: item.optionName, + properties: properties, + }; + }), + }; + } + + if (schema.anyOf) { + return { + type: 'anyOf', + name: key, + title: key, + options: schema.anyOf.map((item: JSONSchemaType<{}>) => { + let properties; + if (item.properties) { + properties = generateFromAJV(item); + } else { + properties = [ + generateSingleEntity('', { + ...item, + name: '', + title: item.optionName, + }), + ]; + } + + return { + value: item.optionName, + title: item.optionName, + properties: properties, + }; + }), + }; + } + + switch (type) { + case 'string': { + if (schema.inputType === 'textarea') { + return { + type: 'textarea', + name: key, + title: key, + } as TextAreaInput; + } + if (schema.enum) { + return { + type: 'select', + view: 'select', + name: key, + title: key, + enum: schema.enum.map((enumValue: string) => ({ + content: enumValue, + value: enumValue, + })), + } as SelectBaseInput; + } + return { + type: 'text', + name: key, + title: key, + } as TextInput; + } + case 'number': { + return { + type: 'number', + name: key, + title: key, + } as NumberInput; + } + case 'object': { + return { + type: 'object', + name: key, + title: key, + properties: generateFromAJV(schema), + } as ObjectInput; + } + case 'boolean': { + return { + type: 'boolean', + name: key, + title: key, + properties: generateFromAJV(schema), + } as BooleanInput; + } + case 'array': { + if (schema.items.type === 'string') { + return { + type: 'array', + name: key, + title: key, + properties: generateFromAJV(schema.items), + buttonText: 'Add', + arrayType: 'text', + } as ArrayBaseInput; + } + + return { + type: 'array', + name: key, + title: key, + properties: generateFromAJV(schema.items), + buttonText: 'Add', + arrayType: 'object', + } as ArrayBaseInput; + } + } + + return undefined; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 13dca7be8b..d11cb7b05e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,6 +7,8 @@ export * from './cn'; export * from './url'; export * from './theme'; export * from './icons'; +export * from './navigation'; +export * from './svg'; export type {HubspotEventData, HubspotEventHandlers, HubspotEventName} from './hubspot'; export {isHubspotEventData} from './hubspot'; diff --git a/src/utils/navigation.ts b/src/utils/navigation.ts new file mode 100644 index 0000000000..080f950738 --- /dev/null +++ b/src/utils/navigation.ts @@ -0,0 +1,12 @@ +import _get from 'lodash/get'; +import _isEmpty from 'lodash/isEmpty'; + +import {HeaderData, ThemedNavigationLogoData} from '../models'; + +export function isLogoSet(logo?: ThemedNavigationLogoData): logo is ThemedNavigationLogoData { + return Boolean(_get(logo, 'icon') || _get(logo, 'light.icon')); +} + +export function isHeaderSet(header?: HeaderData): header is HeaderData { + return !_isEmpty(header?.leftItems); +} diff --git a/src/utils/svg.ts b/src/utils/svg.ts index b3fb410c2b..8e2befa8d4 100644 --- a/src/utils/svg.ts +++ b/src/utils/svg.ts @@ -2,3 +2,7 @@ export const a11yHiddenSvgProps = { // Hides element from a11y tree 'aria-hidden': true, }; + +export function svgToDataUri(svgContent: string): string { + return `data:image/svg+xml,${encodeURIComponent(svgContent)}`; +} diff --git a/styles/mixins.scss b/styles/mixins.scss index 6768b93cb4..2d16a8fa7e 100644 --- a/styles/mixins.scss +++ b/styles/mixins.scss @@ -177,12 +177,12 @@ @mixin block { @include add-specificity(&) { - margin-top: $indentL; - padding: 0 0 $indentL; + //margin-top: $indentL; + padding: $indentL 0; &:first-child { // @deprecated - margin-top: var(--pc-first-block-indent, #{$indentXXL}); + //margin-top: var(--pc-first-block-indent, #{$indentXXL}); } } } @@ -605,27 +605,27 @@ unpredictable css rules order in build */ @include add-specificity(&) { &_indentTop { &_0 { - margin-top: 0; + padding-top: 0; } &_xs { - margin-top: $indentXS; + padding-top: $indentXS; } &_s { - margin-top: $indentSM; + padding-top: $indentSM; } &_m { - margin-top: $indentM; + padding-top: $indentM; } &_l { - margin-top: $indentL; + padding-top: $indentL; } &_xl { - margin-top: $indentXL; + padding-top: $indentXL; } } diff --git a/styles/yfm.scss b/styles/yfm.scss index ec65264e7b..3bc78e2a90 100644 --- a/styles/yfm.scss +++ b/styles/yfm.scss @@ -1,4 +1,4 @@ -@import '~@diplodoc/transform/dist/css/yfm.css'; +@import '@diplodoc/transform/dist/css/yfm.css'; @import './mixins.scss'; @import './variables.scss'; diff --git a/test-utils/setup-tests.ts b/test-utils/setup-tests.ts index e8fe85f72f..07eab11107 100644 --- a/test-utils/setup-tests.ts +++ b/test-utils/setup-tests.ts @@ -1,6 +1,12 @@ import {Lang, configure as uiKitConfigure} from '@gravity-ui/uikit'; import {configure} from '@testing-library/dom'; +global.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +}; + uiKitConfigure({ lang: Lang.En, }); diff --git a/test-utils/svg-mock.js b/test-utils/svg-mock.js new file mode 100644 index 0000000000..1e41a6c100 --- /dev/null +++ b/test-utils/svg-mock.js @@ -0,0 +1 @@ +module.exports = 'svg-mock'; diff --git a/widget.webpack.js b/widget.webpack.js index ccc09f12a9..e128f30b25 100644 --- a/widget.webpack.js +++ b/widget.webpack.js @@ -50,9 +50,20 @@ module.exports = { }, }, 'resolve-url-loader', - 'sass-loader', + { + loader: 'sass-loader', + options: { + sassOptions: { + silenceDeprecations: ['legacy-js-api', 'import', 'global-builtin'], + }, + }, + }, ], }, + { + test: /\.svg$/i, + type: 'asset/resource', + }, ], }, resolve: { @@ -61,7 +72,10 @@ module.exports = { plugins: [ { apply: (compiler) => { - compiler.hooks.assetEmitted.tap('InjectWidgetBundlePlugin', (_, {content}) => { + compiler.hooks.assetEmitted.tap('InjectWidgetBundlePlugin', (file, {content}) => { + if (path.basename(file) !== WIDGET_BUNDLE_FILENAME) { + return; + } const script = JSON.stringify(content.toString()); const fileContent = `export default ${script};`;