diff --git a/V3.md b/V3.md
new file mode 100644
index 00000000..43a08e8c
--- /dev/null
+++ b/V3.md
@@ -0,0 +1,50 @@
+## Breaking
+
+- Remove dependency on `@doc-tools/transform`
+ `@doc-tools/transform` js and css should be attached directly to final projects
+
+
+### ErrorPage
+- Removed `lang` prop
+
+### Feedback
+- Removed `lang` prop
+- Removed `singlePage` prop
+- Pick `dislikeVariants` from i18n by default
+
+### Contributors
+- Removed `lang` prop
+
+### MiniToc
+- Removed `lang` prop
+- MiniToc returns `null` on empty headings. (Previously it render useless title)
+
+### SettingsControl
+- Removed `lang` prop
+
+### PdfControl
+- Removed `lang` prop
+- Wrapped by react `memo`
+
+### SinglePageControl
+- Removed `lang` prop
+- Wrapped by react `memo`
+
+### FullScreenControl
+- Removed `lang` prop
+- Wrapped by react `memo`
+
+### EditButton
+- Removed `lang` prop
+- Wrapped by react `memo`
+
+### SearchPage
+- Removed `lang` prop
+
+### SearchBar
+- Removed `lang` prop
+- Wrapped by react `memo`
+
+### SearchItem
+- Removed `lang` prop
+- Wrapped by react `memo`
diff --git a/demo/.babelrc.json b/demo/.babelrc.json
new file mode 100644
index 00000000..603379f7
--- /dev/null
+++ b/demo/.babelrc.json
@@ -0,0 +1,18 @@
+{
+ "sourceType": "unambiguous",
+ "presets": [
+ [
+ "@babel/preset-env",
+ {
+ "targets": {
+ "chrome": 100,
+ "safari": 15,
+ "firefox": 91
+ }
+ }
+ ],
+ "@babel/preset-react",
+ "@babel/preset-typescript"
+ ],
+ "plugins": []
+}
diff --git a/demo/.storybook/main.js b/demo/.storybook/main.js
new file mode 100644
index 00000000..8d777502
--- /dev/null
+++ b/demo/.storybook/main.js
@@ -0,0 +1,49 @@
+// import {join, dirname} from 'path';
+
+/**
+ * This function is used to resolve the absolute path of a package.
+ * It is needed in projects that use Yarn PnP or are set up within a monorepo.
+ */
+// function getAbsolutePath(value) {
+// return dirname(require.resolve(join(value, 'package.json')));
+// }
+
+/** @type { import('@storybook/react-webpack5').StorybookConfig } */
+const config = {
+ stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+ addons: [
+ '@storybook/addon-links',
+ '@storybook/addon-essentials',
+ {
+ name: '@storybook/addon-styling',
+ options: {
+ sass: {
+ implementation: require("sass"),
+ },
+ }
+ },
+ '@storybook/addon-onboarding',
+ '@storybook/addon-interactions',
+
+ ],
+ framework: {
+ name: '@storybook/react-webpack5',
+ options: {},
+ },
+ docs: {
+ autodocs: 'tag',
+ },
+ async webpackFinal(config, { configType }) {
+ config.module.rules.push({
+ test: /\.svg$/,
+ type: 'javascript/auto',
+ use: ['@svgr/webpack'],
+ });
+
+ config.module.rules.forEach(rule => console.log(rule.test, rule.use));
+
+ return config;
+ }
+};
+
+export default config;
diff --git a/demo/.storybook/preview.js b/demo/.storybook/preview.js
new file mode 100644
index 00000000..ecf5d4f0
--- /dev/null
+++ b/demo/.storybook/preview.js
@@ -0,0 +1,14 @@
+/** @type { import('@storybook/react').Preview } */
+const preview = {
+ parameters: {
+ actions: {argTypesRegex: '^on[A-Z].*'},
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/,
+ },
+ },
+ },
+};
+
+export default preview;
diff --git a/demo/package.json b/demo/package.json
new file mode 100644
index 00000000..20e980c2
--- /dev/null
+++ b/demo/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@doc-tools/components-demo",
+ "private": true,
+ "license": "MIT",
+ "version": "1.0.0",
+ "scripts": {
+ "storybook": "storybook dev -p 6006",
+ "build-storybook": "storybook build"
+ },
+ "devDependencies": {
+ "@babel/preset-env": "^7.22.14",
+ "@babel/preset-react": "^7.22.5",
+ "@doc-tools/transform": "^3.10.2",
+ "@storybook/addon-essentials": "^7.4.0",
+ "@storybook/addon-interactions": "^7.4.0",
+ "@storybook/addon-knobs": "^7.0.2",
+ "@storybook/addon-links": "^7.4.0",
+ "@storybook/addon-onboarding": "^1.0.8",
+ "@storybook/addon-styling-webpack": "^0.0.3",
+ "@storybook/blocks": "^7.4.0",
+ "@storybook/react": "^7.4.0",
+ "@storybook/react-webpack5": "^7.4.0",
+ "@storybook/testing-library": "^0.2.0",
+ "prop-types": "^15.8.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "storybook": "^7.4.0"
+ },
+ "dependencies": {
+ "@babel/preset-typescript": "^7.22.11",
+ "@doc-tools/components": "^2.8.3",
+ "@storybook/addon-styling": "^1.3.7",
+ "@svgr/webpack": "^8.1.0",
+ "css-loader": "^6.8.1",
+ "sass": "^1.66.1",
+ "sass-loader": "^13.3.2"
+ }
+}
diff --git a/demo/src/Components/DocLeadingPage/index.tsx b/demo/src/Components/DocLeadingPage/index.tsx
new file mode 100644
index 00000000..6cf5e62e
--- /dev/null
+++ b/demo/src/Components/DocLeadingPage/index.tsx
@@ -0,0 +1,38 @@
+import React, {useState} from 'react';
+import cn from 'bem-cn-lite';
+import {DocLeadingPage, DocLeadingPageData, DEFAULT_SETTINGS} from '@doc-tools/components';
+import Header from '../Header/Header';
+import {getIsMobile} from '../../controls/settings';
+import getLangControl from '../../controls/lang';
+import pageContent from './page.json';
+
+const layoutBlock = cn('Layout');
+
+const DocLeadingPageDemo = () => {
+ const langValue = getLangControl();
+ const isMobile = getIsMobile();
+ const router = {pathname: '/docs/compute'};
+
+ const [fullScreen, onChangeFullScreen] = useState(DEFAULT_SETTINGS.fullScreen);
+ const [lang, onChangeLang] = useState(langValue);
+
+ return (
+
([]);
+
+ const onSendFeedback = useCallback((data: FeedbackSendData) => {
+ const {type} = data;
+
+ if (type === FeedbackType.like) {
+ setIsLiked(true);
+ setIsDisliked(false);
+ } else if (type === FeedbackType.dislike) {
+ setIsLiked(false);
+ setIsDisliked(true);
+ } else {
+ setIsLiked(false);
+ setIsDisliked(false);
+ }
+
+ console.log('Feedback:', data);
+ }, []);
+
+ const onChangeBookmarkPage = useCallback((data: boolean) => {
+ setIsPinned(data);
+ console.log(`This page pinned: ${data}`);
+ }, []);
+
+ updateBodyClassName(theme);
+
+ useEffect(() => {
+ const newSearchWords = searchQuery.split(' ').filter((word) => {
+ if (!word) {
+ return false;
+ }
+
+ if (searchQuery.length > 10) {
+ return word.length > 3;
+ }
+
+ return true;
+ });
+
+ setSearchWords(newSearchWords);
+ }, [searchQuery]);
+
+ const onNotFoundWords = useCallback(() => {
+ console.log(`Not found words for the query: ${searchQuery}`);
+ }, [searchQuery]);
+ const onContentMutation = useCallback(() => {
+ console.log('onContentMutation');
+ }, []);
+ const onContentLoaded = useCallback(() => {
+ console.log('onContentLoaded');
+ }, []);
+
+ const onChangeLangWrapper = useCallback((value) => {
+ onChangeLang(value);
+ configureUikit({lang: value});
+ }, []);
+
+ const props = {
+ ...getContent(lang, singlePage),
+ vcsType,
+ lang,
+ onChangeLang: onChangeLangWrapper,
+ langs,
+ router,
+ // headerHeight: fullScreen ? 0 : 64,
+ // headerHeight: fullScreen ? 0 : 64,
+ fullScreen,
+ onChangeFullScreen,
+ wideFormat,
+ onChangeWideFormat,
+ showMiniToc,
+ onChangeShowMiniToc,
+ onSubscribe: hasSubscribe === 'true' ? () => {} : undefined,
+ theme,
+ onChangeTheme: (themeValue: Theme) => {
+ updateBodyClassName(themeValue);
+ onChangeTheme(themeValue);
+ },
+ textSize,
+ onChangeTextSize,
+ singlePage,
+ onChangeSinglePage,
+ isLiked,
+ isDisliked,
+ onSendFeedback,
+ pdfLink,
+ // onNotFoundWords,
+ // showSearchBar,
+ // searchWords,
+ // searchQuery,
+ // onCloseSearchBar,
+ // useSearchBar: true,
+ // bookmarkedPage: hasBookmark === 'true' && isPinned,
+ // onChangeBookmarkPage: hasBookmark === 'true' ? onChangeBookmarkPage : undefined,
+ };
+
+ const tocTitleIcon = (
+
+ {/* eslint-disable-next-line max-len */}
+
+
+ );
+ const convertPathToOriginalArticle = (path: string) => join('prefix', path);
+ const generatePathToVcs = (path: string) =>
+ join(`https://github.com/yandex-cloud/docs/blob/master/${props.lang}`, path);
+ const renderLoader = () => 'Loading...';
+ // const onChangeSearch = (value: string) => {
+ // setShowSearchBar(true);
+ // setSearchQuery(value);
+ // };
+
+ return (
+
+ {/*{props.fullScreen ? null : (*/}
+ {/*
*/}
+ {/*)}*/}
+
+
+
+
+ );
+};
+
+export default {
+ title: 'Example/DocPage',
+ component: DocPageDemo,
+ parameters: {
+ // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
+ layout: 'centered',
+ },
+ // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
+ // tags: ['autodocs'],
+ // More on argTypes: https://storybook.js.org/docs/react/api/argtypes
+ // argTypes: {
+ // backgroundColor: { control: 'color' },
+ // },
+};
+
+// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
+export const Primary = {
+ args: {
+ primary: true,
+ label: 'Button',
+ },
+};
+
diff --git a/demo/src/Components/DocPage/page-en.json b/demo/src/Components/DocPage/page-en.json
new file mode 100644
index 00000000..8119c91a
--- /dev/null
+++ b/demo/src/Components/DocPage/page-en.json
@@ -0,0 +1,260 @@
+{
+ "html": "Yandex.Cloud services can be subject to quotas and limits:
\n\nQuotas are organizational restrictions that can be changed by technical support on request. \nLimits are technical restrictions due to the Yandex.Cloud architecture specifics. The limits cannot be changed. \n \nYou can view your current account quotas in the management console .
\nWhen designing your infrastructure in Yandex.Cloud, consider the limits as the boundary of opportunities that Yandex Cloud can provide to you. Quotas are changeable restrictions that can potentially be increased to the values of limits.
\n Why quotas are needed \nQuotas serve as a soft restriction for requesting resources, and allow Yandex Cloud to guarantee the stability of the service: new users cannot take up too much resources for testing purposes. If you are ready to use more resources, contact the technical support and tell us exactly which quotas you need to increase, and how.
\nTechnical support decides whether or not to increase quotas on an individual basis.
\n Quotas and limits defaults for Yandex.Cloud services \nQuotas are listed with default values that match the quotas of the trial period .
\n Yandex Compute Cloud \n Quotas \n\n\n\nType of limit \nValue \n \n \n\n\nNumber of virtual machines per cloud \n12 \n \n\nTotal number of vCPUs for all VMs per cloud \n32 \n \n\nTotal virtual memory for all VMs per cloud \n128 GB \n \n\nTotal number of disks per cloud \n32 \n \n\nTotal SSD storage capacity per cloud \n200 GB \n \n\nTotal HDD storage capacity per cloud \n500 GB \n \n\nTotal number of disk snapshots per cloud \n32 \n \n\nTotal storage capacity of all disk snapshots per cloud \n400 GB \n \n\nNumber of images per cloud \n8 \n \n\nNumber of instance groups per cloud \n10 \n \n\nTotal number of GPUs for all VMs per cloud* \n0 \n \n\nNumber of concurrent operations in the cloud \n15 \n \n\nMaximum number of VM instances in a placement group \n5 \n \n\nMaximum number of placement groups per cloud \n2 \n \n \n
\n* To create a VM with a GPU, contact technical support .
\n VM limits \n\n\n\nType of limit \nValue \n \n \n\n\nMaximum number of vCPUs per VM \n32 and 64 for Intel Broadwell and Intel Cascade Lake platforms , respectively \n \n\nMaximum virtual memory per VM \n256 GB and 512 GB for Intel Broadwell and Intel Cascade Lake platforms , respectively \n \n\nMaximum number of disks connected to a single VM \n7 \n \n\nMaximum number of GPUs connected to a single VM \n4 \n \n\nMaximum number of vCPUs for VMs with GPUs \n32 \n \n\nMaximum RAM for VMs with GPUs \n384 \n \n \n
\n Disk limits \n\n
\n
Network SSD
\n
Network HDD
\n
\n
\n
\n\n\nType of limit \nValue \n \n \n\n\nMaximum disk size \n4 TB \n \n\nMaximum disk snapshot size \n4 TB \n \n\nAllocation unit size \n32 GB \n \n\nMaximum* IOPS for writes, per disk \n40,000 \n \n\nMaximum* IOPS for writes, per allocation unit \n1000 \n \n\nMaximum** bandwidth for writes, per disk \n450 MB/s \n \n\nMaximum** bandwidth for writes, per allocation unit \n15 MB/s \n \n\nMaximum* IOPS for reads, per disk \n12,000 \n \n\nMaximum* IOPS for reads, per allocation unit \n400 \n \n\nMaximum** bandwidth for reads, per disk \n450 MB/s \n \n\nMaximum** bandwidth for reads, per allocation unit \n15 MB/s \n \n \n
\n
\n
\n
\n\n\nType of limit \nValue \n \n \n\n\nMaximum disk size \n4 TB \n \n\nMaximum disk snapshot size \n4 TB \n \n\nAllocation unit size \n256 GB \n \n\nMaximum* IOPS for writes, per disk \n11,000 \n \n\nMaximum* IOPS for writes, per allocation unit \n300 \n \n\nMaximum** bandwidth for writes, per disk \n240 MB/s \n \n\nMaximum** bandwidth for writes, per allocation unit \n30 MB/s \n \n\nMaximum* IOPS for reads, per disk \n300 \n \n\nMaximum* IOPS for reads, per allocation unit \n100 \n \n\nMaximum** bandwidth for reads, per disk \n240 MB/s \n \n\nMaximum** bandwidth for reads, per allocation unit \n30 MB/s \n \n \n
\n
\n
\n * \nTo achieve maximum IOPS, we recommend performing read and write operations that are 4 KB and less.
\n ** \nTo achieve the maximum possible bandwidth, we recommend performing 4 MB reads and writes.
\n Yandex Object Storage \n Quotas \n\n\n\nType of limit \nValue \n \n \n\n\nStorage volume in a cloud \n5 TB \n \n\nNumber of buckets per cloud \n25 \n \n \n
\n Limits \n\n\n\nType of limit \nValue \n \n \n\n\nMaximum object size \n5 TB \n \n\nTotal header size per request to the HTTP API \n8 KB \n \n\nSize of user-defined metadata in an object \n2 KB \n \n\nMaximum size of data to be uploaded per request \n5 GB \n \n\nMinimum size of data parts for multipart uploading, except the last one \n5 MB \n \n\nMaximum number of parts in multi-part uploading \n10,000 \n \n \n
\n Yandex Virtual Private Cloud \n Quotas \n\n\n\nType of limit \nValue \n \n \n\n\nNumber of cloud networks per cloud \n2 \n \n\nNumber of subnets per cloud \n6 \n \n\nNumber of all public IP addresses per cloud \n8 \n \n\nNumber of static public IP addresses per cloud \n2 \n \n\nNumber of route tables per cloud \n8 \n \n\nNumber of static routes per cloud \n256 \n \n \n
\n Limits \n\n\n\nType of limit \nValue \n \n \n\n\nMinimum CIDR size for a subnet \n/28 \n \n\nMaximum CIDR size for a subnet \n/16 \n \n\nMaximum number of simultaneous TCP/UDP connections per VM \n50000 \n \n \n
\n Filtering outgoing traffic \nYandex.Cloud always blocks outgoing traffic to TCP port 25:
\n\nTo any servers on the internet other than Yandex.Mail servers. \nTo Yandex Compute Cloud VMs when accessed via a public IP address. \n \nYandex.Cloud may open TCP port 25 by request via technical support if you comply with Acceptable Use Policy . Yandex.Cloud is entitled to block outgoing traffic on TCP port 25 if you violate the Use Policy.
\n Yandex Resource Manager \nThere are no quotas or limits for Yandex Resource Manager.
\n Yandex Load Balancer \n Quotas \n\n\n\nType of limit \nValue \n \n \n\n\nNumber of load balancers per cloud \n2 \n \n\nNumber of target groups per cloud \n2 \n \n \n
\n Limits \n\n\n\nType of limit \nValue \n \n \n\n\nNumber of resources per target group \n254 \n \n\nNumber of listening ports \n10 \n \n\nNumber of health checks per attached target group \n1 \n \n\nStatus check protocol \nTCP, HTTP \n \n \n
\n Other restrictions \nA particular target group can only contain target resources from a single cloud network.
\nA target group can include resources that are connected to the same subnet within a single availability zone.
\nYou can create a load balancer without a listener.
\nHealth checks are transmitted from the IP address range 198.18.235.0/24
.
\nWhen connecting resources to the load balancer, keep in mind the limit on the maximum number of simultaneous TCP/UDP connections per VM.
\n Yandex Managed Service for ClickHouse \n Quotas \n\n\n\nType of limit \nValue \n \n \n\n\nNumber of clusters per cloud \n16 \n \n\nTotal number of processor cores for all DB hosts per cloud \n64 \n \n\nTotal virtual memory for all DB hosts per cloud \n512 GB \n \n\nTotal storage capacity for all clusters per cloud \n4096 GB \n \n \n
\n Limits \n\n\n\nType of limit \nValue \n \n \n\n\nLowest host class \nb1.nano (5% × 2 vCPU Intel Broadwell, 2 GB RAM) \n \n\nHighest host class \nm2.8xlarge (64 vCPU Intel Cascade Lake, 512 GB RAM) \n \n\nMaximum number of hosts in the cluster \n10 \n \n\nMaximum network storage capacity \n4096 GB \n \n\nMaximum local storage capacity \n1400 GB \n \n \n
\n Yandex Managed Service for MongoDB \n Quotas \n\n\n\nType of limit \nValue \n \n \n\n\nNumber of clusters per cloud \n16 \n \n\nTotal number of processor cores for all DB hosts per cloud \n64 \n \n\nTotal virtual memory for all DB hosts per cloud \n512 GB \n \n\nTotal storage capacity for all clusters per cloud \n4096 GB \n \n \n
\n Limits \n\n\n\nType of limit \nValue \n \n \n\n\nLowest host class \nb1.nano (5% × 2 vCPU Intel Broadwell, 2 GB RAM) \n \n\nHighest host class \nm2.8xlarge (64 vCPU Intel Cascade Lake, 512 GB RAM) \n \n\nMaximum number of hosts per cluster MongoDB \n5 \n \n\nMaximum storage capacity for a cluster MongoDB \n512 GB \n \n \n
\n Yandex Managed Service for MySQL \n Quotas \n\n\n\nType of limit \nValue \n \n \n\n\nNumber of clusters per cloud \n16 \n \n\nTotal number of processor cores for all DB hosts per cloud \n64 \n \n\nTotal virtual memory for all DB hosts per cloud \n512 GB \n \n\nTotal storage capacity for all clusters per cloud \n4096 GB \n \n \n
\n Limits \n\n\n\nType of limit \nValue \n \n \n\n\nLowest host class \nb1.nano (5% × 2 vCPU Intel Broadwell, 2 GB RAM) \n \n\nHighest host class \nm2.8xlarge (64 vCPU Intel Cascade Lake, 512 GB RAM) \n \n\nMaximum number of hosts per cluster \n7 \n \n\nMaximum network storage capacity \n2048 GB \n \n\nMaximum local storage capacity \n1400 GB \n \n \n
\n Yandex Managed Service for PostgreSQL \n Quotas \n\n\n\nType of limit \nValue \n \n \n\n\nNumber of clusters per cloud \n16 \n \n\nTotal number of processor cores for all DB hosts per cloud \n64 \n \n\nTotal virtual memory for all DB hosts per cloud \n512 GB \n \n\nTotal storage capacity for all clusters per cloud \n4096 GB \n \n \n
\n Limits \n\n\n\nType of limit \nValue \n \n \n\n\nLowest host class \nb1.nano (5% × 2 vCPU Intel Broadwell, 2 GB RAM) \n \n\nHighest host class \nm2.8xlarge (64 vCPU Intel Cascade Lake, 512 GB RAM) \n \n\nMaximum number of hosts per cluster \n10 \n \n\nMaximum network storage capacity \n2048 GB \n \n\nMaximum local storage capacity \n1400 GB \n \n \n
\n Yandex Managed Service for Redis \n Quotas \n\n\n\nType of limit \nValue \n \n \n\n\nNumber of clusters per cloud \n16 \n \n\nTotal number of processor cores for all DB hosts per cloud \n64 \n \n\nTotal virtual memory for all hosts per cloud \n512 GB \n \n\nTotal disk storage capacity for all clusters per cloud \n4096 GB \n \n \n
\n Limits \n\n\n\nType of limit \nValue \n \n \n\n\nLowest host class \nb1.nano (burstable
with 2 GB RAM) \n \n\nHighest host class \nhm1.9xlarge (high-memory
with 256 GB RAM) \n \n\nMaximum number of hosts per cluster \n7 \n \n\nMinimum disk size per cluster \n2 times more than the amount of RAM selected \n \n\nMaximum disk size per cluster \n8 times more than the amount of RAM selected \n \n \n
\n Yandex Message Queue \n Quotas \n Messages \n\n\n\nType of limit \nValue \n \n \n\n\nNumber of SendMessage
and SendMessageBatch
calls per queue \n300 calls per second for standard queues 30 calls per second for FIFO queues \n \n\nNumber of ReceiveMessage
calls per queue \n300 calls per second for standard queues 30 calls per second for FIFO queues \n \n\nNumber of DeleteMessage
and DeleteMessageBatch
calls per queue \n300 calls per second for standard queues 30 calls per second for FIFO queues \n \n\nNumber of ChangeMessageVisibility
and ChangeMessageVisibilityBatch
calls per queue \n300 calls per second for standard queues 30 calls per second for FIFO queues \n \n\nNumber of CreateQueue calls
per cloud \n2 calls per second \n \n\nNumber of DeleteQueue calls
per cloud \n5 calls per second \n \n\nNumber of other request calls per cloud \n100 calls per second \n \n\nNumber of queues per cloud \n10 \n \n \n
\n Limits \n Queues \n\n\n\nType of limit \nValue \n \n \n\n\nMinimum message enqueue delay (DelaySeconds
parameter) \n0 seconds \n \n\nMaximum message enqueue delay (DelaySeconds
parameter) \n900 seconds (15 minutes) \n \n\nNumber of messages being processed per standard queue \n120,000 \n \n\nNumber of messages being processed per FIFO queue \n20,000 \n \n\nQueue name \nMaximum of 80 characters, including numbers, small and capital Latin letters, hyphens, and underscores. The name of a FIFO queue must end with the .fifo
suffix. \n \n \n
\n Messages \n\n\n\nType of limit \nValue \n \n \n\n\nBatch entry ID \nMaximum of 80 characters, including numbers, small and capital Latin letters, hyphens, and underscores. \n \n\nMaximum number of message attributes \n10 \n \n\nMaximum number of entries per batch \n10 \n \n\nMessage content \nXML, JSON, and unformatted text. The following Unicode characters are supported: #x9
#xA
#xD
from #x20
to #xD7FF
from #xE000
to #xFFFD
from #x10000
to #x10FFFF
\n \n\nMaximum period for retaining messages in a queue \n1209600 seconds (14 days) \n \n\nMinimum period for retaining messages in a queue \n60 seconds (1 minute) \n \n\nMaximum message enqueue delay (DelaySeconds
parameter) \n900 seconds (15 minutes) \n \n\nMinimum message enqueue delay (DelaySeconds
parameter) \n0 seconds \n \n\nMaximum message size \n262144 bytes (256 KB) \n \n\nMinimum message size \n1 byte \n \n\nMaximum message visibility timeout \n12 hours \n \n\nMinimum message visibility timeout \n0 seconds \n \n\nMaximum clients waiting time to wait for the message from an empty queue (WaitTimeSeconds
parameter) \n20 seconds \n \n \n
\n Yandex SpeechKit \n Quotas \n\n Limits \n\n\n\nType of limit \nValue \n \n \n\n\nShort audio recognition \n \n \n\nMaximum file size \n1 MB \n \n\nMaximum duration of audio \n30 seconds \n \n\nMaximum number of audio channels \n1 \n \n\nStreaming mode of short audio recognition \n \n \n\nMaximum duration of transmitted audio for the entire session \n5 minutes \n \n\nMaximum size of transmitted audio data \n10 MB \n \n\nMaximum number of audio channels \n1 \n \n\nLong audio recognition \n \n \n\nMaximum file size \n1 GB \n \n\nMaximum duration of audio \n4 hours \n \n\nPeriod for storing recognition results on the server \n3 days \n \n\nSpeech synthesis \n \n \n\nMaximum request size \n5000 characters \n \n \n
\n Yandex Translate \n Quotas \n\n\n\nType of limit \nValue \n \n \n\n\nCalls of a single API method per second \n20 \n \n\nCharacters sent for translation per hour \n1 million \n \n\nCharacters sent for language detection per hour \n1 million \n \n \n
\n Limits \nThere are no limits in the service. For limitations on the field values in the request body, see the API reference .
\n Yandex Vision \n Quotas \n\n Limits \n\n\n\nType of limit \nValue \n \n \n\n\nMaximum file size \n1 MB \n \n\nMaximum image size \n20 MP (length × width) \n \n\nMaximum number of pages in a PDF file \n8 \n \n \n
\n",
+ "title": "Quotas and limits",
+ "headings": [
+ {
+ "title": "Why quotas are needed",
+ "href": "#quotas",
+ "level": 2
+ },
+ {
+ "title": "Quotas and limits defaults for Yandex.Cloud services",
+ "href": "#quotas-and-limits-defaults-for-yandex.cloud-services",
+ "level": 2,
+ "items": [
+ {
+ "title": "Yandex Compute Cloud",
+ "href": "#compute",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Object Storage",
+ "href": "#storage",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Virtual Private Cloud",
+ "href": "#vpc",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Resource Manager",
+ "href": "#resource-manager",
+ "level": 3
+ },
+ {
+ "title": "Yandex Load Balancer",
+ "href": "#load-balancer",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Managed Service for ClickHouse",
+ "href": "#mch",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Managed Service for MongoDB",
+ "href": "#mmg",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Managed Service for MySQL",
+ "href": "#mmy",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Managed Service for PostgreSQL",
+ "href": "#mpg",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Managed Service for Redis",
+ "href": "#mrd",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Message Queue",
+ "href": "#mq",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex SpeechKit",
+ "href": "#speechkit",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Translate",
+ "href": "#translate",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Vision",
+ "href": "#vision",
+ "level": 3,
+ "items": []
+ }
+ ]
+ }
+ ],
+ "meta": {
+ "author": {
+ "avatar": "",
+ "email": "robot-dataui-vcs@yandex-team.ru",
+ "login": "",
+ "name": "",
+ "url": "http://yandex.ru/"
+ },
+ "contributors": [
+ {
+ "avatar": "https://avatars.githubusercontent.com/u/2485945?v=6",
+ "email": "robot-dataui-vcs@yandex-team.ru",
+ "login": "",
+ "name": "DataUI VCS Robot",
+ "url": ""
+ },
+ {
+ "avatar": "",
+ "email": "skanunnikov@yandex-team.ru",
+ "login": "",
+ "name": "Sergey Kanunnikov",
+ "url": ""
+ },
+ {
+ "avatar": "https://avatars.githubusercontent.com/u/2485935?v=4",
+ "email": "",
+ "login": "zamulla",
+ "name": "Aleksey Zamulla",
+ "url": "http://yandex.ru/"
+ },
+ {
+ "avatar": "https://avatars.githubusercontent.com/u/2485945?v=6",
+ "email": "migelle@yandex-team.ru",
+ "login": "migelle",
+ "name": "",
+ "url": "http://yandex.ru/"
+ },
+ {
+ "avatar": "https://avatars.githubusercontent.com/u/2485945?v=6",
+ "email": "leeuw@yandex-team.ru",
+ "login": "",
+ "name": "",
+ "url": "http://yandex.ru/"
+ },
+ {
+ "avatar": "",
+ "email": "dottir@yandex-team.ru",
+ "login": "",
+ "name": "Anastasia Karavaeva",
+ "url": ""
+ }
+ ]
+ },
+ "toc": {
+ "title": "Yandex.Cloud overview",
+ "href": "/docs/overview/",
+ "items": [
+ {
+ "name": "Yandex.Cloud services",
+ "href": "/docs/overview/concepts/services",
+ "id": "86fb5a9101b917e55d57955e41a4a773"
+ },
+ {
+ "name": "Equivalent services on other platforms",
+ "expanded": true,
+ "items": [
+ {
+ "name": "Overview",
+ "href": "/docs/overview/platform-comparison/",
+ "id": "db26ca551a9e94ece774f19a472cc500"
+ },
+ {
+ "name": "Equivalents for Amazon Web Services",
+ "href": "/docs/overview/platform-comparison/aws",
+ "id": "c735da730d10e0f31b4b9a722251c0f0"
+ },
+ {
+ "name": "Equivalents for Google Cloud Platform",
+ "href": "/docs/overview/platform-comparison/gcp",
+ "id": "f7aff73e87f0aca60595896b050a002d"
+ },
+ {
+ "name": "Equivalents for Microsoft Azure",
+ "href": "/docs/overview/platform-comparison/azure",
+ "id": "ca1deb97788e2d54631f4908ac1b5ca6"
+ }
+ ],
+ "id": "c5625beecf9017c1290bd748fd9dc854"
+ },
+ {
+ "name": "Availability zones",
+ "href": "/docs/overview/concepts/geo-scope",
+ "id": "685e6c38153524c07dcd17f415e1f516"
+ },
+ {
+ "name": "Getting started",
+ "href": "/docs/overview/quickstart",
+ "id": "ab338ee86e023121249934c955ed9810"
+ },
+ {
+ "name": "Release stages",
+ "href": "/docs/overview/concepts/launch-stages",
+ "id": "de2baab2bd603f5630a1a398d25fa370"
+ },
+ {
+ "name": "Quotas and limits",
+ "href": "/docs/overview/concepts/quotas-limits",
+ "id": "a3bb97002572fbdca0f056d75d42acd2"
+ },
+ {
+ "name": "API",
+ "href": "/docs/overview/api",
+ "id": "136bdc27253376a450eee99e4539c186"
+ },
+ {
+ "name": "Security and compliance",
+ "items": [
+ {
+ "name": "Security bulletin",
+ "href": "/docs/overview/security-bulletins/",
+ "id": "51e1474da0ee2ab04126285a0e3ca7df"
+ },
+ {
+ "name": "Rules for performing external security scans",
+ "href": "/docs/overview/compliance/pentest",
+ "id": "50ced679abe4df4b724e5b2dc462846e"
+ }
+ ],
+ "id": "a2541cbb86e50d2f6ed9c2eba245611d"
+ },
+ {
+ "name": "Deleting user data",
+ "href": "/docs/overview/concepts/data-deletion",
+ "id": "bba0c3f3dc2cd1b71c1aeb83d67d77a8"
+ },
+ {
+ "name": "SLA",
+ "href": "/docs/overview/sla",
+ "id": "1636c6203504367fc47f73077e95ebad"
+ },
+ {
+ "name": "Questions and answers",
+ "href": "/docs/overview/qa",
+ "id": "6293190c6aa223973af0014e104c70ef"
+ }
+ ]
+ },
+ "breadcrumbs": [
+ {
+ "name": "Documentation"
+ },
+ {
+ "name": "Quotas and limits"
+ }
+ ],
+ "filePath": "/cache/c12878f1-cc2e-445f-8f96-7374fe76b074/en/overview/concepts/quotas-limits.md",
+ "githubUrl": "https://github.com/yandex-cloud/docs/tree/master/en/overview/concepts/quotas-limits.md",
+ "vcsType": "github",
+ "vcsUrl": "https://github.com/yandex-cloud/docs/tree/master/en/overview/concepts/quotas-limits.md"
+}
diff --git a/demo/src/Components/DocPage/page-ru.json b/demo/src/Components/DocPage/page-ru.json
new file mode 100644
index 00000000..fa452b63
--- /dev/null
+++ b/demo/src/Components/DocPage/page-ru.json
@@ -0,0 +1,271 @@
+{
+ "html": "В сервисах Яндекс.Облака могут действовать квоты и лимиты:
\n\nКвоты — организационные ограничения, которые можно изменить по запросу в техническую поддержку. \nЛимиты — технические ограничения, обусловленные особенностями архитектуры Яндекс.Облака. Изменение лимитов невозможно. \n \nКвоты, установленные для вашего аккаунта, можно посмотреть в консоли управления .
\nПроектируя инфраструктуру в Облаке, учитывайте лимиты как предел возможностей, которые Облако может вам предоставить. Квоты — изменяемые ограничения, которые потенциально могут быть увеличены до значения лимитов.
\n Зачем нужны квоты \nКвоты служат мягким ограничением для запроса ресурсов, и позволяют Облаку гарантировать стабильность работы сервиса: новые пользователи не могут занять слишком много ресурсов в тестовых целях. Если вы готовы использовать большее количество ресурсов, обратитесь в техническую поддержку и расскажите, какие именно квоты нужно увеличить, и каким образом.
\nТехническая поддержка принимает решение увеличивать или не увеличивать квоты в индивидуальном порядке.
\n Квоты и лимиты по умолчанию для сервисов Облака \nКвоты приведены со значениями по умолчанию, которые совпадают с квотами на время пробного периода .
\n Yandex Compute Cloud \n Квоты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nКоличество виртуальных машин в одном облаке \n12 \n \n\nСуммарное количество vCPU для всех виртуальных машин в одном облаке \n32 \n \n\nСуммарный объем виртуальной памяти для всех виртуальных машин в одном облаке \n128 ГБ \n \n\nСуммарное количество дисков в одном облаке \n32 \n \n\nСуммарный объем SSD-дисков в одном облаке \n200 ГБ \n \n\nСуммарный объем HDD-дисков в одном облаке \n500 ГБ \n \n\nСуммарное количество снимков дисков в одном облаке \n32 \n \n\nСуммарный объем всех снимков дисков в одном облаке \n400 ГБ \n \n\nКоличество образов в одном облаке \n8 \n \n\nКоличество групп виртуальных машин в одном облаке \n10 \n \n\nСуммарное количество GPU для всех виртуальных машин в одном облаке* \n0 \n \n\nКоличество одновременно выполняемых операций в облаке \n15 \n \n\nМаксимальное количество ВМ в одной группе размещения \n5 \n \n\nМаксимальное количество групп размещения в одном облаке \n2 \n \n \n
\n* Чтобы создать виртуальную машину с GPU, обратитесь в техническую поддержку .
\n Лимиты виртуальных машин \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nМаксимальное количество vCPU для одной виртуальной машины \n32 и 64 для платформ Intel Broadwell и Intel Cascade Lake соответственно \n \n\nМаксимальный объем виртуальной памяти для одной виртуальной машины \n256 ГБ и 512 ГБ для платформ Intel Broadwell и Intel Cascade Lake соответственно \n \n\nМаксимальное количество дисков, подключенных к одной виртуальной машине \n7 \n \n\nМаксимальное количество GPU, подключенных к одной виртуальной машине \n4 \n \n\nМаксимальное количество vCPU для виртуальных машин с GPU \n32 \n \n\nМаксимальное количество RAM для виртуальных машин с GPU \n384 \n \n \n
\n Лимиты дисков \n\n
\n
Сетевой SSD-диск
\n
Cетевой HDD-диск
\n
\n
\n
\n\n\nВид ограничения \nЗначение \n \n \n\n\nМаксимальный размер диска \n4 ТБ \n \n\nМаксимальный размер снимка диска \n4 ТБ \n \n\nРазмер блока размещения \n32 ГБ \n \n\nМаксимальный* IOPS на запись, на 1 диск \n40000 \n \n\nМаксимальный* IOPS на запись, на блок размещения \n1000 \n \n\nМаксимальная** пропускная способность на запись, на 1 диск \n450 МБ/с \n \n\nМаксимальная** пропускная способность на запись, на блок размещения \n15 МБ/с \n \n\nМаксимальный* IOPS на чтение, на 1 диск \n12000 \n \n\nМаксимальный* IOPS на чтение, на блок размещения \n400 \n \n\nМаксимальная** пропускная способность на чтение, на 1 диск \n450 МБ/с \n \n\nМаксимальная** пропускная способность на чтение, на блок размещения \n15 МБ/с \n \n \n
\n
\n
\n
\n\n\nВид ограничения \nЗначение \n \n \n\n\nМаксимальный размер диска \n4 ТБ \n \n\nМаксимальный размер снимка диска \n4 ТБ \n \n\nРазмер блока размещения \n256 ГБ \n \n\nМаксимальный* IOPS на запись, на 1 диск \n11000 \n \n\nМаксимальный* IOPS на запись, на блок размещения \n300 \n \n\nМаксимальная** пропускная способность на запись, на 1 диск \n240 МБ/с \n \n\nМаксимальная** пропускная способность на запись, на блок размещения \n30 МБ/с \n \n\nМаксимальный* IOPS на чтение, на 1 диск \n300 \n \n\nМаксимальный* IOPS на чтение, на блок размещения \n100 \n \n\nМаксимальная** пропускная способность на чтение, на 1 диск \n240 МБ/с \n \n\nМаксимальная** пропускная способность на чтение, на блок размещения \n30 МБ/с \n \n \n
\n
\n
\nОперации чтения и записи потребляют один и тот же дисковый ресурс — чем больше производится операций чтения, тем меньше операций записи, и наоборот. Подробнее читайте в разделе Диски .
\n * \nДля получения максимального значения IOPS рекомендуется делать чтения и записи, не превышающие 4 КБ.
\n ** \nДля получения максимального значения пропускной способности рекомендуется делать чтения и записи размером 4 МБ.
\n Yandex Object Storage \n Квоты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nОбъем хранилища в одном облаке \n5 ТБ \n \n\nКоличество бакетов в одном облаке \n25 \n \n \n
\n Лимиты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nМаксимальный размер объекта \n5 TБ \n \n\nОбщий размер заголовков для 1 запроса к HTTP API \n8 KБ \n \n\nРазмер пользовательских метаданных объекта \n2 KБ \n \n\nМаксимальный размер данных для загрузки за 1 запрос \n5 ГБ \n \n\nМинимальный размер части данных для составной загрузки, кроме последнего \n5 МБ \n \n\nМаксимальное количество частей в составной загрузке \n10000 \n \n \n
\n Yandex Virtual Private Cloud \n Квоты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nКоличество облачных сетей в одном облаке \n2 \n \n\nКоличество подсетей в одном облаке \n6 \n \n\nКоличество всех публичных IP-адресов в одном облаке \n8 \n \n\nКоличество статических публичных IP-адресов в одном облаке \n2 \n \n\nКоличество таблиц маршрутизации в одном облаке \n8 \n \n\nКоличество статических маршрутов в одном облаке \n256 \n \n \n
\n Лимиты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nМинимальный размер CIDR для подсети \n/28 \n \n\nМаксимальный размер CIDR для подсети \n/16 \n \n\nМаксимальное количество одновременно установленных TCP/UDP-соединений для одной виртуальной машины \n50000 \n \n \n
\n Фильтрация исходящего трафика \nВ Яндекс.Облаке всегда блокируется исходящий трафик на TCP-порт 25:
\n\nна любые серверы в интернете, кроме почтовых серверов Яндекс.Почты; \nна виртуальные машины Yandex Compute Cloud, при обращении через публичный IP-адрес. \n \nЯндекс.Облако может открыть TCP-порт 25 по запросу в поддержку, если вы соблюдаете Правила допустимого использования . При этом Яндекс.Облако всегда может снова заблокировать исходящий трафик на TCP-порте 25, если вы нарушите Правила.
\n Yandex Resource Manager \nКвоты и лимиты для сервиса Yandex Resource Manager не определены.
\n Yandex Load Balancer \n Квоты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nКоличество балансировщиков в одном облаке \n2 \n \n\nКоличество целевых групп в одном облаке \n2 \n \n \n
\n Лимиты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nКоличество ресурсов в целевой группе \n254 \n \n\nКоличество портов обработчика \n10 \n \n\nКоличество проверок состояния на подключенную целевую группу \n1 \n \n\nПротокол проверок состояния \nTCP, HTTP \n \n \n
\n Прочие ограничения \nВ одной целевой группе могут находиться целевые ресурсы только из одной облачной сети.
\nВ пределах одной зоны доступности в целевую группу могут входить ресурсы, подключенные к одной подсети.
\nМожно создать балансировщик без обработчика.
\nПроверки состояния передаются из диапазона адресов 198.18.235.0/24
.
\nПри подключении ресурсов к балансировщику учитывайте лимит на максимальное количество одновременно установленных TCP/UDP-соединений для одной виртуальной машины.
\n Yandex Managed Service for ClickHouse \n Квоты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nКоличество кластеров в одном облаке \n16 \n \n\nСуммарное количество ядер процессора для всех хостов баз данных в одном облаке \n64 \n \n\nСуммарный объем виртуальной памяти для всех хостов баз данных в одном облаке \n512 ГБ \n \n\nСуммарный объем хранилищ для всех кластеров в одном облаке \n4096 ГБ \n \n \n
\n Лимиты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nМинимальный класс хоста \nb1.nano (5% × 2 vCPU Intel Broadwell, 2 ГБ RAM) \n \n\nМаксимальный класс хоста \nm2.8xlarge (64 vCPU Intel Cascade Lake, 512 ГБ RAM) \n \n\nМаксимальное количество шардов в одном кластере \n10 \n \n\nМаксимальное количество хостов в одном шарде \n7 \n \n\nМаксимальное количество хостов в одном кластере \n73 (10 шардов × 7 хостов + 3 хоста ZooKeeper) \n \n\nМаксимальный объем данных при использовании сетевого хранилища \n4096 ГБ \n \n\nМаксимальный объем данных при использовании локального хранилища \n1400 ГБ \n \n \n
\n Yandex Managed Service for MongoDB \n Квоты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nКоличество кластеров в одном облаке \n16 \n \n\nСуммарное количество ядер процессора для всех хостов баз данных в одном облаке \n64 \n \n\nСуммарный объем виртуальной памяти для всех хостов баз данных в одном облаке \n512 ГБ \n \n\nСуммарный объем хранилищ для всех кластеров в одном облаке \n4096 ГБ \n \n \n
\n Лимиты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nМинимальный класс хоста \nb1.nano (5% × 2 vCPU Intel Broadwell, 2 ГБ RAM) \n \n\nМаксимальный класс хоста \nm2.8xlarge (64 vCPU Intel Cascade Lake, 512 ГБ RAM) \n \n\nМаксимальное количество шардов в одном кластере MongoDB \n10 \n \n\nМаксимальное количество хостов в одном шарде \n7 \n \n\nМаксимальное количество хостов в одном кластере \n70 (10 шардов × 7 хостов) \n \n\nМаксимальный объем хранилища для кластера \n512 ГБ \n \n \n
\n Yandex Managed Service for MySQL \n Квоты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nКоличество кластеров в одном облаке \n16 \n \n\nСуммарное количество ядер процессора для всех хостов баз данных в одном облаке \n64 \n \n\nСуммарный объем виртуальной памяти для всех хостов баз данных в одном облаке \n512 ГБ \n \n\nСуммарный объем хранилищ для всех кластеров в одном облаке \n4096 ГБ \n \n \n
\n Лимиты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nМинимальный класс хоста \nb1.nano (5% × 2 vCPU Intel Broadwell, 2 ГБ RAM) \n \n\nМаксимальный класс хоста \nm2.8xlarge (64 vCPU Intel Cascade Lake, 512 ГБ RAM) \n \n\nМаксимальное количество хостов в одном кластере \n7 \n \n\nМаксимальный объем данных при использовании сетевого хранилища \n2048 ГБ \n \n\nМаксимальный объем данных при использовании локального хранилища \n1400 ГБ \n \n \n
\n Yandex Managed Service for PostgreSQL \n Квоты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nКоличество кластеров в одном облаке \n16 \n \n\nСуммарное количество ядер процессора для всех хостов баз данных в одном облаке \n64 \n \n\nСуммарный объем виртуальной памяти для всех хостов баз данных в одном облаке \n512 ГБ \n \n\nСуммарный объем хранилищ для всех кластеров в одном облаке \n4096 ГБ \n \n \n
\n Лимиты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nМинимальный класс хоста \nb1.nano (5% × 2 vCPU Intel Broadwell, 2 ГБ RAM) \n \n\nМаксимальный класс хоста \nm2.8xlarge (64 vCPU Intel Cascade Lake, 512 ГБ RAM) \n \n\nМаксимальное количество хостов в одном кластере \n7 \n \n\nМаксимальный объем данных при использовании сетевого хранилища \n2048 ГБ \n \n\nМаксимальный объем данных при использовании локального хранилища \n1400 ГБ \n \n \n
\n Yandex Managed Service for Redis \n Квоты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nКоличество кластеров в одном облаке \n16 \n \n\nСуммарное количество ядер процессора для всех хостов баз данных в одном облаке \n64 \n \n\nСуммарный объем виртуальной памяти для всех хостов в одном облаке \n512 ГБ \n \n\nСуммарный объем дисков для всех кластеров в одном облаке \n4096 ГБ \n \n \n
\n Лимиты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nМинимальный класс хоста \nb1.nano (burstable
с 2 ГБ RAM) \n \n\nМаксимальный класс хоста \nhm1.9xlarge (high-memory
с 256 ГБ RAM) \n \n\nМаксимальное количество хостов в одном кластере \n7 \n \n\nМинимальный размер диска для кластера \nВ 2 раза больше выбранного объема RAM \n \n\nМаксимальный размер диска для кластера \nВ 8 раз больше выбранного объема RAM \n \n \n
\n Yandex Message Queue \n Квоты \n Сообщения \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nКоличество вызовов SendMessage
и SendMessageBatch
на одну очередь \n300 вызовов/с для стандартной очереди 30 вызовов/с для очереди FIFO \n \n\nКоличество вызовов ReceiveMessage
на одну очередь \n300 вызовов/с для стандартной очереди 30 вызовов/с для очереди FIFO \n \n\nКоличество вызовов DeleteMessage
и DeleteMessageBatch
на одну очередь \n300 вызовов/с для стандартной очереди 30 вызовов/с для очереди FIFO \n \n\nКоличество вызовов ChangeMessageVisibility
и ChangeMessageVisibilityBatch
на одну очередь \n300 вызовов/с для стандартной очереди 30 вызовов/с для очереди FIFO \n \n\nКоличество вызовов CreateQueue
на одно облако \n2 вызова/с \n \n\nКоличество вызовов DeleteQueue
на одно облако \n5 вызовов/с \n \n\nКоличество вызовов других запросов на одно облако \n100 вызовов/с \n \n\nКоличество очередей в одном облаке \n10 \n \n \n
\n Лимиты \n Очереди \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nМинимальное время задержки доставки сообщения в очередь (параметр DelaySeconds
) \n0 секунд \n \n\nМаксимальное время задержки доставки сообщения в очередь (параметр DelaySeconds
) \n900 секунд (15 минут) \n \n\nКоличество сообщений в обработке на одну стандартную очередь \n120000 \n \n\nКоличество сообщений в обработке на одну очередь FIFO \n20000 \n \n\nИмя очереди \nНе более 80 символов: цифр, маленьких и заглавных латинских букв, дефисов и подчеркиваний. Имя очереди FIFO должно заканчиваться суффиксом .fifo
. \n \n \n
\n Сообщения \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nИдентификатор сообщения в группе \nНе более 80 символов: цифр, маленьких и заглавных латинских букв, дефисов и подчеркиваний. \n \n\nМаксимальное количество атрибутов сообщения \n10 \n \n\nМаксимальное количество сообщений в группе \n10 \n \n\nСодержимое сообщений \nXML, JSON и неформатированный текст. Поддерживаются следующие символы Unicode: #x9
#xA
#xD
от #x20
до #xD7FF
от #xE000
до #xFFFD
от #x10000
до #x10FFFF
\n \n\nМаксимальный срок хранения сообщений в очереди \n1209600 секунд (14 дней) \n \n\nМинимальный срок хранения сообщений в очереди \n60 секунд (1 минута) \n \n\nМаксимальное время задержки доставки сообщения в очередь (параметр DelaySeconds
) \n900 секунд (15 минут) \n \n\nМинимальное время задержки доставки сообщения в очередь (параметр DelaySeconds
) \n0 секунд \n \n\nМаксимальный размер сообщения \n262144 байт (256 КБ) \n \n\nМинимальный размер сообщения \n1 байт \n \n\nМаксимальный таймаут видимости сообщения \n12 часов \n \n\nМинимальный таймаут видимости сообщения \n0 секунд \n \n\nМаксимальное время ожидания клиентом сообщения в пустой очереди. (параметр WaitTimeSeconds
) \n20 секунд \n \n \n
\n Yandex SpeechKit \n Квоты \n\n Лимиты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nРаспознавание коротких аудио \n \n \n\nМаксимальный размер файла \n1 МБ \n \n\nМаксимальная длительность аудио \n30 секунд \n \n\nМаксимальное количество аудиоканалов \n1 \n \n\nПотоковый режим распознавания коротких аудио \n \n \n\nМаксимальная длительность переданного аудио за всю сессию \n5 минут \n \n\nМаксимальный размер переданных аудиоданных \n10 МБ \n \n\nМаксимальное количество аудиоканалов \n1 \n \n\nРаспознавание длинных аудио \n \n \n\nМаксимальный размер файла \n1 ГБ \n \n\nМаксимальная длительность аудио \n4 часа \n \n\nСрок хранения результатов распознавания на сервере \n3 суток \n \n\nСинтез речи \n \n \n\nМаксимальный размер запроса \n5000 символов \n \n \n
\n Yandex Translate \n Квоты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nВызовов одного метода API в секунду \n20 \n \n\nСимволов, отправленных на перевод, в час \n1 млн \n \n\nСимволов, отправленных на определение языка, в час \n1 млн \n \n \n
\n Лимиты \nЛимиты в сервисе отсутствуют. Ограничения на значения полей в теле запроса см. в справочнике API .
\n Yandex Vision \n Квоты \n\n Лимиты \n\n\n\nВид ограничения \nЗначение \n \n \n\n\nМаксимальный размер файла \n1 МБ \n \n\nМаксимальный размер изображения \n20 мегапикселей (длина × ширина) \n \n\nМаксимальное количество страниц в PDF-файле \n8 \n \n \n
\n",
+ "title": "Квоты и лимиты",
+ "headings": [
+ {
+ "title": "Зачем нужны квоты",
+ "href": "#quotas",
+ "level": 2
+ },
+ {
+ "title": "Квоты и лимиты по умолчанию для сервисов Облака",
+ "href": "#kvoty-i-limity-po-umolchaniyu-dlya-servisov-oblaka",
+ "level": 2,
+ "items": [
+ {
+ "title": "Yandex Compute Cloud",
+ "href": "#compute",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Object Storage",
+ "href": "#storage",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Virtual Private Cloud",
+ "href": "#vpc",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Resource Manager",
+ "href": "#resource-manager",
+ "level": 3
+ },
+ {
+ "title": "Yandex Load Balancer",
+ "href": "#load-balancer",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Managed Service for ClickHouse",
+ "href": "#mch",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Managed Service for MongoDB",
+ "href": "#mmg",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Managed Service for MySQL",
+ "href": "#mmy",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Managed Service for PostgreSQL",
+ "href": "#mpg",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Managed Service for Redis",
+ "href": "#mrd",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Message Queue",
+ "href": "#mq",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex SpeechKit",
+ "href": "#speechkit",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Translate",
+ "href": "#translate",
+ "level": 3,
+ "items": []
+ },
+ {
+ "title": "Yandex Vision",
+ "href": "#vision",
+ "level": 3,
+ "items": []
+ }
+ ]
+ }
+ ],
+ "meta": {
+ "author": {
+ "avatar": "https://avatars.githubusercontent.com/u/2485935?v=4",
+ "email": "",
+ "login": "robot-dataui-vcs@yandex-team.ru",
+ "name": "DataUI VCS Robot",
+ "url": "http://yandex.ru/"
+ },
+ "contributors": [
+ {
+ "avatar": "https://avatars.githubusercontent.com/u/2485945?v=6",
+ "email": "robot-dataui-vcs@yandex-team.ru",
+ "login": "",
+ "name": "DataUI VCS Robot",
+ "url": ""
+ }
+ ]
+ },
+ "toc": {
+ "title": "Обзор платформы",
+ "href": "/docs/overview/",
+ "items": [
+ {
+ "name": "Сервисы Яндекс.Облака",
+ "href": "/docs/overview/concepts/services",
+ "id": "e619422ec28a87ea25c41d4ad7e45563"
+ },
+ {
+ "name": "Сопоставление с другими платформами",
+ "expanded": true,
+ "items": [
+ {
+ "name": "Обзор",
+ "href": "/docs/overview/platform-comparison/",
+ "id": "6cb52f285fd5d6ec04cbd96854dffab6"
+ },
+ {
+ "name": "Сопоставление с Amazon Web Services",
+ "href": "/docs/overview/platform-comparison/aws",
+ "id": "1d84cf0e31f3f0968bdc6e8c2e11ba2f"
+ },
+ {
+ "name": "Сопоставление с Google Cloud Platform",
+ "href": "/docs/overview/platform-comparison/gcp",
+ "id": "931f7302b4162c44fbe9bb07cea05b1e"
+ },
+ {
+ "name": "Сопоставление с Microsoft Azure",
+ "href": "/docs/overview/platform-comparison/azure",
+ "id": "c14b119f6944d57afd8d4b3e61c37e21"
+ }
+ ],
+ "id": "fa5cef5c0b0cf236fdaa1562cbc48eb8"
+ },
+ {
+ "name": "Зоны доступности",
+ "href": "/docs/overview/concepts/geo-scope",
+ "id": "054328058d7c7b60ff85e3c95be1a264"
+ },
+ {
+ "name": "Начало работы",
+ "href": "/docs/overview/quickstart",
+ "id": "c984893c04c28bc3fe91f5ff87b79c29"
+ },
+ {
+ "name": "Стадии готовности сервисов",
+ "href": "/docs/overview/concepts/launch-stages",
+ "id": "c53a86addb3cf17f0847acde5282ad84"
+ },
+ {
+ "name": "Квоты и лимиты",
+ "href": "/docs/overview/concepts/quotas-limits",
+ "id": "56baa37ef41a57f75c4fce09722ab06c"
+ },
+ {
+ "name": "API",
+ "href": "/docs/overview/api",
+ "id": "136bdc27253376a450eee99e4539c186"
+ },
+ {
+ "name": "Безопасность и соответствие стандартам",
+ "items": [
+ {
+ "name": "Рекомендации по безопасности",
+ "href": "/docs/overview/security-bulletins/",
+ "id": "c87ac8d995548646dec5661b1b28e45c"
+ },
+ {
+ "name": "Правила проведения внешних сканирований безопасности",
+ "href": "/docs/overview/compliance/pentest",
+ "id": "544f4d243ace1fc32591d87d2460742d"
+ },
+ {
+ "name": "Безопасность платформы Яндекс.Облако",
+ "items": [
+ {
+ "name": "Обзор",
+ "href": "/docs/overview/security/",
+ "id": "022cc6311c43fdd3c2848a238c92de1e"
+ },
+ {
+ "name": "Ключевые принципы безопасности",
+ "href": "/docs/overview/security/principles",
+ "id": "fba46ca0333fec72f16d0e29fb78e239"
+ },
+ {
+ "name": "Разделение ответственности за обеспечение безопасности",
+ "href": "/docs/overview/security/respons",
+ "id": "177e184118e1e45bc890a53bf76971c7"
+ },
+ {
+ "name": "Следование лучшим практикам и стандартам",
+ "href": "/docs/overview/security/standarts",
+ "id": "778f32125645fc81b0f680d2d9d09d23"
+ },
+ {
+ "name": "Соответствие требованиям",
+ "href": "/docs/overview/security/conform",
+ "id": "4d7329262a479edc4b28a7607a31c2b4"
+ },
+ {
+ "name": "Технические меры защиты на стороне провайдера",
+ "href": "/docs/overview/security/tech-measures",
+ "id": "98cfa650bb690aa5b9a9aa66f3e7e52b"
+ },
+ {
+ "name": "Средства защиты, доступные пользователям облачных сервисов",
+ "href": "/docs/overview/security/user-side",
+ "id": "aaffdd260266d59cd003e6fcab17a1ec"
+ },
+ {
+ "name": "Полезные ресурсы",
+ "href": "/docs/overview/security/resources",
+ "id": "72071e3e599234cb4df3fe56ac378dc1"
+ }
+ ],
+ "id": "f8cca556a8df3df325a1dce2ffec52dc"
+ }
+ ],
+ "id": "7f196892c00a04c9d36a10bc2275ccb8"
+ },
+ {
+ "name": "Удаление данных пользователей",
+ "href": "/docs/overview/concepts/data-deletion",
+ "id": "66fe49c2a324ffd44541fe6a33e14ed0"
+ },
+ {
+ "name": "SLA",
+ "href": "/docs/overview/sla",
+ "id": "1636c6203504367fc47f73077e95ebad"
+ },
+ {
+ "name": "Вопросы и ответы",
+ "href": "/docs/overview/qa",
+ "id": "ff7aacc415c714c83d0ea5715eabb016"
+ }
+ ]
+ },
+ "breadcrumbs": [
+ {
+ "name": "Документация"
+ },
+ {
+ "name": "Квоты и лимиты"
+ }
+ ],
+ "filePath": "/cache/c12878f1-cc2e-445f-8f96-7374fe76b074/ru/overview/concepts/quotas-limits.md",
+ "githubUrl": "https://github.com/yandex-cloud/docs/tree/master/ru/overview/concepts/quotas-limits.md",
+ "vcsType": "github",
+ "vcsUrl": "https://github.com/yandex-cloud/docs/tree/master/ru/overview/concepts/quotas-limits.md"
+}
diff --git a/demo/src/Components/DocPage/single-page-en.json b/demo/src/Components/DocPage/single-page-en.json
new file mode 100644
index 00000000..d24f3073
--- /dev/null
+++ b/demo/src/Components/DocPage/single-page-en.json
@@ -0,0 +1,30 @@
+{
+ "html": " Get started \nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 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."
\n \n Second page \nSed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
\n",
+ "title": "",
+ "headings": [
+ {"title": "Get started", "href": "#_en_index_get-started", "level": 2},
+ {"title": "Second page", "href": "#_en_second-page_second-page", "level": 2}
+ ],
+ "meta": {},
+ "toc": {
+ "title": "Test langs",
+ "href": "/test-lang/",
+ "items": [
+ {
+ "name": "Get started",
+ "href": "/test-lang/#_en_index",
+ "id": "5b421eb9a84e5f57b317fa003da0b25c"
+ },
+ {
+ "name": "Second page",
+ "href": "/test-lang/#_en_second-page",
+ "id": "291ab0582d4b7c0831afdf82a80a8c92"
+ }
+ ],
+ "base": "en",
+ "singlePage": true
+ },
+ "breadcrumbs": [],
+ "vcsUrl": "https://github.com/yandex-cloud/docs/tree/test-lang/1/en/_single_page/index.md",
+ "vcsType": "github"
+}
diff --git a/demo/src/Components/DocPage/single-page-ru.json b/demo/src/Components/DocPage/single-page-ru.json
new file mode 100644
index 00000000..40206f88
--- /dev/null
+++ b/demo/src/Components/DocPage/single-page-ru.json
@@ -0,0 +1,30 @@
+{
+ "html": " Ознакомление \nLorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
\n \n Вторая страница \nДавно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации "Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.." Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам "lorem ipsum" сразу показывает, как много веб-страниц всё ещё дожидаются своего настоящего рождения. За прошедшие годы текст Lorem Ipsum получил много версий. Некоторые версии появились по ошибке, некоторые - намеренно (например, юмористические варианты).
\n",
+ "title": "",
+ "headings": [
+ {"title": "Ознакомление", "href": "#_ru_index_oznakomlenie", "level": 2},
+ {"title": "Вторая страница", "href": "#_ru_second-page_vtoraya-stranica", "level": 2}
+ ],
+ "meta": {},
+ "toc": {
+ "title": "Тест языков",
+ "href": "/test-lang/",
+ "items": [
+ {
+ "name": "Ознакомление",
+ "href": "/test-lang/#_ru_index",
+ "id": "7c9fe5f5f46c91e3c70bc359bef65b22"
+ },
+ {
+ "name": "Вторая страница",
+ "href": "/test-lang/#_ru_second-page",
+ "id": "dc946863157ca4b7430495cf64689a64"
+ }
+ ],
+ "base": "ru",
+ "singlePage": true
+ },
+ "breadcrumbs": [],
+ "vcsUrl": "https://github.com/yandex-cloud/docs/tree/test-lang/1/ru/_single_page/index.md",
+ "vcsType": "github"
+}
diff --git a/demo/src/Components/ErrorPage/index.tsx b/demo/src/Components/ErrorPage/index.tsx
new file mode 100644
index 00000000..96401277
--- /dev/null
+++ b/demo/src/Components/ErrorPage/index.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+
+import {ErrorPage, ERROR_CODES} from '@doc-tools/components';
+import {getIsMobile} from '../../controls/settings';
+import getLangControl from '../../controls/lang';
+import {radios, text} from '@storybook/addon-knobs';
+
+const ErrorPageDemo = () => {
+ const langValue = getLangControl();
+ const isMobile = getIsMobile();
+ const errorCode = getErrorCode();
+
+ return (
+
+
+
+ );
+};
+
+function getErrorCode() {
+ return radios('Errors', ERROR_CODES, ERROR_CODES.SERVER_ERROR);
+}
+
+export default ErrorPageDemo;
diff --git a/demo/src/Components/Feedback/index.stories.tsx b/demo/src/Components/Feedback/index.stories.tsx
new file mode 100644
index 00000000..ceef1156
--- /dev/null
+++ b/demo/src/Components/Feedback/index.stories.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+
+import {configure, Feedback} from '@doc-tools/components';
+
+configure();
+
+const FeedbackDemo = () => {
+ return (
+
+ );
+};
+
+export default {
+ title: 'Example/Feedback',
+ component: FeedbackDemo,
+ parameters: {
+ // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
+ layout: 'centered',
+ },
+ // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
+ // tags: ['autodocs'],
+ // More on argTypes: https://storybook.js.org/docs/react/api/argtypes
+ // argTypes: {
+ // backgroundColor: { control: 'color' },
+ // },
+};
+
+// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
+export const Primary = {
+ args: {
+ primary: true,
+ label: 'Button',
+ },
+};
+
diff --git a/demo/src/Components/Header/Header.scss b/demo/src/Components/Header/Header.scss
new file mode 100644
index 00000000..6fdf87dc
--- /dev/null
+++ b/demo/src/Components/Header/Header.scss
@@ -0,0 +1,5 @@
+.Header {
+ &__control-input {
+ max-width: 170px;
+ }
+}
diff --git a/demo/src/Components/Header/Header.tsx b/demo/src/Components/Header/Header.tsx
new file mode 100644
index 00000000..b571b6de
--- /dev/null
+++ b/demo/src/Components/Header/Header.tsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import cn from 'bem-cn-lite';
+import {TextInput} from '@gravity-ui/uikit';
+import {
+ Lang,
+ ControlSizes,
+ LangControl,
+ FullScreenControl,
+ DividerControl,
+} from '@doc-tools/components';
+import './Header.scss';
+
+const headBlock = cn('Header');
+const layoutBlock = cn('Layout');
+
+export interface HeaderProps {
+ lang: Lang;
+ fullScreen: boolean;
+ searchText?: string;
+ onChangeLang?: (lang: Lang) => void;
+ onChangeFullScreen?: (value: boolean) => void;
+ onChangeSearch?: (value: string) => void;
+}
+
+const Header: React.FC = ({
+ lang,
+ fullScreen,
+ searchText,
+ onChangeFullScreen,
+ onChangeLang,
+ onChangeSearch,
+}) => {
+ return (
+
+
+
+
+
+
+ {onChangeFullScreen ? (
+
+ ) : null}
+
+
+ );
+};
+
+export default Header;
diff --git a/demo/src/Components/Paginator/index.tsx b/demo/src/Components/Paginator/index.tsx
new file mode 100644
index 00000000..d9a2f1a8
--- /dev/null
+++ b/demo/src/Components/Paginator/index.tsx
@@ -0,0 +1,21 @@
+import React, {useState} from 'react';
+
+import {Paginator} from '@doc-tools/components';
+
+const PaginatorDemo = () => {
+ const [page, setPage] = useState(1);
+
+ return (
+
+ );
+};
+
+export default PaginatorDemo;
diff --git a/demo/src/Components/SearchItem/index.tsx b/demo/src/Components/SearchItem/index.tsx
new file mode 100644
index 00000000..4a7e77a5
--- /dev/null
+++ b/demo/src/Components/SearchItem/index.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+
+import {SearchItem} from '@doc-tools/components';
+import data from './page.json';
+
+const SearchItemDemo = () => {
+ return ;
+};
+
+export default SearchItemDemo;
diff --git a/demo/src/Components/SearchItem/page.json b/demo/src/Components/SearchItem/page.json
new file mode 100644
index 00000000..3e79f05f
--- /dev/null
+++ b/demo/src/Components/SearchItem/page.json
@@ -0,0 +1,5 @@
+{
+ "title": "Add a chart",
+ "description": "Follow these instruction to add a chart to your dashboard.",
+ "url": "/en/data-visualization/step-by-step/charts/add-chart"
+}
diff --git a/demo/src/Components/SearchPage/data.ts b/demo/src/Components/SearchPage/data.ts
new file mode 100644
index 00000000..a5d2cf75
--- /dev/null
+++ b/demo/src/Components/SearchPage/data.ts
@@ -0,0 +1,26 @@
+export default [
+ {
+ title: 'Lorem ipsum dolor sit amet',
+ url: '/en/data-visualization/step-by-step/charts/add-chart',
+ description:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean nec nisi eget tellus pharetra pharetra sit amet in metus. Fusce eu consequat nisl. Sed dictum porta scelerisque. Fusce auctor enim ligula, at rhoncus ligula placerat id. Aliquam venenatis cursus ante, quis feugiat tortor mattis et. Donec sed justo eu est egestas malesuada.',
+ },
+ {
+ title: 'Vivamus scelerisque dictum blandit',
+ url: '/en/data-visualization/step-by-step/charts/add-chart',
+ description:
+ 'Vivamus scelerisque dictum blandit. Curabitur metus odio, lobortis eu est id, iaculis accumsan massa. Ut et euismod nisl. Donec mollis odio at elementum sagittis. Quisque dapibus eros purus, eu vulputate nisl pellentesque et. Ut vehicula mattis euismod. Vestibulum ornare nulla vel nisi consectetur accumsan. Quisque id felis tempus, porttitor ex et, pellentesque ligula.',
+ },
+ {
+ title: 'Integer tincidunt rhoncus purus',
+ url: '/en/data-visualization/step-by-step/charts/add-chart',
+ description:
+ 'Integer tincidunt rhoncus purus, nec laoreet arcu. Ut fermentum nulla sit amet arcu tristique vulputate. Vestibulum et tempor arcu. Cras laoreet ipsum ac mi rutrum, in sagittis metus pharetra. In mi nibh, lobortis sed tempor quis, fermentu',
+ },
+ {
+ title: 'Cras aliquam et eros ut',
+ url: '/en/data-visualization/step-by-step/charts/add-chart',
+ description:
+ 'Cras aliquam et eros ut lobortis. Quisque malesuada, nunc vitae placerat varius, ante nulla pharetra libero, at fringilla magna sem non velit. Curabitur finibus mauris vitae est pellentesque, vitae molestie tortor condimentum. ',
+ },
+];
diff --git a/demo/src/Components/SearchPage/index.tsx b/demo/src/Components/SearchPage/index.tsx
new file mode 100644
index 00000000..cd9163f9
--- /dev/null
+++ b/demo/src/Components/SearchPage/index.tsx
@@ -0,0 +1,40 @@
+import React, {useState} from 'react';
+
+import {ISearchItem, SearchPage} from '@doc-tools/components';
+import {getIsMobile} from '../../controls/settings';
+import getLangControl from '../../controls/lang';
+import mockData from './data';
+
+const SearchPageDemo = () => {
+ const langValue = getLangControl();
+ const isMobile = getIsMobile();
+ const [page, setPage] = useState(1);
+ const [items, setItems] = useState(getItems(page, mockData));
+
+ function getItems(newPage: number, data: ISearchItem[]): ISearchItem[] {
+ return newPage === 1 ? data.slice(0, 2) : data.slice(2);
+ }
+
+ return (
+
+ {
+ setPage(newPage);
+ setItems(getItems(newPage, mockData));
+ }}
+ onSubmit={() => setItems(getItems(page, mockData))}
+ itemOnClick={(item) => console.log('Click on search result', item)}
+ irrelevantOnClick={(item) => console.log('Click on dislike button', item)}
+ relevantOnClick={(item) => console.log('Click on like button', item)}
+ itemsPerPage={2}
+ totalItems={mockData.length}
+ lang={langValue}
+ />
+
+ );
+};
+
+export default SearchPageDemo;
diff --git a/demo/src/controls/lang.tsx b/demo/src/controls/lang.tsx
new file mode 100644
index 00000000..92c440c4
--- /dev/null
+++ b/demo/src/controls/lang.tsx
@@ -0,0 +1,13 @@
+import {radios} from '@storybook/addon-knobs';
+import {Lang} from '@doc-tools/components';
+
+export default function getLangControl() {
+ const label = 'Language';
+ const options = {
+ ru: Lang.Ru,
+ en: Lang.En,
+ };
+ const defaultValue = Lang.Ru;
+
+ return radios(label, options, defaultValue);
+}
diff --git a/demo/src/controls/settings.tsx b/demo/src/controls/settings.tsx
new file mode 100644
index 00000000..1361508b
--- /dev/null
+++ b/demo/src/controls/settings.tsx
@@ -0,0 +1,12 @@
+import {radios} from '@storybook/addon-knobs';
+
+export function getIsMobile() {
+ const label = 'Mobile version';
+ const options = {
+ enabled: 'true',
+ disabled: 'false',
+ };
+ const defaultValue = 'false';
+
+ return radios(label, options, defaultValue);
+}
diff --git a/demo/src/controls/vcs.tsx b/demo/src/controls/vcs.tsx
new file mode 100644
index 00000000..45b12e72
--- /dev/null
+++ b/demo/src/controls/vcs.tsx
@@ -0,0 +1,13 @@
+import {radios} from '@storybook/addon-knobs';
+import {Vcs} from '@doc-tools/components';
+
+export default function getVcsControl() {
+ const label = 'VCS';
+ const options = {
+ github: Vcs.Github,
+ arcanum: Vcs.Arcanum,
+ };
+ const defaultValue = Vcs.Github;
+
+ return radios(label, options, defaultValue);
+}
diff --git a/demo/src/decorators/bookmark.ts b/demo/src/decorators/bookmark.ts
new file mode 100644
index 00000000..25c10cee
--- /dev/null
+++ b/demo/src/decorators/bookmark.ts
@@ -0,0 +1,12 @@
+import {radios} from '@storybook/addon-knobs';
+
+export function getHasBookmark() {
+ const label = 'Bookmark page';
+ const options = {
+ enabled: 'true',
+ disabled: 'false',
+ };
+ const defaultValue = 'false';
+
+ return radios(label, options, defaultValue);
+}
diff --git a/demo/src/decorators/subscribe.tsx b/demo/src/decorators/subscribe.tsx
new file mode 100644
index 00000000..d8e7b260
--- /dev/null
+++ b/demo/src/decorators/subscribe.tsx
@@ -0,0 +1,12 @@
+import {radios} from '@storybook/addon-knobs';
+
+export function getHasSubscribe() {
+ const label = 'Subscribe button';
+ const options = {
+ enabled: 'true',
+ disabled: 'false',
+ };
+ const defaultValue = 'true';
+
+ return radios(label, options, defaultValue);
+}
diff --git a/demo/src/decorators/withTheme.tsx b/demo/src/decorators/withTheme.tsx
new file mode 100644
index 00000000..f221ac0c
--- /dev/null
+++ b/demo/src/decorators/withTheme.tsx
@@ -0,0 +1,23 @@
+// import {StoryFn} from '@storybook/addons';
+import {radios} from '@storybook/addon-knobs';
+import {Theme} from '@doc-tools/components';
+
+export function getThemeSelector() {
+ const label = 'Theme';
+ const options = {
+ Light: Theme.Light,
+ Dark: Theme.Dark,
+ };
+ const defaultValue = Theme.Dark;
+
+ return radios(label, options, defaultValue);
+}
+
+export default function withTheme(story: any) {
+ const theme = getThemeSelector();
+ document.body.classList.add('yc-root');
+ document.body.classList.toggle('yc-root_theme_light', theme === 'light');
+ document.body.classList.toggle('yc-root_theme_dark', theme === 'dark');
+
+ return story();
+}
diff --git a/demo/src/reset-storybook.scss b/demo/src/reset-storybook.scss
new file mode 100644
index 00000000..e0d4fe31
--- /dev/null
+++ b/demo/src/reset-storybook.scss
@@ -0,0 +1,129 @@
+@import '../../styles/mixins';
+
+*,
+*::before,
+*::after {
+ box-sizing: inherit;
+}
+
+html,
+body {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+#root {
+ min-height: 100vh;
+}
+
+@import './variables';
+@import './mixins';
+
+body.yc-root {
+ font-feature-settings: 'liga', 'kern';
+ text-size-adjust: 100%;
+
+ /* stylelint-disable-next-line value-keyword-case */
+ text-rendering: optimizeLegibility;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin: 0;
+}
+
+h1 {
+ @include heading1();
+}
+
+h2 {
+ @include heading2();
+}
+
+h3 {
+ @include heading3();
+}
+
+h4 {
+ @include heading4();
+}
+
+h5 {
+ @include heading5();
+}
+
+h6 {
+ @include heading6();
+}
+
+p,
+div.p {
+ margin: 0 0 20px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+sub,
+sup {
+ font-size: 0.75em;
+ line-height: 0;
+}
+
+a {
+ @include link();
+}
+
+.yc-root {
+ & code[class*='language-'],
+ pre[class*='language-'] {
+ background: initial;
+ padding: initial;
+ margin: 0 0 15px;
+ text-shadow: none;
+ }
+}
+
+body.yc-root {
+ --dc-header-height: 64px;
+}
+
+.yc-root .Header {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ height: 64px;
+ padding: 0 24px;
+ background-color: var(--g-color-base-background);
+ box-shadow: inset 0 -1px 0 var(--g-color-line-generic);
+
+ &__control {
+ color: var(--g-color-text-primary);
+ }
+
+ &__control-input {
+ height: 28px;
+ border-radius: 4px;
+ border: 1px solid var(--g-color-line-generic);
+ margin-right: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &__divider {
+ margin: 0 4px;
+ }
+}
+
+.Layout__header {
+ position: sticky;
+ top: 0;
+ z-index: 101;
+}
diff --git a/demo/tsconfig.json b/demo/tsconfig.json
new file mode 100644
index 00000000..a837a784
--- /dev/null
+++ b/demo/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "@yandex-cloud/tsconfig/tsconfig",
+ "compilerOptions": {
+ "module": "esNext",
+ "allowJs": false,
+ "importHelpers": true,
+ "resolveJsonModule": true,
+ "declaration": true,
+ "jsx": "react",
+ "outDir": "build",
+ "baseUrl": "."
+ },
+ "include": ["src/**/*.ts", "src/**/*.tsx"]
+}
diff --git a/package.json b/package.json
index 5402126e..d3153085 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,10 @@
"build",
"styles"
],
+ "workspaces": [
+ ".",
+ "./demo"
+ ],
"exports": {
".": {
"import": {
@@ -106,6 +110,7 @@
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "sass": "^1.66.1",
"stylelint": "^15.10.3",
"svgo": "2.8.0",
"typescript": "^5.2.2"
diff --git a/src/.eslintrc b/src/.eslintrc
index 9e3fa058..9ecc6200 100644
--- a/src/.eslintrc
+++ b/src/.eslintrc
@@ -1,3 +1,3 @@
{
- "extends": "@yandex-cloud/eslint-config/client"
+ "extends": "@gravity-ui/eslint-config/client"
}
diff --git a/src/components/Contributors/Contributors.tsx b/src/components/Contributors/Contributors.tsx
index f472c87c..7db8e145 100644
--- a/src/components/Contributors/Contributors.tsx
+++ b/src/components/Contributors/Contributors.tsx
@@ -1,16 +1,16 @@
-import React, {useEffect} from 'react';
+import React from 'react';
+
import block from 'bem-cn-lite';
-import {useTranslation} from 'react-i18next';
+import {useTranslation} from '../../hooks';
+import {Contributor} from '../../models';
import {ContributorAvatars} from '../ContributorAvatars';
-import {Lang, Contributor} from '../../models';
import './Contributors.scss';
const b = block('contributors');
export interface ContributorsProps {
- lang: Lang;
users: Contributor[];
onlyAuthor?: boolean;
isAuthor?: boolean;
@@ -18,18 +18,8 @@ export interface ContributorsProps {
}
const Contributors: React.FC = (props) => {
- const {
- users,
- lang,
- onlyAuthor = false,
- isAuthor = false,
- translationName = 'contributors',
- } = props;
- const {t, i18n} = useTranslation(translationName);
-
- useEffect(() => {
- i18n.changeLanguage(lang);
- }, [i18n, lang]);
+ const {users, onlyAuthor = false, isAuthor = false, translationName = 'contributors'} = props;
+ const {t} = useTranslation(translationName);
return (
diff --git a/src/components/Control/Control.tsx b/src/components/Control/Control.tsx
index 8ee101bf..340ff21f 100644
--- a/src/components/Control/Control.tsx
+++ b/src/components/Control/Control.tsx
@@ -1,9 +1,10 @@
-import React, {useCallback, useState, useRef} from 'react';
+import React, {forwardRef, useCallback, useImperativeHandle, useRef} from 'react';
+
+import {Button, Popup} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
-import {Popup, Button} from '@gravity-ui/uikit';
+import {PopperPosition, usePopupState} from '../../hooks';
import {ControlSizes} from '../../models';
-import {PopperPosition} from '../../hooks';
import './Control.scss';
@@ -30,23 +31,21 @@ const ICONS_SIZES = {
[ControlSizes.L]: 20,
};
-const Control = (props: ControlProps) => {
+const Control = forwardRef((props: ControlProps, ref) => {
const {
onClick,
className,
tooltipText,
isVerticalView,
- setRef,
size = ControlSizes.M,
icon,
popupPosition,
} = props;
const controlRef = useRef
(null);
- const [isVisibleTooltip, setIsVisibleTooltip] = useState(false);
- const showTooltip = () => setIsVisibleTooltip(true);
- const hideTooltip = () => setIsVisibleTooltip(false);
+ const popupState = usePopupState({autoclose: 3000});
+
const getTooltipAlign = useCallback(() => {
if (popupPosition) {
return popupPosition;
@@ -54,16 +53,8 @@ const Control = (props: ControlProps) => {
return isVerticalView ? PopperPosition.LEFT_START : PopperPosition.BOTTOM_END;
}, [isVerticalView, popupPosition]);
- const _setRef = useCallback(
- (ref: HTMLButtonElement) => {
- controlRef.current = ref;
-
- if (setRef) {
- setRef(ref);
- }
- },
- [setRef],
- );
+
+ useImperativeHandle(ref, () => controlRef.current, [controlRef]);
const position = getTooltipAlign();
const Icon = icon;
@@ -74,9 +65,9 @@ const Control = (props: ControlProps) => {
@@ -84,18 +75,20 @@ const Control = (props: ControlProps) => {
-
- {tooltipText}
-
+ {controlRef.current && (
+
+ {tooltipText}
+
+ )}
);
-};
+});
Control.displayName = 'DCControl';
diff --git a/src/components/Controls/Controls.tsx b/src/components/Controls/Controls.tsx
index 6d350b6e..c402e5ae 100644
--- a/src/components/Controls/Controls.tsx
+++ b/src/components/Controls/Controls.tsx
@@ -1,28 +1,30 @@
-import React from 'react';
+import React, {memo} from 'react';
+
import block from 'bem-cn-lite';
-import {withTranslation, WithTranslation, WithTranslationProps} from 'react-i18next';
-import {Control} from '../Control';
+import {PopperPosition} from '../../hooks';
+import {ControlSizes, FeedbackSendData, Lang, SubscribeData, TextSizes, Theme} from '../../models';
import {Feedback, FeedbackView} from '../Feedback';
import {Subscribe, SubscribeView} from '../Subscribe';
+
+import {CommonPropsContext} from './contexts';
+
import {
+ DividerControl,
+ EditControl,
FullScreenControl,
- SettingsControl,
- SinglePageControl,
LangControl,
- DividerControl,
PdfControl,
+ SettingsControl,
+ SinglePageControl,
} from './';
-import {PopperPosition} from '../../hooks';
-import {Lang, TextSizes, Theme, FeedbackSendData, ControlSizes, SubscribeData} from '../../models';
-import EditIcon from '@gravity-ui/icons/svgs/pencil.svg';
import './Controls.scss';
const b = block('dc-controls');
export interface ControlsProps {
- lang: Lang;
+ lang?: Lang;
langs?: string[];
fullScreen?: boolean;
singlePage?: boolean;
@@ -53,207 +55,148 @@ export interface ControlsProps {
popupPosition?: PopperPosition;
}
-type ControlsInnerProps = ControlsProps & WithTranslation & WithTranslationProps;
-
-class Controls extends React.Component {
- componentDidUpdate(prevProps: ControlsProps) {
- const {i18n, lang} = this.props;
-
- if (prevProps.lang !== lang) {
- i18n.changeLanguage(lang);
- }
- }
-
- render() {
- const {lang, i18n, className, isVerticalView} = this.props;
-
- if (i18n.language !== lang) {
- i18n.changeLanguage(lang);
- }
-
- return (
-
- {this.renderCommonControls()}
- {this.renderEditLink()}
- {this.renderFeedbackControls()}
- {this.renderSubscribeControls()}
-
- );
- }
-
- private renderEditLink() {
- const {
- vcsUrl,
- vcsType,
- showEditControl,
- singlePage,
- isVerticalView,
- controlSize,
- popupPosition,
- t,
- } = this.props;
-
- if (!showEditControl || singlePage) {
- return null;
- }
-
- return (
-
-
-
-
-
-
- );
- }
-
- private renderCommonControls() {
- const {
- fullScreen,
- singlePage,
- theme,
- wideFormat,
- showMiniToc,
- textSize,
- onChangeFullScreen,
- onChangeTheme,
- onChangeShowMiniToc,
- onChangeTextSize,
- onChangeWideFormat,
- onChangeLang,
- onChangeSinglePage,
- isVerticalView,
- controlSize,
- lang,
- langs,
- popupPosition,
- pdfLink,
- } = this.props;
-
- return (
-
-
-
-
-
-
-
- );
- }
-
- private renderFeedbackControls = () => {
- const {
- lang,
- singlePage,
- onSendFeedback,
- isLiked,
- isDisliked,
- dislikeVariants,
- isVerticalView,
- hideFeedbackControls,
- popupPosition,
- } = this.props;
-
- if (singlePage || !onSendFeedback || hideFeedbackControls) {
- return null;
- }
-
- return (
-
-
-
-
- );
- };
-
- private renderSubscribeControls = () => {
- const {lang, singlePage, onSubscribe, isVerticalView, popupPosition} = this.props;
-
- if (singlePage || !onSubscribe) {
- return null;
- }
-
- return (
-
-
-
-
- );
- };
-}
-
-export default withTranslation('controls')(Controls);
+type Defined = {
+ [P in keyof ControlsProps]-?: ControlsProps[P];
+};
+
+const Controls = memo((props) => {
+ const {
+ className,
+ fullScreen,
+ singlePage,
+ theme,
+ wideFormat,
+ showMiniToc,
+ showEditControl,
+ hideFeedbackControls,
+ textSize,
+ onChangeFullScreen,
+ onChangeTheme,
+ onChangeShowMiniToc,
+ onChangeTextSize,
+ onChangeWideFormat,
+ onChangeLang,
+ onChangeSinglePage,
+ onSendFeedback,
+ onSubscribe,
+ isVerticalView = false,
+ controlSize = ControlSizes.M,
+ popupPosition,
+ lang,
+ langs,
+ pdfLink,
+ vcsUrl,
+ vcsType,
+ isLiked,
+ isDisliked,
+ dislikeVariants,
+ } = props;
+
+ const withFullscreenControl = Boolean(onChangeFullScreen);
+ const withSettingsControl = Boolean(
+ onChangeWideFormat && onChangeTheme && onChangeShowMiniToc && onChangeTextSize,
+ );
+ const withLangControl = Boolean(lang && onChangeLang);
+ const withSinglePageControl = Boolean(onChangeSinglePage);
+ const withPdfControl = Boolean(pdfLink);
+ const withEditControl = Boolean(!singlePage && showEditControl && vcsUrl);
+ const withFeedbackControl = Boolean(!singlePage && onSendFeedback && !hideFeedbackControls);
+ const withSubscribeControls = Boolean(!singlePage && onSubscribe);
+
+ const controls = [
+ withFullscreenControl && (
+
+ ),
+ withSettingsControl && (
+
+ ),
+ withLangControl && (
+
+ ),
+ withSinglePageControl && (
+
+ ),
+ withPdfControl && ,
+ '---',
+ withEditControl && (
+
+ ),
+ '---',
+ withFeedbackControl && (
+
+ ),
+ '---',
+ withSubscribeControls && (
+
+ ),
+ ]
+ .filter(Boolean)
+ .reduce((result, control, index, array) => {
+ if (control === '---') {
+ if (array[index - 1] && array[index + 1] && array[index + 1] !== '---') {
+ result.push(
+ ,
+ );
+ }
+ } else {
+ result.push(control as React.ReactElement);
+ }
+
+ return result;
+ }, [] as React.ReactElement[]);
+
+ return (
+
+ {controls}
+
+ );
+});
+
+Controls.displayName = 'DCControls';
+
+export default Controls;
diff --git a/src/components/Controls/contexts.ts b/src/components/Controls/contexts.ts
new file mode 100644
index 00000000..c7bb390d
--- /dev/null
+++ b/src/components/Controls/contexts.ts
@@ -0,0 +1,16 @@
+import {createContext} from 'react';
+
+import {PopperPosition} from '../../hooks';
+import {ControlSizes} from '../../models';
+
+export const CommonPropsContext = createContext<{
+ isVerticalView?: boolean;
+ controlClassName?: string;
+ controlSize?: ControlSizes;
+ popupPosition?: PopperPosition;
+}>({
+ controlClassName: '',
+ isVerticalView: false,
+ controlSize: ControlSizes.M,
+ // popupPosition?: PopperPosition;
+});
diff --git a/src/components/Controls/single-controls/DividerControl/DividerControl.tsx b/src/components/Controls/single-controls/DividerControl/DividerControl.tsx
index 9c58c159..963f37d1 100644
--- a/src/components/Controls/single-controls/DividerControl/DividerControl.tsx
+++ b/src/components/Controls/single-controls/DividerControl/DividerControl.tsx
@@ -1,24 +1,21 @@
-import React from 'react';
+import React, {useContext} from 'react';
+
import cn from 'bem-cn-lite';
-import {ControlSizes} from '../../../../models';
+import {CommonPropsContext} from '../../contexts';
import './DividerControl.scss';
const b = cn('dc-divider-control');
interface DividerControlProps {
- size?: ControlSizes;
className?: string;
- isVerticalView?: boolean;
}
-const DividerControl = ({
- size = ControlSizes.M,
- className,
- isVerticalView = true,
-}: DividerControlProps) => {
- return
;
+const DividerControl: React.FC = ({className}) => {
+ const {isVerticalView, controlSize} = useContext(CommonPropsContext);
+
+ return
;
};
export default DividerControl;
diff --git a/src/components/Controls/single-controls/EditControl.tsx b/src/components/Controls/single-controls/EditControl.tsx
new file mode 100644
index 00000000..97d4d886
--- /dev/null
+++ b/src/components/Controls/single-controls/EditControl.tsx
@@ -0,0 +1,40 @@
+import React, {memo, useContext} from 'react';
+
+import block from 'bem-cn-lite';
+
+import {useTranslation} from '../../../hooks';
+import {Control} from '../../Control';
+import {CommonPropsContext} from '../contexts';
+
+import EditIcon from '@gravity-ui/icons/svgs/pencil.svg';
+
+interface EditControlProps {
+ vcsUrl: string;
+ vcsType: string;
+}
+
+const b = block('dc-controls');
+
+const EditControl = memo((props) => {
+ const {t} = useTranslation('controls');
+ const {controlClassName, controlSize, isVerticalView, popupPosition} =
+ useContext(CommonPropsContext);
+ const {vcsUrl, vcsType} = props;
+
+ return (
+
+
+
+ );
+});
+
+EditControl.displayName = 'EditControl';
+
+export default EditControl;
diff --git a/src/components/Controls/single-controls/FullScreenControl.tsx b/src/components/Controls/single-controls/FullScreenControl.tsx
index df913e4b..3f1fba14 100644
--- a/src/components/Controls/single-controls/FullScreenControl.tsx
+++ b/src/components/Controls/single-controls/FullScreenControl.tsx
@@ -1,27 +1,24 @@
-import React, {useCallback, useEffect} from 'react';
-import {WithTranslation, withTranslation, WithTranslationProps} from 'react-i18next';
-import {Icon as IconComponent} from '@gravity-ui/uikit';
+import React, {memo, useCallback, useContext, useEffect} from 'react';
+import {Icon} from '@gravity-ui/uikit';
+
+import {useTranslation} from '../../../hooks';
import {Control} from '../../Control';
-import {ControlSizes, Lang} from '../../../models';
+import {CommonPropsContext} from '../contexts';
import FullScreenClickedIcon from '../../../../assets/icons/full-screen-clicked.svg';
import FullScreenIcon from '@gravity-ui/icons/svgs/square-dashed.svg';
interface ControlProps {
- lang: Lang;
value?: boolean;
onChange?: (value: boolean) => void;
- isVerticalView?: boolean;
- className?: string;
- size?: ControlSizes;
- popupPosition?: PopperPosition;
}
-type ControlInnerProps = ControlProps & WithTranslation & WithTranslationProps;
-
-const FullScreenControl = (props: ControlInnerProps) => {
- const {className, isVerticalView, size, value, onChange, lang, popupPosition, i18n, t} = props;
+const FullScreenControl = memo((props) => {
+ const {t} = useTranslation('controls');
+ const {controlClassName, controlSize, isVerticalView, popupPosition} =
+ useContext(CommonPropsContext);
+ const {value, onChange} = props;
const onClick = useCallback(() => {
if (onChange) {
@@ -46,29 +43,23 @@ const FullScreenControl = (props: ControlInnerProps) => {
};
}, [onKeyDown]);
- useEffect(() => {
- i18n.changeLanguage(lang);
- }, [i18n, lang]);
-
const activeMode = value ? 'enabled' : 'disabled';
- if (!onChange) {
- return null;
- }
-
return (
(
-
+
)}
popupPosition={popupPosition}
/>
);
-};
+});
+
+FullScreenControl.displayName = 'FullScreenControl';
-export default withTranslation('controls')(FullScreenControl);
+export default FullScreenControl;
diff --git a/src/components/Controls/single-controls/LangControl.tsx b/src/components/Controls/single-controls/LangControl.tsx
index 8fe65c8a..3d4fa0c6 100644
--- a/src/components/Controls/single-controls/LangControl.tsx
+++ b/src/components/Controls/single-controls/LangControl.tsx
@@ -1,18 +1,25 @@
-import React, {useCallback, useEffect, useState, useRef} from 'react';
-import {WithTranslation, withTranslation, WithTranslationProps} from 'react-i18next';
-import allLangs from 'langs';
-import {Popup, Icon as IconComponent, List} from '@gravity-ui/uikit';
+import React, {useCallback, useContext, useMemo, useRef} from 'react';
+
+import {Icon, List, Popup} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
+import allLangs from 'langs';
+import {usePopupState, useTranslation} from '../../../hooks';
+import {Lang} from '../../../models';
import {Control} from '../../Control';
-import {ControlSizes, Lang} from '../../../models';
+import {CommonPropsContext} from '../contexts';
+
import {getPopupPosition} from './utils';
-import {PopperPosition} from '../../../hooks';
import LangIcon from '@gravity-ui/icons/svgs/globe.svg';
import '../Controls.scss';
+const ICONS: Record = {
+ en: '🇬🇧',
+ ru: '🇷🇺',
+};
+const DEFAULT_LANGS = ['en', 'ru'];
const LEGACY_LANG_ITEMS = [
{value: Lang.En, text: 'English', icon: '🇬🇧'},
{value: Lang.Ru, text: 'Русский', icon: '🇷🇺'},
@@ -23,11 +30,7 @@ const b = block('dc-controls');
interface ControlProps {
lang: Lang;
langs?: string[];
- isVerticalView?: boolean;
- className?: string;
- size?: ControlSizes;
- onChangeLang?: (lang: Lang) => void;
- popupPosition?: PopperPosition;
+ onChangeLang: (lang: Lang) => void;
}
interface ListItem {
@@ -38,37 +41,16 @@ interface ListItem {
const LIST_ITEM_HEIGHT = 36;
-type ControlInnerProps = ControlProps & WithTranslation & WithTranslationProps;
-
-const LangControl = (props: ControlInnerProps) => {
- const {
- className,
- isVerticalView,
- size,
- lang,
- langs = [],
- i18n,
- onChangeLang,
- popupPosition,
- t,
- } = props;
-
- const [langItems, setLangItems] = useState(LEGACY_LANG_ITEMS);
+const LangControl = (props: ControlProps) => {
+ const {t} = useTranslation('controls');
+ const {controlClassName, controlSize, isVerticalView, popupPosition} =
+ useContext(CommonPropsContext);
+ const {lang, langs = DEFAULT_LANGS, onChangeLang} = props;
+
const controlRef = useRef(null);
- const [isVisiblePopup, setIsVisiblePopup] = useState(false);
- const showPopup = () => setIsVisiblePopup(true);
- const hidePopup = () => setIsVisiblePopup(false);
-
- const _onChangeLang = useCallback(
- (value: Lang) => {
- if (onChangeLang) {
- onChangeLang(value);
- }
- },
- [onChangeLang],
- );
- useEffect(() => {
+ const popupState = usePopupState();
+ const langItems = useMemo(() => {
const preparedLangs = langs
.map((code) => {
const langData = allLangs.where('1', code);
@@ -77,29 +59,27 @@ const LangControl = (props: ControlInnerProps) => {
? {
text: langData.name,
value: langData['1'],
+ icon: ICONS[code] || '',
}
: undefined;
})
.filter(Boolean) as ListItem[];
- if (preparedLangs.length) {
- setLangItems(preparedLangs);
- } else {
- setLangItems(LEGACY_LANG_ITEMS);
- }
+ return preparedLangs.length ? preparedLangs : LEGACY_LANG_ITEMS;
}, [langs]);
-
- useEffect(() => {
- i18n.changeLanguage(lang);
- }, [i18n, lang]);
-
- const setRef = useCallback((ref: HTMLButtonElement) => {
- controlRef.current = ref;
+ const renderItem = useCallback((item: ListItem) => {
+ return (
+
+
{item.icon}
{item.text}
+
+ );
}, []);
-
- if (!onChangeLang) {
- return null;
- }
+ const onItemClick = useCallback(
+ (item: ListItem) => {
+ onChangeLang(item.value as Lang);
+ },
+ [onChangeLang],
+ );
const itemsHeight = LIST_ITEM_HEIGHT * langItems.length;
const selectedItemIndex = langItems.findIndex(({value}) => value === lang);
@@ -107,42 +87,36 @@ const LangControl = (props: ControlInnerProps) => {
return (
}
- setRef={setRef}
+ icon={(args) => }
popupPosition={popupPosition}
/>
-
- {
- _onChangeLang(item.value as Lang);
- }}
- selectedItemIndex={selectedItemIndex}
- itemHeight={LIST_ITEM_HEIGHT}
- itemsHeight={itemsHeight}
- renderItem={(item) => {
- return (
-
-
{item.icon}
{item.text}
-
- );
- }}
- />
-
+ {popupState.visible && (
+
+
+
+ )}
);
};
-export default withTranslation('controls')(LangControl);
+export default LangControl;
diff --git a/src/components/Controls/single-controls/PdfControl.tsx b/src/components/Controls/single-controls/PdfControl.tsx
index 1b5a4acb..51c27cb0 100644
--- a/src/components/Controls/single-controls/PdfControl.tsx
+++ b/src/components/Controls/single-controls/PdfControl.tsx
@@ -1,47 +1,37 @@
-import React, {useEffect} from 'react';
-import {WithTranslation, withTranslation, WithTranslationProps} from 'react-i18next';
-import {Icon as IconComponent} from '@gravity-ui/uikit';
+import React, {memo, useContext} from 'react';
+import {Icon} from '@gravity-ui/uikit';
+
+import {useTranslation} from '../../../hooks';
import {Control} from '../../Control';
-import {ControlSizes, Lang} from '../../../models';
-import {PopperPosition} from '../../../hooks';
+import {CommonPropsContext} from '../contexts';
import PdfIcon from '../../../../assets/icons/pdf.svg';
interface ControlProps {
- lang: Lang;
- pdfLink?: string;
- isVerticalView?: boolean;
- className?: string;
- size?: ControlSizes;
- popupPosition?: PopperPosition;
+ pdfLink: string;
}
-type ControlInnerProps = ControlProps & WithTranslation & WithTranslationProps;
-
-const PdfControl = (props: ControlInnerProps) => {
- const {className, isVerticalView, size, pdfLink, lang, i18n, popupPosition, t} = props;
-
- useEffect(() => {
- i18n.changeLanguage(lang);
- }, [i18n, lang]);
-
- if (!pdfLink) {
- return null;
- }
+const PdfControl = memo((props) => {
+ const {t} = useTranslation('controls');
+ const {controlClassName, controlSize, isVerticalView, popupPosition} =
+ useContext(CommonPropsContext);
+ const {pdfLink} = props;
return (
}
+ icon={(args) => }
popupPosition={popupPosition}
/>
);
-};
+});
+
+PdfControl.displayName = 'PdfControl';
-export default withTranslation('controls')(PdfControl);
+export default PdfControl;
diff --git a/src/components/Controls/single-controls/SettingsControl/SettingsControl.tsx b/src/components/Controls/single-controls/SettingsControl/SettingsControl.tsx
index 7a04f6e3..d59602c1 100644
--- a/src/components/Controls/single-controls/SettingsControl/SettingsControl.tsx
+++ b/src/components/Controls/single-controls/SettingsControl/SettingsControl.tsx
@@ -1,12 +1,12 @@
-import React, {useCallback, useEffect, useState, useRef} from 'react';
-import {WithTranslation, withTranslation, WithTranslationProps} from 'react-i18next';
+import React, {ReactElement, useCallback, useContext, useRef, useState} from 'react';
+
+import {Button, Icon, List, Popup, Switch} from '@gravity-ui/uikit';
import cn from 'bem-cn-lite';
-import {Button, Popup, Switch, List, Icon as IconComponent} from '@gravity-ui/uikit';
+import {useTranslation} from '../../../../hooks';
+import {TextSizes, Theme} from '../../../../models';
import {Control} from '../../../Control';
-import {ControlSizes, Lang, TextSizes, Theme} from '../../../../models';
-import {PopperPosition} from '../../../../hooks';
-
+import {CommonPropsContext} from '../../contexts';
import {getPopupPosition} from '../utils';
import SettingsIcon from '@gravity-ui/icons/svgs/gear.svg';
@@ -23,32 +23,23 @@ interface ControlProps {
showMiniToc?: boolean;
theme?: Theme;
textSize?: TextSizes;
- lang: Lang;
- isVerticalView?: boolean;
- className?: string;
- size?: ControlSizes;
onChangeWideFormat?: (value: boolean) => void;
onChangeShowMiniToc?: (value: boolean) => void;
onChangeTheme?: (theme: Theme) => void;
onChangeTextSize?: (textSize: TextSizes) => void;
- popupPosition?: PopperPosition;
}
-type ControlInnerProps = ControlProps & WithTranslation & WithTranslationProps;
-
interface SettingControlItem {
text: string;
description: string;
- control: Element;
+ control: ReactElement;
}
-const SettingsControl = (props: ControlInnerProps) => {
+const SettingsControl = (props: ControlProps) => {
+ const {t} = useTranslation('controls');
+ const {controlClassName, controlSize, isVerticalView, popupPosition} =
+ useContext(CommonPropsContext);
const {
- className,
- isVerticalView,
- size,
- lang,
- i18n,
textSize,
theme,
wideFormat,
@@ -59,8 +50,6 @@ const SettingsControl = (props: ControlInnerProps) => {
onChangeWideFormat,
onChangeShowMiniToc,
onChangeTextSize,
- popupPosition,
- t,
} = props;
const controlRef = useRef(null);
@@ -69,7 +58,7 @@ const SettingsControl = (props: ControlInnerProps) => {
const hidePopup = () => setIsVisiblePopup(false);
const makeOnChangeTextSize = useCallback(
- (textSizeKey) => () => {
+ (textSizeKey: TextSizes) => () => {
if (onChangeTextSize) {
onChangeTextSize(textSizeKey);
}
@@ -176,31 +165,19 @@ const SettingsControl = (props: ControlInnerProps) => {
makeOnChangeTextSize,
]);
- useEffect(() => {
- i18n.changeLanguage(lang);
- }, [i18n, lang]);
-
- const setRef = useCallback((ref: HTMLButtonElement) => {
- controlRef.current = ref;
- }, []);
-
- if (!(onChangeWideFormat || onChangeTheme || onChangeShowMiniToc || onChangeTextSize)) {
- return null;
- }
-
const settingsItems = getSettingsItems();
return (
}
+ icon={(args) => }
/>
{
itemHeight={ITEM_HEIGHT}
itemsHeight={ITEM_HEIGHT * settingsItems.length}
filterable={false}
- renderItem={(item) => {
+ renderItem={(item: SettingControlItem) => {
return (
@@ -234,4 +211,4 @@ const SettingsControl = (props: ControlInnerProps) => {
);
};
-export default withTranslation('controls')(SettingsControl);
+export default SettingsControl;
diff --git a/src/components/Controls/single-controls/SinglePageControl.tsx b/src/components/Controls/single-controls/SinglePageControl.tsx
index bed7cd43..170cff1e 100644
--- a/src/components/Controls/single-controls/SinglePageControl.tsx
+++ b/src/components/Controls/single-controls/SinglePageControl.tsx
@@ -1,58 +1,46 @@
-import React, {useCallback, useEffect} from 'react';
-import {WithTranslation, withTranslation, WithTranslationProps} from 'react-i18next';
-import {Icon as IconComponent} from '@gravity-ui/uikit';
+import React, {memo, useCallback, useContext} from 'react';
+import {Icon} from '@gravity-ui/uikit';
+
+import {useTranslation} from '../../../hooks';
import {Control} from '../../Control';
-import {ControlSizes, Lang} from '../../../models';
-import {PopperPosition} from '../../../hooks';
+import {CommonPropsContext} from '../contexts';
-import SinglePageIcon from '../../../../assets/icons/single-page.svg';
import SinglePageClickedIcon from '../../../../assets/icons/single-page-clicked.svg';
+import SinglePageIcon from '../../../../assets/icons/single-page.svg';
interface ControlProps {
- lang: Lang;
value?: boolean;
- onChange?: (value: boolean) => void;
- isVerticalView?: boolean;
- className?: string;
- size?: ControlSizes;
- popupPosition?: PopperPosition;
+ onChange: (value: boolean) => void;
}
-type ControlInnerProps = ControlProps & WithTranslation & WithTranslationProps;
-
-const SinglePageControl = (props: ControlInnerProps) => {
- const {className, isVerticalView, size, value, onChange, lang, i18n, popupPosition, t} = props;
+const SinglePageControl = memo
((props) => {
+ const {t} = useTranslation('controls');
+ const {controlClassName, controlSize, isVerticalView, popupPosition} =
+ useContext(CommonPropsContext);
+ const {value, onChange} = props;
const onClick = useCallback(() => {
- if (onChange) {
- onChange(!value);
- }
+ onChange(!value);
}, [value, onChange]);
- useEffect(() => {
- i18n.changeLanguage(lang);
- }, [i18n, lang]);
-
const activeMode = value ? 'enabled' : 'disabled';
- if (!onChange) {
- return null;
- }
-
return (
(
-
+
)}
popupPosition={popupPosition}
/>
);
-};
+});
+
+SinglePageControl.displayName = 'SinglePageControl';
-export default withTranslation('controls')(SinglePageControl);
+export default SinglePageControl;
diff --git a/src/components/Controls/single-controls/index.ts b/src/components/Controls/single-controls/index.ts
index b0c435a8..e3211962 100644
--- a/src/components/Controls/single-controls/index.ts
+++ b/src/components/Controls/single-controls/index.ts
@@ -4,3 +4,4 @@ export {default as SinglePageControl} from './SinglePageControl';
export {default as LangControl} from './LangControl';
export {default as DividerControl} from './DividerControl/DividerControl';
export {default as PdfControl} from './PdfControl';
+export {default as EditControl} from './EditControl';
diff --git a/src/components/DocLayout/DocLayout.tsx b/src/components/DocLayout/DocLayout.tsx
index ad9ee9f7..6e8bbc70 100644
--- a/src/components/DocLayout/DocLayout.tsx
+++ b/src/components/DocLayout/DocLayout.tsx
@@ -2,7 +2,7 @@ import React, {PropsWithChildren, ReactElement} from 'react';
import block from 'bem-cn-lite';
-import {TocData, Router, Lang} from '../../models';
+import {Router, TocData} from '../../models';
import {getStateKey} from '../../utils';
import {Toc} from '../Toc';
@@ -17,7 +17,6 @@ const Right: React.FC = () => null;
export interface DocLayoutProps {
toc: TocData;
router: Router;
- lang: Lang;
children: (ReactElement | null)[] | ReactElement;
fullScreen?: boolean;
hideRight?: boolean;
@@ -104,7 +103,6 @@ export class DocLayout extends React.Component {
wideFormat,
hideTocHeader,
hideToc,
- lang,
singlePage,
onChangeSinglePage,
pdfLink,
@@ -124,7 +122,6 @@ export class DocLayout extends React.Component {
headerHeight={headerHeight}
tocTitleIcon={tocTitleIcon}
hideTocHeader={hideTocHeader}
- lang={lang}
singlePage={singlePage}
onChangeSinglePage={onChangeSinglePage}
pdfLink={pdfLink}
diff --git a/src/components/DocLeadingPage/DocLeadingPage.tsx b/src/components/DocLeadingPage/DocLeadingPage.tsx
index eea825f7..786bd58e 100644
--- a/src/components/DocLeadingPage/DocLeadingPage.tsx
+++ b/src/components/DocLeadingPage/DocLeadingPage.tsx
@@ -2,8 +2,8 @@ import React from 'react';
import block from 'bem-cn-lite';
-import {DocLeadingPageData, DocLeadingLinks, Router, Lang} from '../../models';
import {DEFAULT_SETTINGS} from '../../constants';
+import {DocLeadingLinks, DocLeadingPageData, Router} from '../../models';
import {DocLayout} from '../DocLayout';
import {DocPageTitle} from '../DocPageTitle';
import {HTML} from '../HTML';
@@ -17,7 +17,6 @@ const {wideFormat: defaultWideFormat} = DEFAULT_SETTINGS;
export interface DocLeadingPageProps extends DocLeadingPageData {
router: Router;
- lang: Lang;
headerHeight?: number;
wideFormat?: boolean;
hideTocHeader?: boolean;
@@ -106,7 +105,6 @@ export const DocLeadingPage: React.FC = ({
data: {title, description, links},
toc,
router,
- lang,
headerHeight,
wideFormat = defaultWideFormat,
hideTocHeader,
@@ -122,7 +120,6 @@ export const DocLeadingPage: React.FC = ({
{
const {
toc,
router,
- lang,
headerHeight,
wideFormat,
fullScreen,
@@ -155,7 +154,6 @@ class DocPage extends React.Component {
{
}
private addLinksToOriginalArticle = () => {
- const {singlePage, lang, convertPathToOriginalArticle, generatePathToVcs} = this.props;
+ const {singlePage, convertPathToOriginalArticle, generatePathToVcs} = this.props;
if (singlePage) {
const elements = document.querySelectorAll('[data-original-article]');
@@ -291,7 +289,8 @@ class DocPage extends React.Component {
const vcsHref = callSafe(generatePathToVcs, href);
const linkToVcs = createElementFromHTML(
ReactDOMServer.renderToStaticMarkup(
- ,
+ // FIXME: add common context
+ ,
),
);
linkWrapperEl.append(linkToVcs);
@@ -373,7 +372,7 @@ class DocPage extends React.Component {
}
private renderAuthor(onlyAuthor: boolean) {
- const {meta, lang} = this.props;
+ const {meta} = this.props;
if (!isContributor(meta?.author)) {
return null;
@@ -381,7 +380,6 @@ class DocPage extends React.Component {
return (
{
}
private renderContributors() {
- const {meta, lang} = this.props;
+ const {meta} = this.props;
if (!meta?.contributors || meta.contributors.length === 0) {
return null;
}
- return ;
+ return ;
}
private renderContentMiniToc() {
@@ -463,7 +461,7 @@ class DocPage extends React.Component {
}
private renderAsideMiniToc() {
- const {headings, router, headerHeight, lang} = this.props;
+ const {headings, router, headerHeight} = this.props;
const {keyDOM} = this.state;
return (
@@ -472,7 +470,6 @@ class DocPage extends React.Component {
headings={headings}
router={router}
headerHeight={headerHeight}
- lang={lang}
key={keyDOM}
/>
@@ -480,29 +477,16 @@ class DocPage extends React.Component
{
}
private renderFeedback() {
- const {
- toc,
- lang,
- singlePage,
- isLiked,
- isDisliked,
- onSendFeedback,
- dislikeVariants,
- hideFeedbackControls,
- } = this.props;
+ const {toc, isLiked, isDisliked, onSendFeedback, dislikeVariants, hideFeedbackControls} =
+ this.props;
- if (!toc || toc.singlePage || hideFeedbackControls) {
+ if (!toc || toc.singlePage || hideFeedbackControls || !onSendFeedback) {
return null;
}
- const isVerticalView = this.getIsVerticalView();
-
return (
{
}
private renderTocNavPanel() {
- const {toc, router, fullScreen, lang} = this.props;
+ const {toc, router, fullScreen} = this.props;
if (!toc || toc.singlePage) {
return null;
@@ -523,7 +507,6 @@ class DocPage extends React.Component {
return (
{
onClickPrevSearch,
onClickNextSearch,
onCloseSearchBar,
- lang,
singlePage,
} = this.props;
@@ -567,7 +549,6 @@ class DocPage extends React.Component {
return (
{
- componentDidUpdate(prevProps: EditButtonProps) {
- const {i18n, lang} = this.props;
- if (prevProps.lang !== lang) {
- i18n.changeLanguage(lang);
- }
- }
-
- render() {
- const {t, href} = this.props;
-
- return (
-
-
-
-
-
- {t('edit-text')}
-
-
- );
- }
-}
+const EditButton = memo(({href}) => {
+ const {t} = useTranslation('controls');
+
+ return (
+
+
+
+
+
+ {t('edit-text')}
+
+
+ );
+});
+
+EditButton.displayName = 'EditButton';
-export default withTranslation('controls')(EditButton);
+export default EditButton;
diff --git a/src/components/ErrorPage/ErrorPage.tsx b/src/components/ErrorPage/ErrorPage.tsx
index 980ea549..6277d29f 100644
--- a/src/components/ErrorPage/ErrorPage.tsx
+++ b/src/components/ErrorPage/ErrorPage.tsx
@@ -2,10 +2,9 @@ import React from 'react';
import {Button, Link} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
-import {withTranslation, WithTranslation, WithTranslationProps} from 'react-i18next';
-import {Lang} from '../../models';
import {ERROR_CODES} from '../../constants';
+import {useTranslation} from '../../hooks';
import './ErrorPage.scss';
@@ -13,29 +12,20 @@ const b = block('ErrorPage');
export interface ErrorPageProps {
code?: number;
- lang?: Lang;
pageGroup?: string;
homeUrl?: string;
receiveAccessUrl?: string;
}
-type ErrorPagePropsInnerProps = ErrorPageProps & WithTranslation & WithTranslationProps;
-
-const ErrorPage = ({
+const ErrorPage: React.FC = ({
code = 500,
- lang = Lang.En,
- i18n,
- t,
homeUrl,
receiveAccessUrl,
pageGroup,
-}: ErrorPagePropsInnerProps): JSX.Element => {
- if (i18n.language !== lang) {
- i18n.changeLanguage(lang);
- }
-
+}) => {
let title;
let description;
+ const {t} = useTranslation('error');
const href = homeUrl || '/';
const homeLink = (
@@ -91,4 +81,4 @@ const ErrorPage = ({
);
};
-export default withTranslation('error')(ErrorPage);
+export default ErrorPage;
diff --git a/src/components/Feedback/Feedback.scss b/src/components/Feedback/Feedback.scss
index b3bea843..92469aaf 100644
--- a/src/components/Feedback/Feedback.scss
+++ b/src/components/Feedback/Feedback.scss
@@ -39,12 +39,6 @@ $popupWidth: 320px;
}
}
- &__like-button {
- &_active {
- color: var(--g-color-base-brand);
- }
- }
-
&__success-popup {
padding: $popupPadding 50px $popupPadding $popupPadding;
width: $popupWidth;
@@ -145,10 +139,4 @@ $popupWidth: 320px;
}
}
}
-
- &__feedback-button {
- &_active {
- color: var(--g-color-base-brand);
- }
- }
}
diff --git a/src/components/Feedback/Feedback.tsx b/src/components/Feedback/Feedback.tsx
index 08ef49f3..b091f0d1 100644
--- a/src/components/Feedback/Feedback.tsx
+++ b/src/components/Feedback/Feedback.tsx
@@ -1,15 +1,14 @@
-import React, {useCallback, useState, useEffect, useRef} from 'react';
+import React, {PropsWithChildren, useCallback, useEffect, useRef, useState} from 'react';
+
import block from 'bem-cn-lite';
-import {withTranslation, WithTranslation, WithTranslationProps} from 'react-i18next';
-import {Checkbox, Popup, TextArea, Button, Icon as IconComponent} from '@gravity-ui/uikit';
-import {Control} from '../Control';
-import {PopperPosition} from '../../hooks';
-import {FeedbackSendData, FeedbackType, Lang} from '../../models';
-import {DISLIKE_VARIANTS} from '../../constants';
+import {usePopupState, useTranslation} from '../../hooks';
+import {FeedbackSendData, FeedbackType} from '../../models';
-import DislikeActiveIcon from '@gravity-ui/icons/svgs/thumbs-down-fill.svg';
-import DislikeIcon from '@gravity-ui/icons/svgs/thumbs-down.svg';
+import DislikeControl from './controls/DislikeControl';
+import DislikeVariantsPopup, {FormData} from './controls/DislikeVariantsPopup';
+import LikeControl from './controls/LikeControl';
+import SuccessPopup from './controls/SuccessPopup';
import './Feedback.scss';
@@ -21,455 +20,144 @@ export enum FeedbackView {
}
export interface FeedbackProps {
- lang: Lang;
- singlePage?: boolean;
isLiked?: boolean;
isDisliked?: boolean;
dislikeVariants?: string[];
- isVerticalView?: boolean;
- onSendFeedback?: (data: FeedbackSendData) => void;
+ onSendFeedback: (data: FeedbackSendData) => void;
view?: FeedbackView;
- classNameControl?: string;
- popupPosition?: PopperPosition;
-}
-
-interface FeedbackCheckboxes {
- [key: string]: boolean;
}
-type FeedbackInnerProps = FeedbackProps & WithTranslation & WithTranslationProps;
+const getInnerState = (isLiked: boolean, isDisliked: boolean) => {
+ return isDisliked
+ ? FeedbackType.dislike
+ : isLiked
+ ? FeedbackType.like
+ : FeedbackType.indeterminate;
+};
-const Feedback: React.FC = (props) => {
+const Feedback: React.FC = (props) => {
+ const {i18n} = useTranslation('feedback-variants');
const {
- lang,
- singlePage,
- isLiked,
- isDisliked,
- dislikeVariants = DISLIKE_VARIANTS[lang],
- isVerticalView,
+ isLiked = false,
+ isDisliked = false,
onSendFeedback,
- view,
- classNameControl,
- i18n,
- popupPosition,
- t,
+ dislikeVariants = i18n.getResourceBundle(i18n.language, 'feedback-variants'),
+ view = FeedbackView.Regular,
} = props;
const likeControlRef = useRef(null);
const dislikeControlRef = useRef(null);
- const timerId = useRef();
- const timeout = 3000;
-
- const [innerIsDisliked, setInnerIsDisliked] = useState(isDisliked);
- const [feedbackComment, setFeedbackComment] = useState('');
- const [feedbackCheckboxes, setFeedbackCheckboxes] = useState({} as FeedbackCheckboxes);
- const [showLikeSuccessPopup, setShowLikeSuccessPopup] = useState(false);
- const [showDislikeSuccessPopup, setShowDislikeSuccessPopup] = useState(false);
- const [showDislikeVariantsPopup, setShowDislikeVariantsPopup] = useState(false);
-
- const hideFeedbackPopups = useCallback(() => {
- setShowDislikeSuccessPopup(false);
- setShowLikeSuccessPopup(false);
- setShowDislikeVariantsPopup(false);
- }, []);
-
- const resetFeedbackAdditionalInfo = useCallback(() => {
- setFeedbackComment('');
- setFeedbackCheckboxes({});
- }, []);
-
- useEffect(() => {
- setInnerIsDisliked(isDisliked);
- }, [isDisliked]);
-
- useEffect(() => {
- i18n.changeLanguage(lang);
- }, [i18n, lang]);
-
- const setTimer = useCallback((callback: () => void) => {
- timerId.current = setTimeout(async () => {
- callback();
- }, timeout);
- }, []);
-
- const clearTimer = useCallback(() => {
- clearTimeout(timerId.current as number);
- timerId.current = undefined;
- }, []);
+ const [innerState, setInnerState] = useState(getInnerState(isLiked, isDisliked));
useEffect(() => {
- if (showLikeSuccessPopup || showDislikeSuccessPopup) {
- setTimer(() => {
- setShowDislikeSuccessPopup(false);
- setShowLikeSuccessPopup(false);
- clearTimer();
- });
- }
- }, [isDisliked, clearTimer, setTimer, showLikeSuccessPopup, showDislikeSuccessPopup]);
-
- const setLikeControlRef = useCallback((ref) => {
- likeControlRef.current = ref;
- }, []);
-
- const setDislikeControlRef = useCallback((ref) => {
- dislikeControlRef.current = ref;
- }, []);
-
- const onOutsideClick = useCallback(() => {
- hideFeedbackPopups();
-
- if (showDislikeVariantsPopup && innerIsDisliked && !isDisliked) {
- setInnerIsDisliked(false);
- }
- }, [
- isDisliked,
- innerIsDisliked,
- hideFeedbackPopups,
- setInnerIsDisliked,
- showDislikeVariantsPopup,
- ]);
-
- const onSendDislikeInformation = useCallback(() => {
- setShowDislikeSuccessPopup(true);
- setShowLikeSuccessPopup(false);
- setShowDislikeVariantsPopup(false);
-
- if (onSendFeedback) {
- const type = FeedbackType.dislike;
-
- const additionalInfo = getPreparedFeedbackAdditionalInfo(
- feedbackComment,
- feedbackCheckboxes,
- );
- const data = {
- type,
- ...additionalInfo,
- };
-
- onSendFeedback(data);
-
- resetFeedbackAdditionalInfo();
- }
- }, [onSendFeedback, feedbackComment, feedbackCheckboxes, resetFeedbackAdditionalInfo]);
+ setInnerState(getInnerState(isLiked, isDisliked));
+ }, [isLiked, isDisliked, setInnerState]);
- const getPopupPosition = useCallback(() => {
- if (!view || view === FeedbackView.Regular) {
- return isVerticalView ? PopperPosition.LEFT_START : PopperPosition.BOTTOM_END;
- }
+ const likeSuccessPopup = usePopupState({autoclose: 3000});
+ const dislikeSuccessPopup = usePopupState({autoclose: 3000});
+ const dislikeVariantsPopup = usePopupState();
- return PopperPosition.RIGHT;
- }, [isVerticalView, view]);
+ const hideFeedbackPopups = useCallback(() => {
+ likeSuccessPopup.close();
+ dislikeSuccessPopup.close();
+ dislikeVariantsPopup.close();
+ }, [likeSuccessPopup.close, dislikeSuccessPopup.close, dislikeVariantsPopup.close]);
const onChangeLike = useCallback(() => {
- setShowLikeSuccessPopup(true);
- setShowDislikeSuccessPopup(false);
- setShowDislikeVariantsPopup(false);
- setInnerIsDisliked(false);
+ hideFeedbackPopups();
- if (onSendFeedback) {
- onSendFeedback({
- type: isLiked ? FeedbackType.indeterminate : FeedbackType.like,
- });
+ if (innerState === FeedbackType.like) {
+ setInnerState(FeedbackType.indeterminate);
+ onSendFeedback({type: FeedbackType.indeterminate});
+ } else {
+ setInnerState(FeedbackType.like);
+ onSendFeedback({type: FeedbackType.like});
+ likeSuccessPopup.open();
}
- }, [isLiked, onSendFeedback]);
+ }, [isLiked, onSendFeedback, setInnerState, likeSuccessPopup.open, hideFeedbackPopups]);
const onChangeDislike = useCallback(() => {
- if (!isDisliked && !innerIsDisliked) {
- // Нажать дизлайк и показать окно с доп. информацией
- setShowDislikeVariantsPopup(true);
- setInnerIsDisliked(true);
- setShowLikeSuccessPopup(false);
- setShowDislikeSuccessPopup(false);
-
- if (isLiked && onSendFeedback) {
- onSendFeedback({type: FeedbackType.indeterminate});
- }
- } else if (!isDisliked && innerIsDisliked) {
- hideFeedbackPopups();
- setInnerIsDisliked(false);
- } else if (isDisliked && innerIsDisliked) {
- // Отжать дизлайк и отправить событие в неопределенное состояние
- hideFeedbackPopups();
- setInnerIsDisliked(false);
-
- if (onSendFeedback) {
- onSendFeedback({type: FeedbackType.indeterminate});
- }
- }
- }, [innerIsDisliked, isDisliked, isLiked, onSendFeedback, hideFeedbackPopups]);
-
- const renderLikeControl = useCallback(() => {
- return (
- (
-
- )}
- popupPosition={popupPosition}
- />
- );
- }, [
- onChangeLike,
- classNameControl,
- view,
- isVerticalView,
- isLiked,
- setLikeControlRef,
- popupPosition,
- t,
- ]);
-
- const renderDislikeControl = useCallback(() => {
- return (
- (
-
- )}
- />
- );
- }, [
- innerIsDisliked,
- onChangeDislike,
- classNameControl,
- view,
- isVerticalView,
- setDislikeControlRef,
- t,
- ]);
-
- const renderRegularFeedbackControls = useCallback(() => {
- return (
-
- {renderLikeControl()}
- {renderDislikeControl()}
-
- );
- }, [renderLikeControl, renderDislikeControl]);
-
- const renderWideFeedbackControls = useCallback(() => {
- return (
-
-
-
{t('main-question')}
-
-
-
-
-
- {t('button-like-text')}
-
-
-
-
-
- {t('button-dislike-text')}
-
-
-
-
- );
- }, [
- innerIsDisliked,
- isLiked,
- view,
- t,
- setLikeControlRef,
- setDislikeControlRef,
- onChangeLike,
- onChangeDislike,
- ]);
-
- const renderFeedbackControls = useCallback(() => {
- return view === FeedbackView.Regular
- ? renderRegularFeedbackControls()
- : renderWideFeedbackControls();
- }, [view, renderRegularFeedbackControls, renderWideFeedbackControls]);
-
- const renderFeedbackSuccessPopup = useCallback(() => {
- const anchor = showLikeSuccessPopup ? likeControlRef : dislikeControlRef;
- const visible = showLikeSuccessPopup || showDislikeSuccessPopup;
-
- if (!visible) {
- return null;
- }
-
- return (
-
- {t('success-title')}
- {t('success-text')}
-
- );
- }, [
- showLikeSuccessPopup,
- showDislikeSuccessPopup,
- hideFeedbackPopups,
- view,
- getPopupPosition,
- t,
- ]);
-
- const renderDislikeVariantsList = useCallback(() => {
- if (!dislikeVariants.length) {
- return null;
- }
-
- return (
-
- {dislikeVariants.map((variant, index) => (
- {
- setFeedbackCheckboxes({
- ...feedbackCheckboxes,
- [variant]: checked,
- });
- }}
- content={variant}
- />
- ))}
-
- );
- }, [dislikeVariants, feedbackCheckboxes]);
-
- const renderDislikeVariantsTextArea = useCallback(() => {
- return (
-
-
- );
- }, [feedbackComment, t]);
-
- const renderDislikeVariantsActions = useCallback(() => {
- return (
-
-
- {t('send-action-text')}
-
-
- );
- }, [onSendDislikeInformation, t]);
-
- const renderDislikeVariantsContent = useCallback(() => {
- return (
-
- {t('dislike-variants-title')}
- {renderDislikeVariantsList()}
- {renderDislikeVariantsTextArea()}
- {renderDislikeVariantsActions()}
-
- );
- }, [t, renderDislikeVariantsList, renderDislikeVariantsTextArea, renderDislikeVariantsActions]);
+ hideFeedbackPopups();
- const renderDislikeVariantsPopup = useCallback(() => {
- if (!showDislikeVariantsPopup) {
- return null;
+ if (innerState === FeedbackType.dislike) {
+ setInnerState(FeedbackType.indeterminate);
+ onSendFeedback({type: FeedbackType.indeterminate});
+ } else {
+ dislikeVariantsPopup.open();
}
+ }, [isDisliked, onSendFeedback, hideFeedbackPopups]);
- return (
-
- {renderDislikeVariantsContent()}
-
- );
- }, [
- showDislikeVariantsPopup,
- onOutsideClick,
- view,
- getPopupPosition,
- renderDislikeVariantsContent,
- ]);
-
- if (singlePage || !onSendFeedback) {
- return null;
- }
+ const onSendDislikeInformation = useCallback(
+ (data: FormData) => {
+ hideFeedbackPopups();
+ dislikeSuccessPopup.open();
+ setInnerState(FeedbackType.dislike);
+ onSendFeedback({type: FeedbackType.dislike, ...data});
+ },
+ [onSendFeedback, dislikeSuccessPopup.open, hideFeedbackPopups],
+ );
return (
- {renderFeedbackControls()}
- {renderFeedbackSuccessPopup()}
- {renderDislikeVariantsPopup()}
+
+
+
+
+ {likeControlRef.current && (
+
+ )}
+ {dislikeControlRef.current && (
+
+ )}
+ {dislikeControlRef.current && (
+
+ )}
);
};
-function getPreparedFeedbackAdditionalInfo(
- feedbackComment: string,
- feedbackCheckboxes: FeedbackCheckboxes,
-) {
- const answers = Object.keys(feedbackCheckboxes).reduce((acc, key) => {
- if (feedbackCheckboxes[key]) {
- acc.push(key);
- }
+const ControlsLayout: React.FC> = ({view, children}) => {
+ const {t} = useTranslation('feedback');
- return acc;
- }, [] as string[]);
+ if (view === FeedbackView.Regular) {
+ return {children} ;
+ }
- return {
- comment: feedbackComment,
- answers,
- };
-}
+ return (
+
+
+
{t('main-question')}
+
{children}
+
+
+ );
+};
-export default withTranslation('feedback')(Feedback);
+export default Feedback;
diff --git a/src/components/Feedback/controls/DislikeControl.tsx b/src/components/Feedback/controls/DislikeControl.tsx
new file mode 100644
index 00000000..07464f7a
--- /dev/null
+++ b/src/components/Feedback/controls/DislikeControl.tsx
@@ -0,0 +1,58 @@
+import React, {forwardRef, memo, useContext} from 'react';
+
+import {Button, Icon} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
+
+import {useTranslation} from '../../../hooks';
+import {Control} from '../../Control';
+import {CommonPropsContext} from '../../Controls/contexts';
+import type {FeedbackView} from '../Feedback';
+
+import DislikeActiveIcon from '@gravity-ui/icons/svgs/thumbs-down-fill.svg';
+import DislikeIcon from '@gravity-ui/icons/svgs/thumbs-down.svg';
+
+type DislikeControlProps = {
+ isVerticalView?: boolean | undefined;
+ isDisliked: boolean | undefined;
+ className?: string | undefined;
+ view: FeedbackView | undefined;
+ onClick: () => void;
+};
+
+const b = block('dc-feedback');
+
+const DislikeControl = memo(
+ forwardRef(({isDisliked, view, onClick}, ref) => {
+ const {t} = useTranslation('feedback');
+ const {isVerticalView, controlClassName} = useContext(CommonPropsContext);
+ const tooltipText = isDisliked ? t('cancel-dislike-text') : t('dislike-text');
+
+ if (view === 'wide') {
+ return (
+
+
+
+
+ {t('button-dislike-text')}
+
+ );
+ }
+
+ return (
+ (
+
+ )}
+ />
+ );
+ }),
+);
+
+DislikeControl.displayName = 'DislikeControl';
+
+export default DislikeControl;
diff --git a/src/components/Feedback/controls/DislikeVariantsPopup.tsx b/src/components/Feedback/controls/DislikeVariantsPopup.tsx
new file mode 100644
index 00000000..c751362b
--- /dev/null
+++ b/src/components/Feedback/controls/DislikeVariantsPopup.tsx
@@ -0,0 +1,185 @@
+import React, {
+ RefObject,
+ SyntheticEvent,
+ forwardRef,
+ memo,
+ useCallback,
+ useContext,
+ useImperativeHandle,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+
+import {Button, Checkbox, Popup, TextArea} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
+
+import {PopperPosition, useTranslation} from '../../../hooks';
+import {CommonPropsContext} from '../../Controls/contexts';
+import {FeedbackView} from '../Feedback';
+
+export interface FeedbackCheckboxes {
+ [key: string]: boolean;
+}
+
+const b = block('dc-feedback');
+
+const DislikeVariantsList = memo(
+ forwardRef, {variants: Record}>(({variants}, ref) => {
+ const {t} = useTranslation('feedback-variants');
+ const [checked, setChecked] = useState({} as FeedbackCheckboxes);
+
+ useImperativeHandle(ref, () => {
+ return {
+ data() {
+ return Object.keys(checked)
+ .filter((key) => Boolean(checked[key]))
+ .map((key) => variants[key]);
+ },
+
+ clean() {
+ setChecked({});
+ },
+ };
+ });
+
+ if (!Object.keys(variants).length) {
+ return null;
+ }
+
+ return (
+
+ {Object.keys(variants).map((variant) => (
+ {
+ setChecked({
+ ...checked,
+ [variant]: state,
+ });
+ }}
+ content={t(variant)}
+ />
+ ))}
+
+ );
+ }),
+);
+
+DislikeVariantsList.displayName = 'DislikeVariantsList';
+
+const DislikeVariantsInput = memo(
+ forwardRef>((_props, ref) => {
+ const {t} = useTranslation('feedback');
+ const [feedbackComment, setFeedbackComment] = useState('');
+ const onChange = useCallback((event: SyntheticEvent) => {
+ setFeedbackComment((event.target as HTMLTextAreaElement).value);
+ }, []);
+
+ useImperativeHandle(ref, () => {
+ return {
+ data() {
+ return feedbackComment;
+ },
+
+ clean() {
+ setFeedbackComment('');
+ },
+ };
+ });
+
+ return (
+
+
+
+ );
+ }),
+);
+
+DislikeVariantsInput.displayName = 'DislikeVariantsInput';
+
+type FormPart = {
+ data(): T;
+ clean(): void;
+};
+
+export type FormData = {
+ comment: string;
+ answers: string[];
+};
+
+type DislikeVariantsPopupProps = {
+ variants: Record;
+ view: FeedbackView;
+ visible: boolean;
+ anchor: RefObject;
+ onOutsideClick: () => void;
+ onSubmit: (data: FormData) => void;
+};
+
+const DislikeVariantsPopup: React.FC = memo(
+ ({variants, anchor, visible, view, onOutsideClick, onSubmit}) => {
+ const {t} = useTranslation('feedback');
+ const {isVerticalView} = useContext(CommonPropsContext);
+ const position = useMemo(() => {
+ if (!view || view === FeedbackView.Regular) {
+ return isVerticalView ? PopperPosition.LEFT_START : PopperPosition.BOTTOM_END;
+ }
+
+ return PopperPosition.RIGHT;
+ }, [isVerticalView, view]);
+
+ const feedbackComment = useRef | null>(null);
+ const feedbackCheckboxes = useRef | null>(null);
+
+ const onFormSubmit = useCallback(
+ (event: SyntheticEvent) => {
+ event.preventDefault();
+
+ onSubmit({
+ comment: feedbackComment.current?.data() || '',
+ answers: feedbackCheckboxes.current?.data() || [],
+ });
+
+ feedbackComment.current?.clean();
+ feedbackCheckboxes.current?.clean();
+ },
+ [onSubmit],
+ );
+
+ return (
+
+ {t('dislike-variants-title')}
+
+
+ );
+ },
+);
+
+DislikeVariantsPopup.displayName = 'DislikeVariantsPopup';
+
+export default DislikeVariantsPopup;
diff --git a/src/components/Feedback/controls/LikeControl.tsx b/src/components/Feedback/controls/LikeControl.tsx
new file mode 100644
index 00000000..ab38c04f
--- /dev/null
+++ b/src/components/Feedback/controls/LikeControl.tsx
@@ -0,0 +1,65 @@
+import React, {forwardRef, memo, useContext} from 'react';
+
+import {Button, Icon} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
+
+import type {PopperPosition} from '../../../hooks';
+import {useTranslation} from '../../../hooks';
+import {Control} from '../../Control';
+import {CommonPropsContext} from '../../Controls/contexts';
+import {FeedbackView} from '../Feedback';
+
+import LikeActiveIcon from '@gravity-ui/icons/svgs/thumbs-up-fill.svg';
+import LikeIcon from '@gravity-ui/icons/svgs/thumbs-up.svg';
+
+type LikeControlProps = {
+ isVerticalView?: boolean | undefined;
+ isLiked: boolean | undefined;
+ className?: string | undefined;
+ view: FeedbackView | undefined;
+ onClick: () => void;
+ popupPosition?: PopperPosition | undefined;
+};
+
+const b = block('dc-feedback');
+
+const LikeControl = memo(
+ forwardRef(({isLiked, view, onClick}, ref) => {
+ const {t} = useTranslation('feedback');
+ const {isVerticalView, popupPosition, controlClassName} = useContext(CommonPropsContext);
+ const tooltipText = isLiked ? t('cancel-like-text') : t('like-text');
+
+ if (view === FeedbackView.Wide) {
+ return (
+
+
+
+
+ {t('button-like-text')}
+
+ );
+ }
+
+ return (
+ }
+ popupPosition={popupPosition}
+ />
+ );
+ }),
+);
+
+LikeControl.displayName = 'LikeControl';
+
+export default LikeControl;
diff --git a/src/components/Feedback/controls/SuccessPopup.tsx b/src/components/Feedback/controls/SuccessPopup.tsx
new file mode 100644
index 00000000..e8222a82
--- /dev/null
+++ b/src/components/Feedback/controls/SuccessPopup.tsx
@@ -0,0 +1,46 @@
+import React, {RefObject, memo, useContext, useMemo} from 'react';
+
+import {Popup} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
+
+import {PopperPosition, useTranslation} from '../../../hooks';
+import {CommonPropsContext} from '../../Controls/contexts';
+import {FeedbackView} from '../Feedback';
+
+type SuccessPopupProps = {
+ view: FeedbackView;
+ visible: boolean;
+ anchor: RefObject;
+ onOutsideClick: () => void;
+};
+
+const b = block('dc-feedback');
+
+const SuccessPopup = memo(({visible, anchor, onOutsideClick, view}) => {
+ const {t} = useTranslation('feedback');
+ const {isVerticalView} = useContext(CommonPropsContext);
+ const position = useMemo(() => {
+ if (!view || view === FeedbackView.Regular) {
+ return isVerticalView ? PopperPosition.LEFT_START : PopperPosition.BOTTOM_END;
+ }
+
+ return PopperPosition.RIGHT;
+ }, [isVerticalView, view]);
+
+ return (
+
+ {t('success-title')}
+ {t('success-text')}
+
+ );
+});
+
+SuccessPopup.displayName = 'SuccessPopup';
+
+export default SuccessPopup;
diff --git a/src/components/MiniToc/MiniToc.tsx b/src/components/MiniToc/MiniToc.tsx
index 980492f5..daf707be 100644
--- a/src/components/MiniToc/MiniToc.tsx
+++ b/src/components/MiniToc/MiniToc.tsx
@@ -1,10 +1,9 @@
-import React, {ReactElement} from 'react';
-import PropTypes from 'prop-types';
-import block from 'bem-cn-lite';
+import React, {memo, useMemo} from 'react';
-import {withTranslation, WithTranslation, WithTranslationProps} from 'react-i18next';
+import block from 'bem-cn-lite';
-import {DocHeadingItem, Router, Lang} from '../../models';
+import {useTranslation} from '../../hooks';
+import {DocHeadingItem, Router} from '../../models';
import {Scrollspy} from '../Scrollspy';
import './MiniToc.scss';
@@ -12,59 +11,46 @@ import './MiniToc.scss';
const b = block('dc-mini-toc');
export interface MinitocProps {
- lang: Lang;
headings: DocHeadingItem[];
router: Router;
headerHeight?: number;
}
-type MinitocInnerProps = MinitocProps & WithTranslation & WithTranslationProps;
-
-class MiniToc extends React.Component {
- static propTypes = {
- headings: PropTypes.array.isRequired,
- };
+export interface MinitocSectionProps {
+ headings: DocHeadingItem[];
+ router: Router;
+ headerHeight?: number;
+}
- componentDidUpdate(prevProps: MinitocProps) {
- const {i18n, lang} = this.props;
- if (prevProps.lang !== lang) {
- i18n.changeLanguage(lang);
- }
- }
+interface FlatHeadingItem {
+ title: string;
+ href: string;
+ isChild: boolean;
+}
- render() {
- const {lang, i18n, t} = this.props;
+function getFlatHeadings(items: DocHeadingItem[], isChild = false): FlatHeadingItem[] {
+ return items.reduce((result, {href, title, items: subItems}) => {
+ return result.concat({title, href, isChild}, getFlatHeadings(subItems || [], true));
+ }, [] as FlatHeadingItem[]);
+}
- if (i18n.language !== lang) {
- i18n.changeLanguage(lang);
- }
+const MiniToc = memo(({headings, router, headerHeight}) => {
+ const {t} = useTranslation('mini-toc');
+ const flatHeadings = useMemo(() => getFlatHeadings(headings), [headings]);
+ const sectionHrefs = useMemo(
+ () => flatHeadings.map(({href}) => href, []),
+ [flatHeadings],
+ );
- return (
-
-
{t('title')}:
- {this.renderSections()}
-
- );
+ if (flatHeadings.length === 0) {
+ return null;
}
- private renderSections() {
- const {headings, router, headerHeight} = this.props;
-
- if (headings.length === 0) {
- return null;
- }
-
- const sectionHrefs = headings.reduce((prevHrefs, {href, items}) => {
- const children = items ? items.map(({href: itemHref}) => itemHref) : [];
-
- return prevHrefs.concat(href, children);
- }, []);
+ console.log('headings', headings, flatHeadings);
- if (sectionHrefs.length === 0) {
- return null;
- }
-
- return (
+ return (
+
+
{t('title')}:
{
router={router}
headerHeight={headerHeight}
>
- {headings.reduce(this.renderSection, [])}
+ {flatHeadings.map(({href, title, isChild}) => (
+
+
+ {title}
+
+
+ ))}
- );
- }
+
+ );
+});
- private renderSection = (prevSections: ReactElement[], heading: DocHeadingItem) => {
- return prevSections.concat(
- this.renderItem(heading),
- heading.items ? heading.items.map((item) => this.renderItem(item, true)) : [],
- );
- };
-
- private renderItem = ({title, href}: DocHeadingItem, isChild = false) => {
- return (
-
-
- {title}
-
-
- );
- };
-}
+MiniToc.displayName = 'MiniToc';
-export default withTranslation('mini-toc')(MiniToc);
+export default MiniToc;
diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx
index 81d660d5..35e2b04f 100644
--- a/src/components/SearchBar/SearchBar.tsx
+++ b/src/components/SearchBar/SearchBar.tsx
@@ -1,13 +1,12 @@
-import React from 'react';
+import React, {memo} from 'react';
import {Icon} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
-import {withTranslation, WithTranslation, WithTranslationProps} from 'react-i18next';
import {useHotkeys} from 'react-hotkeys-hook';
+import {useTranslation} from '../../hooks';
import {Control} from '../Control';
-import {Lang} from '../../models';
import ArrowLeftIcon from '@gravity-ui/icons/svgs/chevron-left.svg';
import CloseIcon from '@gravity-ui/icons/svgs/xmark.svg';
@@ -16,7 +15,6 @@ import './SearchBar.scss';
const b = block('dc-search-bar');
export interface SearchBarProps {
- lang: Lang;
searchQuery?: string;
onClickPrevSearch?: () => void;
onClickNextSearch?: () => void;
@@ -25,15 +23,11 @@ export interface SearchBarProps {
searchCountResults?: number;
}
-type SearchBarInnerProps = SearchBarProps & WithTranslation & WithTranslationProps;
-
const noop = () => {};
-const SearchBar: React.FC = (props) => {
+const SearchBar = memo((props) => {
+ const {t} = useTranslation('search');
const {
- t,
- i18n,
- lang,
searchQuery,
searchCurrentIndex,
searchCountResults,
@@ -48,10 +42,6 @@ const SearchBar: React.FC = (props) => {
useHotkeys(hotkeysPrev, onClickPrevSearch, hotkeysOptions, [onClickPrevSearch]);
useHotkeys(hotkeysNext, onClickNextSearch, hotkeysOptions, [onClickNextSearch]);
- if (i18n.language !== lang) {
- i18n.changeLanguage(lang);
- }
-
return (
@@ -92,4 +82,4 @@ const SearchBar: React.FC
= (props) => {
SearchBar.displayName = 'DCSearchBar';
-export default withTranslation('search-bar')(SearchBar);
+export default SearchBar;
diff --git a/src/components/SearchItem/SearchItem.tsx b/src/components/SearchItem/SearchItem.tsx
index 76e3025d..6402cdab 100644
--- a/src/components/SearchItem/SearchItem.tsx
+++ b/src/components/SearchItem/SearchItem.tsx
@@ -1,12 +1,13 @@
-import React, {useState} from 'react';
-import block from 'bem-cn-lite';
+import React, {memo, useState} from 'react';
+
import {Button} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
-import './SearchItem.scss';
-import {WithTranslation, withTranslation, WithTranslationProps} from 'react-i18next';
-import {Lang} from '../../models';
+import {useTranslation} from '../../hooks';
import {HTML} from '../HTML';
+import './SearchItem.scss';
+
const b = block('SearchItem');
export interface ISearchItem {
@@ -24,86 +25,74 @@ export interface SearchOnClickProps {
export interface SearchItemProps {
item: ISearchItem;
className?: string;
- lang?: Lang;
}
-type SearchPageInnerProps = SearchItemProps &
- SearchOnClickProps &
- WithTranslation &
- WithTranslationProps;
+type SearchPageInnerProps = SearchItemProps & SearchOnClickProps;
-const SearchItem = ({
- item,
- className,
- lang = Lang.En,
- i18n,
- t,
- itemOnClick,
- irrelevantOnClick,
- relevantOnClick,
-}: SearchPageInnerProps) => {
- const {url, title, description} = item;
+const SearchItem = memo(
+ ({item, className, itemOnClick, irrelevantOnClick, relevantOnClick}) => {
+ const {t} = useTranslation('search');
+ const {url, title, description} = item;
- if (i18n.language !== lang) {
- i18n.changeLanguage(lang);
- }
+ const [markedItem, setMarkedItem] = useState(false);
- const [markedItem, setMarkedItem] = useState(false);
-
- const renderItem = () => {
- return (
-
-
(itemOnClick ? itemOnClick(item) : undefined)}
- >
- {title}
- {description}
-
- {irrelevantOnClick && relevantOnClick && (
-
-
- {markedItem ? (
-
- {t('search_mark-result-text')}
-
- ) : (
-
- {
- setMarkedItem(true);
- if (irrelevantOnClick) {
- irrelevantOnClick(item);
- }
- }}
- >
- {t('search_mark_dislike')}
-
- {
- setMarkedItem(true);
- if (relevantOnClick) {
- relevantOnClick(item);
- }
- }}
- >
- {t('search_mark_like')}
-
-
- )}
+ const renderItem = () => {
+ return (
+
+
(itemOnClick ? itemOnClick(item) : undefined)}
+ >
+ {title}
+ {description}
+
+ {irrelevantOnClick && relevantOnClick && (
+
+
+ {markedItem ? (
+
+ {t('search_mark-result-text')}
+
+ ) : (
+
+ {
+ setMarkedItem(true);
+ if (irrelevantOnClick) {
+ irrelevantOnClick(item);
+ }
+ }}
+ >
+ {t('search_mark_dislike')}
+
+ {
+ setMarkedItem(true);
+ if (relevantOnClick) {
+ relevantOnClick(item);
+ }
+ }}
+ >
+ {t('search_mark_like')}
+
+
+ )}
+
-
- )}
-
- );
- };
+ )}
+
+ );
+ };
+
+ return
{renderItem()}
;
+ },
+);
- return
{renderItem()}
;
-};
+SearchItem.displayName = 'SearchItem';
-export default withTranslation('search')(SearchItem);
+export default SearchItem;
diff --git a/src/components/SearchPage/SearchPage.tsx b/src/components/SearchPage/SearchPage.tsx
index 2c5e21d4..003b1389 100644
--- a/src/components/SearchPage/SearchPage.tsx
+++ b/src/components/SearchPage/SearchPage.tsx
@@ -2,20 +2,15 @@ import React, {useRef, useState} from 'react';
import {Button, Loader, TextInput} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
-import {TFunction, withTranslation, WithTranslation, WithTranslationProps} from 'react-i18next';
+import {useTranslation} from '../../hooks';
import {Paginator, PaginatorProps} from '../Paginator';
import {ISearchItem, SearchItem, SearchOnClickProps} from '../SearchItem';
-import {Lang} from '../../models';
import './SearchPage.scss';
const b = block('dc-search-page');
-interface Translation {
- t: TFunction<'translation'>;
-}
-
interface Loading {
loading?: boolean;
}
@@ -28,36 +23,26 @@ interface InputProps {
type RenderInput = {
inputRef: React.MutableRefObject
;
onQueryUpdate: (arg: string) => void;
-} & InputProps &
- Translation;
+} & InputProps;
-type RenderNoContent = Loading & Translation;
+type RenderNoContent = Loading;
interface SearchPageProps extends Loading {
items: ISearchItem[];
page: number;
- lang?: Lang;
isMobile?: boolean;
loading?: boolean;
}
-type RenderFoundProps = SearchPageProps & SearchOnClickProps & PaginatorProps & Translation;
+type RenderFoundProps = SearchPageProps & SearchOnClickProps & PaginatorProps;
-type SearchPageInnerProps = SearchPageProps &
- SearchOnClickProps &
- InputProps &
- PaginatorProps &
- WithTranslation &
- WithTranslationProps;
+type SearchPageInnerProps = SearchPageProps & SearchOnClickProps & InputProps & PaginatorProps;
const SearchPage = ({
query = '',
items = [],
page = 1,
- lang = Lang.En,
isMobile,
- i18n,
- t,
totalItems,
maxPages,
itemsPerPage,
@@ -68,48 +53,46 @@ const SearchPage = ({
relevantOnClick,
loading,
}: SearchPageInnerProps) => {
- if (i18n.language !== lang) {
- i18n.changeLanguage(lang);
- }
-
const inputRef = useRef(null);
const [currentQuery, setCurrentQuery] = useState(query);
return (
- {renderInput({
- query: currentQuery,
- onQueryUpdate: setCurrentQuery,
- onSubmit,
- inputRef,
- t,
- })}
+
- {items?.length && query
- ? renderFound({
- items,
- page,
- lang,
- isMobile,
- t,
- totalItems,
- maxPages,
- itemsPerPage,
- itemOnClick,
- onPageChange,
- irrelevantOnClick,
- relevantOnClick,
- })
- : renderWithoutContent({loading, t})}
+ {items?.length && query ? (
+
+ ) : (
+
+ )}
);
};
-function renderFound({
- lang,
+const FoundBlock: React.FC = ({
items,
itemOnClick,
irrelevantOnClick,
@@ -120,25 +103,21 @@ function renderFound({
onPageChange,
itemsPerPage,
isMobile,
- t,
-}: RenderFoundProps) {
+}) => {
+ const {t} = useTranslation('search');
+
return (
{t('search_request-query')}
{items.map((item: ISearchItem) => (
itemOnClick(arg) : undefined}
- irrelevantOnClick={
- irrelevantOnClick ? (arg) => irrelevantOnClick(arg) : undefined
- }
- relevantOnClick={
- relevantOnClick ? (arg) => relevantOnClick(arg) : undefined
- }
+ itemOnClick={itemOnClick}
+ irrelevantOnClick={irrelevantOnClick}
+ relevantOnClick={relevantOnClick}
/>
))}
@@ -154,9 +133,11 @@ function renderFound({
);
-}
+};
+
+const WithoutContentBlock: React.FC = ({loading}) => {
+ const {t} = useTranslation('search');
-function renderWithoutContent({loading, t}: RenderNoContent) {
return loading ? (
) : (
@@ -165,9 +146,11 @@ function renderWithoutContent({loading, t}: RenderNoContent) {
{t('search_not-found-text')}
);
-}
+};
+
+const InputBlock: React.FC
= ({query, onQueryUpdate, onSubmit, inputRef}) => {
+ const {t} = useTranslation('search');
-function renderInput({query, onQueryUpdate, onSubmit, inputRef, t}: RenderInput) {
return (
);
-}
+};
-export default withTranslation('search')(SearchPage);
+export default SearchPage;
diff --git a/src/components/Subscribe/Subscribe.scss b/src/components/Subscribe/Subscribe.scss
index 895526dc..a1f3e245 100644
--- a/src/components/Subscribe/Subscribe.scss
+++ b/src/components/Subscribe/Subscribe.scss
@@ -126,17 +126,4 @@ $popupWidth: 320px;
}
}
}
-
- &__subscribe-button {
- width: 16px;
- height: 16px;
-
- &_view_wide {
- margin-right: 8px;
- }
-
- &_active {
- color: var(--g-color-button-accent-background);
- }
- }
}
diff --git a/src/components/Subscribe/Subscribe.tsx b/src/components/Subscribe/Subscribe.tsx
index 6964ecc4..fcd5d243 100644
--- a/src/components/Subscribe/Subscribe.tsx
+++ b/src/components/Subscribe/Subscribe.tsx
@@ -1,18 +1,19 @@
-import React, {useCallback, useState, useRef} from 'react';
+import React, {PropsWithChildren, forwardRef, memo, useCallback, useContext, useRef} from 'react';
+
+import {Button, Icon} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
-import {WithTranslation, withTranslation} from 'react-i18next';
-import {SubscribeSuccessPopup} from './SubscribeSuccessPopup';
-import {SubscribeVariantsPopup} from './SubscribeVariantsPopup';
-import {Button} from '@gravity-ui/uikit';
+import {PopperPosition, usePopupState, useTranslation} from '../../hooks';
+import {SubscribeData} from '../../models';
import {Control} from '../Control';
+import {CommonPropsContext} from '../Controls/contexts';
-import {SubscribeData, Lang} from '../../models';
+import {SubscribeSuccessPopup} from './SubscribeSuccessPopup';
+import {SubscribeVariantsPopup} from './SubscribeVariantsPopup';
import SubscribeIcon from '../../../assets/icons/subscribe.svg';
import './Subscribe.scss';
-import {PopperPosition} from '../../hooks';
const b = block('dc-subscribe');
@@ -21,101 +22,120 @@ export enum SubscribeView {
Regular = 'regular',
}
-export interface SubscribeProps {
- lang: Lang;
- isVerticalView?: boolean;
- onSubscribe?: (data: SubscribeData) => void;
- view?: SubscribeView;
- classNameControl?: string;
- popupPosition?: PopperPosition;
-}
-
-const Subscribe: React.FC = React.memo((props) => {
- const {isVerticalView, onSubscribe, view, classNameControl, popupPosition, t} = props;
-
- const subscribeControlRef = useRef(null);
-
- const [showSubscribeSuccessPopup, setShowSubscribeSuccessPopup] = useState(false);
- const [showSubscribeVariantsPopup, setShowSubscribeVariantsPopup] = useState(false);
+type SubscribeControlProps = {
+ view: SubscribeView;
+ onChangeSubscribe: () => void;
+};
+
+const SubscribeControl = memo(
+ forwardRef(({view, onChangeSubscribe}, ref) => {
+ const {isVerticalView, popupPosition, controlClassName, controlSize} =
+ useContext(CommonPropsContext);
+ const {t} = useTranslation('controls');
+
+ if (view === SubscribeView.Wide) {
+ return (
+
+
+
+
+ {t('button-Subscribe-text')}
+
+ );
+ }
- const setSubscribeControlRef = useCallback((ref) => {
- subscribeControlRef.current = ref;
- }, []);
-
- const onChangeSubscribe = useCallback(() => {
- setShowSubscribeVariantsPopup(!showSubscribeVariantsPopup);
- setShowSubscribeSuccessPopup(false);
- }, [showSubscribeVariantsPopup]);
-
- const renderSubscribeControl = useCallback(() => {
return (
}
popupPosition={popupPosition}
/>
);
- }, [
- classNameControl,
- view,
- isVerticalView,
- setSubscribeControlRef,
- onChangeSubscribe,
- popupPosition,
- t,
- ]);
-
- const renderWideSubscribeControls = useCallback(() => {
+ }),
+);
+
+SubscribeControl.displayName = 'SubscribeControl';
+
+const SubscribeControlsLayout = memo>(
+ ({view, children}) => {
+ const {t} = useTranslation('controls');
+
+ if (view === SubscribeView.Regular) {
+ return {children} ;
+ }
+
return (
-
{t('main-question')}
-
-
-
- {t('button-Subscribe-text')}
-
-
+
{t('main-question')}
+
{children}
);
- }, [view, setSubscribeControlRef, onChangeSubscribe, t]);
+ },
+);
+
+SubscribeControlsLayout.displayName = 'SubscribeControlsLayout';
+
+export interface SubscribeProps {
+ view?: SubscribeView;
+ isVerticalView?: boolean;
+ onSubscribe: (data: SubscribeData) => void;
+ classNameControl?: string;
+ popupPosition?: PopperPosition;
+}
+
+const Subscribe = memo((props) => {
+ const {view = SubscribeView.Regular, onSubscribe} = props;
+
+ const subscribeControlRef = useRef(null);
- const renderSubscribeControls = useCallback(() => {
- return view === SubscribeView.Regular
- ? renderSubscribeControl()
- : renderWideSubscribeControls();
- }, [view, renderSubscribeControl, renderWideSubscribeControls]);
+ const successPopup = usePopupState({autoclose: 60000});
+ const variantsPopup = usePopupState();
+
+ const onChangeSubscribe = useCallback(() => {
+ variantsPopup.toggle();
+ successPopup.close();
+ }, [successPopup.close, variantsPopup.toggle]);
+
+ const onSubmitVariants = useCallback(() => {
+ variantsPopup.close();
+ successPopup.open();
+ }, [successPopup.open, variantsPopup.close]);
return (
- {renderSubscribeControls()}
- {showSubscribeSuccessPopup && subscribeControlRef.current && (
+
+
+
+ {successPopup.visible && subscribeControlRef.current && (
)}
- {showSubscribeVariantsPopup && subscribeControlRef.current && (
+ {variantsPopup.visible && subscribeControlRef.current && (
{
- setShowSubscribeVariantsPopup(false);
- setShowSubscribeSuccessPopup(true);
- }}
- {...{view, isVerticalView, anchor: subscribeControlRef, t}}
+ onSubmit={onSubmitVariants}
/>
)}
@@ -124,4 +144,4 @@ const Subscribe: React.FC = React.memo((props)
Subscribe.displayName = 'Subscribe';
-export default withTranslation('controls')(Subscribe);
+export default Subscribe;
diff --git a/src/components/Subscribe/SubscribeSuccessPopup/SubscribeSuccessPopup.tsx b/src/components/Subscribe/SubscribeSuccessPopup/SubscribeSuccessPopup.tsx
index 2f8d4733..5bbccddb 100644
--- a/src/components/Subscribe/SubscribeSuccessPopup/SubscribeSuccessPopup.tsx
+++ b/src/components/Subscribe/SubscribeSuccessPopup/SubscribeSuccessPopup.tsx
@@ -1,43 +1,37 @@
-import React, {memo, useCallback} from 'react';
-import {WithTranslation, withTranslation} from 'react-i18next';
-import block from 'bem-cn-lite';
+import React, {memo, useContext} from 'react';
+
import {Popup} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
+import {useTranslation} from '../../../hooks';
+import {CommonPropsContext} from '../../Controls/contexts';
import {SubscribeView} from '../Subscribe';
-import useTimeout from '../../../hooks/useTimeout';
import {getPopupPosition} from '../utils';
const b = block('dc-subscribe');
-const SubscribeSuccessPopup: React.FC<
- {
- anchor: React.RefObject;
- isVerticalView?: boolean;
- view?: SubscribeView;
- visible: boolean;
- setVisible: (value: boolean) => void;
- } & WithTranslation
-> = memo((props) => {
- const {t, visible, setVisible, anchor, isVerticalView, view} = props;
-
- const hide = useCallback(() => setVisible(false), [setVisible]);
-
- useTimeout(hide, 60000);
+const SubscribeSuccessPopup = memo<{
+ anchor: React.RefObject;
+ view?: SubscribeView;
+ onOutsideClick: () => void;
+}>(({anchor, view, onOutsideClick}) => {
+ const {t} = useTranslation('controls');
+ const {isVerticalView} = useContext(CommonPropsContext);
return (
- {t('verify-title')}
- {t('verify-text')}
+ {t('verify-title')}
+ {t('verify-text')}
);
});
SubscribeSuccessPopup.displayName = 'SubscribeSuccessPopup';
-export default withTranslation('controls')(SubscribeSuccessPopup);
+export default SubscribeSuccessPopup;
diff --git a/src/components/Subscribe/SubscribeVariantsPopup/SubscribeVariantsPopup.tsx b/src/components/Subscribe/SubscribeVariantsPopup/SubscribeVariantsPopup.tsx
index d5b87f6d..0fef5db2 100644
--- a/src/components/Subscribe/SubscribeVariantsPopup/SubscribeVariantsPopup.tsx
+++ b/src/components/Subscribe/SubscribeVariantsPopup/SubscribeVariantsPopup.tsx
@@ -1,32 +1,29 @@
-import React, {memo, useCallback, useState} from 'react';
-import {WithTranslation, withTranslation} from 'react-i18next';
-import block from 'bem-cn-lite';
+import React, {SyntheticEvent, memo, useCallback, useContext, useState} from 'react';
+
import {Button, List, Popup, TextInput} from '@gravity-ui/uikit';
+import block from 'bem-cn-lite';
-import {SubscribeView} from '../Subscribe';
+import {useTranslation} from '../../../hooks';
+import {SubscribeData, SubscribeType} from '../../../models';
import {isInvalidEmail} from '../../../utils';
+import {CommonPropsContext} from '../../Controls/contexts';
+import {SubscribeView} from '../Subscribe';
import {getPopupPosition} from '../utils';
-import {SubscribeData, SubscribeType} from '../../../models';
-
const b = block('dc-subscribe');
const LIST_ITEM_HEIGHT = 36;
-const SubscribeVariantsPopup: React.FC<
- {
- anchor: React.RefObject;
- visible: boolean;
- setVisible: (value: boolean) => void;
- isVerticalView?: boolean;
- view?: SubscribeView;
- onSubscribe?: (data: SubscribeData) => void;
- onSubmit: () => void;
- } & WithTranslation
-> = memo((props) => {
- const {t, visible, setVisible, anchor, view, isVerticalView, onSubscribe, onSubmit} = props;
-
- const hide = useCallback(() => setVisible(false), [setVisible]);
+const SubscribeVariantsPopup = memo<{
+ anchor: React.RefObject;
+ view?: SubscribeView;
+ onSubscribe?: (data: SubscribeData) => void;
+ onSubmit: () => void;
+ onOutsideClick: () => void;
+}>((props) => {
+ const {t} = useTranslation('controls');
+ const {isVerticalView} = useContext(CommonPropsContext);
+ const {anchor, view, onSubscribe, onSubmit, onOutsideClick} = props;
const [email, setEmail] = useState('');
const [showError, setShowError] = useState('');
@@ -37,7 +34,7 @@ const SubscribeVariantsPopup: React.FC<
}, []);
const onSendSubscribeInformation = useCallback(
- (event) => {
+ (event: SyntheticEvent) => {
event.preventDefault();
if (isInvalidEmail(email)) {
@@ -102,7 +99,7 @@ const SubscribeVariantsPopup: React.FC<
- {t('subscribe-text')}
+ {t('subscribe-text')}
@@ -112,8 +109,8 @@ const SubscribeVariantsPopup: React.FC<
return (
@@ -125,4 +122,4 @@ const SubscribeVariantsPopup: React.FC<
SubscribeVariantsPopup.displayName = 'SubscribeVariantsPopup';
-export default withTranslation('controls')(SubscribeVariantsPopup);
+export default SubscribeVariantsPopup;
diff --git a/src/components/Toc/Toc.tsx b/src/components/Toc/Toc.tsx
index ec9e4822..216ae856 100644
--- a/src/components/Toc/Toc.tsx
+++ b/src/components/Toc/Toc.tsx
@@ -2,9 +2,9 @@ import React from 'react';
import block from 'bem-cn-lite';
import {omit} from 'lodash';
-import {ControlSizes, Lang, Router, TocData, TocItem} from '../../models';
import {PopperPosition} from '../../hooks';
+import {ControlSizes, Router, TocData, TocItem} from '../../models';
import {isActiveItem, normalizeHash, normalizePath} from '../../utils';
import {Controls} from '../Controls';
import {HTML} from '../HTML';
@@ -26,7 +26,6 @@ export interface TocProps extends TocData {
headerHeight?: number;
tocTitleIcon?: React.ReactNode;
hideTocHeader?: boolean;
- lang: Lang;
singlePage?: boolean;
onChangeSinglePage?: (value: boolean) => void;
pdfLink?: string;
@@ -242,13 +241,12 @@ class Toc extends React.Component {
}
private renderBottom() {
- const {lang, singlePage, onChangeSinglePage, pdfLink} = this.props;
+ const {singlePage, onChangeSinglePage, pdfLink} = this.props;
const {contentScrolled} = this.state;
return (
{
- state: TocNavPanelState = {
- flatToc: [],
- activeItemIndex: 0,
- };
-
- componentDidMount() {
- this.setState(this.getState(this.props));
- }
-
- componentDidUpdate(prevProps: TocNavPanelProps) {
- const {router, i18n, lang} = this.props;
+function getFlatToc(items: TocItem[]): FlatTocItem[] {
+ return items.reduce((result, {href, name, items: subItems}) => {
+ const part: FlatTocItem[] = subItems ? getFlatToc(subItems) : [];
- if (prevProps.router.pathname !== router.pathname) {
- this.setState(this.getState(this.props));
+ if (href) {
+ part.push({name, href});
}
- if (prevProps.lang !== lang) {
- i18n.changeLanguage(lang);
- }
- }
+ return result.concat(part);
+ }, [] as FlatTocItem[]);
+}
- render() {
- const {flatToc, activeItemIndex} = this.state;
- const {fixed = false, className} = this.props;
+function getBoundingItems(flatToc: FlatTocItem[], router: Router) {
+ for (let i = 0; i < flatToc.length; i++) {
+ const {href} = flatToc[i];
- if (!flatToc.length) {
- return null;
+ if (isActiveItem(router, href)) {
+ return {
+ prevItem: flatToc[i - 1] || null,
+ nextItem: flatToc[i + 1] || null,
+ };
}
-
- const prevItem = activeItemIndex > 0 ? flatToc[activeItemIndex - 1] : null;
- const nextItem = activeItemIndex < flatToc.length - 1 ? flatToc[activeItemIndex + 1] : null;
-
- return (
-
-
- {this.renderControl(prevItem)}
- {this.renderControl(nextItem, true)}
-
-
- );
}
- private renderControl(tocItem: FlatTocItem | null, isNext?: boolean) {
- const {t} = this.props;
- const keyHint = isNext ? 'hint_next' : 'hint_previous';
-
- return (
-
- {tocItem && (
-
- {t(keyHint)}
- {this.renderLink(tocItem, isNext)}
-
- )}
-
- );
- }
+ return {
+ prevItem: null,
+ nextItem: null,
+ };
+}
- private renderLink(item: FlatTocItem | null, isNext?: boolean) {
- if (!item) {
- return null;
- }
+const TocNavControl = memo<{isNext?: boolean; item: FlatTocItem}>(({item, isNext}) => {
+ const {t} = useTranslation('toc-nav-panel');
+ const keyHint = isNext ? 'hint_next' : 'hint_previous';
+ const isExternal = isExternalHref(item.href);
+ const linkAttributes = {
+ href: item.href,
+ target: isExternal ? '_blank' : '_self',
+ rel: isExternal ? 'noopener noreferrer' : undefined,
+ };
- const isExternal = isExternalHref(item.href);
- const linkAttributes = {
- href: item.href,
- target: isExternal ? '_blank' : '_self',
- rel: isExternal ? 'noopener noreferrer' : undefined,
- };
-
- return (
-
- {!isNext && }
- {item.name}
- {isNext && }
-
- );
+ return (
+
+ {item && (
+
+ {t(keyHint)}
+
+
+ )}
+
+ );
+});
+
+TocNavControl.displayName = 'TocNavControl';
+
+const TocNavPanel = memo(({items, router, fixed, className}) => {
+ const flatToc = useMemo(() => getFlatToc(items), [items]);
+ const {prevItem, nextItem} = useMemo(
+ () => getBoundingItems(flatToc, router),
+ [flatToc, router],
+ );
+
+ if (!flatToc.length) {
+ return null;
}
- private getState(props: TocNavPanelProps): TocNavPanelState {
- const flatToc: FlatTocItem[] = [];
- let activeItemIndex = 0;
-
- function processItems(items: TocItem[]) {
- items.forEach(({href, name, items: subItems}) => {
- if (subItems) {
- processItems(subItems);
- }
-
- if (href) {
- flatToc.push({
- name,
- href,
- });
- }
-
- if (href && isActiveItem(props.router, href)) {
- activeItemIndex = flatToc.length - 1;
- }
- });
- }
-
- processItems(props.items);
+ return (
+
+
+ {prevItem && }
+ {nextItem && }
+
+
+ );
+});
- return {flatToc, activeItemIndex};
- }
-}
+TocNavPanel.displayName = 'TocNavPanel';
-export default withTranslation('toc-nav-panel')(TocNavPanel);
+export default TocNavPanel;
diff --git a/src/components/ToggleArrow/ToggleArrow.tsx b/src/components/ToggleArrow/ToggleArrow.tsx
index 0749bba2..3b3d2f28 100644
--- a/src/components/ToggleArrow/ToggleArrow.tsx
+++ b/src/components/ToggleArrow/ToggleArrow.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+
import block from 'bem-cn-lite';
import ChevronIcon from '@gravity-ui/icons/svgs/chevron-right.svg';
diff --git a/src/config/i18n.ts b/src/config/i18n.ts
new file mode 100644
index 00000000..9441bd11
--- /dev/null
+++ b/src/config/i18n.ts
@@ -0,0 +1,45 @@
+import i18n, {TFunction} from 'i18next';
+import {initReactI18next} from 'react-i18next';
+
+import {Lang} from '../models';
+
+import en from '../i18n/en.json';
+import ru from '../i18n/ru.json';
+
+export type Loc = Record;
+
+export interface I18NConfig {
+ lang?: string;
+ loc?: Loc;
+}
+
+let initializePromise: Promise | null = null;
+
+export const configureI18N = ({lang, loc}: I18NConfig) => {
+ if (!initializePromise) {
+ lang = lang || Lang.En;
+ loc = loc || {ru, en};
+
+ initializePromise = i18n.use(initReactI18next).init({
+ lng: lang,
+ fallbackLng: lang,
+ ns: Object.keys(loc[lang as Lang]),
+ resources: loc,
+ interpolation: {
+ escapeValue: false,
+ },
+ });
+ } else {
+ if (lang && lang !== i18n.language) {
+ i18n.changeLanguage(lang);
+ }
+
+ if (loc) {
+ for (const [lng, namespaces] of Object.entries(loc)) {
+ for (const [ns, resources] of Object.entries(namespaces as Loc[Lang])) {
+ i18n.addResources(lng, ns, resources);
+ }
+ }
+ }
+ }
+};
diff --git a/src/config/index.ts b/src/config/index.ts
new file mode 100644
index 00000000..c3eb0a54
--- /dev/null
+++ b/src/config/index.ts
@@ -0,0 +1,34 @@
+import {Config, Lang} from '../models';
+
+import {configureI18N} from './i18n';
+
+type Subscriber = (config: Config) => void;
+
+const subs: Set = new Set();
+
+let config: Config = {
+ lang: Lang.En,
+};
+
+export const configure = (newConfig: Partial = {}) => {
+ config = Object.assign({}, config, newConfig);
+
+ configureI18N({
+ lang: config.lang,
+ loc: config.loc,
+ });
+
+ subs.forEach((sub) => {
+ sub(config);
+ });
+};
+
+export const subscribeConfigure = (sub: Subscriber) => {
+ subs.add(sub);
+
+ return () => {
+ subs.delete(sub);
+ };
+};
+
+export const getConfig = () => config;
diff --git a/src/constants.ts b/src/constants.ts
index a9a2c779..75415c71 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,19 +1,4 @@
-import {Lang, TextSizes, Theme} from './models';
-
-export const DISLIKE_VARIANTS = {
- [Lang.Ru]: [
- 'Нет ответа на мой вопрос',
- 'Рекомендации не помогли',
- 'Содержание не соответствует заголовку',
- 'Другое',
- ],
- [Lang.En]: [
- 'No answer to my question',
- "Recommendations didn't help",
- "The content doesn't match the title",
- 'Other',
- ],
-};
+import {TextSizes, Theme} from './models';
export const DEFAULT_SETTINGS = {
fullScreen: false,
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 2667adf3..1a34a4a7 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -1,2 +1,5 @@
export * from './useForkRef';
export * from './usePopper';
+export * from './useTimer';
+export * from './useTranslation';
+export * from './usePopupState';
diff --git a/src/hooks/usePopper.ts b/src/hooks/usePopper.ts
index 4b709032..c91d5fa2 100644
--- a/src/hooks/usePopper.ts
+++ b/src/hooks/usePopper.ts
@@ -1,4 +1,5 @@
import React from 'react';
+
import popper from '@popperjs/core';
import {Modifier, usePopper as useReactPopper} from 'react-popper';
diff --git a/src/hooks/usePopupState.ts b/src/hooks/usePopupState.ts
new file mode 100644
index 00000000..a9c26da3
--- /dev/null
+++ b/src/hooks/usePopupState.ts
@@ -0,0 +1,42 @@
+import {useCallback, useMemo, useState} from 'react';
+
+import {useTimer} from './useTimer';
+
+export const usePopupState = ({autoclose = 0}: {autoclose?: number} = {}) => {
+ const [visible, setVisible] = useState(false);
+ const [setTimer, clearTimer] = useTimer(autoclose);
+
+ const close = useCallback(() => {
+ setVisible(false);
+
+ if (autoclose) {
+ clearTimer();
+ }
+ }, [autoclose, clearTimer, setVisible]);
+ const open = useCallback(() => {
+ setVisible(true);
+
+ if (autoclose) {
+ setTimer(close);
+ }
+ }, [autoclose, setTimer, setVisible]);
+ const toggle = useCallback(() => {
+ if (visible) {
+ close();
+ } else {
+ open();
+ }
+ }, [visible, close, open]);
+
+ return useMemo(
+ () => ({
+ open,
+ close,
+ toggle,
+ get visible() {
+ return visible;
+ },
+ }),
+ [visible, open, close, toggle],
+ );
+};
diff --git a/src/hooks/useTimeout.ts b/src/hooks/useTimeout.ts
index 3c16752c..2aa09216 100644
--- a/src/hooks/useTimeout.ts
+++ b/src/hooks/useTimeout.ts
@@ -1,4 +1,5 @@
import {useEffect, useRef} from 'react';
+
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
function useTimeout(callback: () => void, delay: number | null) {
diff --git a/src/hooks/useTimer.ts b/src/hooks/useTimer.ts
new file mode 100644
index 00000000..be84c7a8
--- /dev/null
+++ b/src/hooks/useTimer.ts
@@ -0,0 +1,19 @@
+import {useCallback, useRef} from 'react';
+
+export function useTimer(timeout = 3000) {
+ const timerId = useRef();
+
+ const clearTimer = useCallback(() => {
+ clearTimeout(timerId.current as number);
+ timerId.current = undefined;
+ }, []);
+
+ const setTimer = useCallback((callback: () => void) => {
+ timerId.current = setTimeout(() => {
+ clearTimer();
+ callback();
+ }, timeout);
+ }, []);
+
+ return [setTimer, clearTimer] as const;
+}
diff --git a/src/hooks/useTranslation.ts b/src/hooks/useTranslation.ts
new file mode 100644
index 00000000..ce69ede0
--- /dev/null
+++ b/src/hooks/useTranslation.ts
@@ -0,0 +1,8 @@
+import {useTranslation as useTranslationI18N} from 'react-i18next';
+
+import {configure} from '../config';
+
+export function useTranslation(...args: Parameters) {
+ configure();
+ return useTranslationI18N(...args);
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index aa542aa6..f5392ce1 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -72,6 +72,12 @@
"button-dislike-text": "No",
"main-question": "Was the article helpful?"
},
+ "feedback-variants": {
+ "irrelevant-answer": "No answer to my question",
+ "useless-recs": "Recommendations didn't help",
+ "content-mismatch": "The content doesn't match the title",
+ "other": "Other"
+ },
"search-bar": {
"search-query-label": "Found on request",
"close": "Close",
diff --git a/src/i18n/index.ts b/src/i18n/index.ts
deleted file mode 100644
index 88508290..00000000
--- a/src/i18n/index.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import i18n from 'i18next';
-import {initReactI18next} from 'react-i18next';
-import {Lang} from '../models';
-
-import ru from './ru.json';
-import en from './en.json';
-
-export default i18n.use(initReactI18next).init({
- fallbackLng: Lang.En,
- ns: ['controls', 'mini-toc', 'toc-nav-panel', 'authors'],
- resources: {ru, en},
- interpolation: {
- escapeValue: false,
- },
-});
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 7de7fb27..56638a00 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -72,6 +72,12 @@
"button-dislike-text": "Нет",
"main-question": "Была ли статья полезна?"
},
+ "feedback-variants": {
+ "irrelevant-answer": "Нет ответа на мой вопрос",
+ "useless-recs": "Рекомендации не помогли",
+ "content-mismatch": "Содержание не соответствует заголовку",
+ "other": "Другое"
+ },
"search-bar": {
"search-query-label": "Найдено по запросу",
"close": "Закрыть",
diff --git a/src/index.ts b/src/index.ts
index afd416ca..09e61511 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,3 @@
-// init i18n
-import './i18n';
-
export * from './components/Breadcrumbs';
export * from './components/DocLayout';
export * from './components/DocLeadingPage';
@@ -27,4 +24,6 @@ export * from './components/Paginator';
export * from './components/SearchItem';
export * from './components/SearchPage';
+export * from './config';
export * from './models';
+export * from './constants';
diff --git a/src/models/index.ts b/src/models/index.ts
index 13f1f0a2..46cc22bf 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -1,3 +1,5 @@
+import type {Loc} from '../config/i18n';
+
export enum Theme {
Light = 'light',
Dark = 'dark',
@@ -14,6 +16,11 @@ export enum ControlSizes {
L = 'l',
}
+export interface Config {
+ lang?: string;
+ loc?: Loc;
+}
+
export interface DocSettings {
fullScreen?: boolean;
singlePage?: boolean;