@sberbusiness/triplex-next-mcp-server 1.40.0-beta → 1.41.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,9 @@
1
1
  ---
2
2
  component: ThemeProvider
3
- category: Infrastructure
4
- figma-node: ""
5
- figma-file: ""
6
- related: [DesignTokens]
3
+ category: ThemeProvider
4
+ related: [useToken]
7
5
  tokens: []
8
- stories: ""
6
+ stories: stories/ThemeProvider/ThemeProvider.stories.tsx
9
7
  version: "1.0"
10
8
  ---
11
9
 
@@ -13,169 +11,359 @@ version: "1.0"
13
11
 
14
12
  ## Назначение
15
13
 
16
- Провайдер темы и дизайн-токенов: задаёт область видимости CSS-переменных
17
- через scoped className на HTML-элементе и подставляет токены темы (LIGHT/
18
- DARK) + опциональные пользовательские override'ы. Токены доступны через
19
- контекст (`useToken`).
14
+ Провайдер темы: рендерит css-переменные всех дизайн-токенов выбранной темы в тег `<style>`,
15
+ ограничивает их областью видимости элемента из `scopeRef` и передаёт тему и итоговые токены
16
+ дочерним компонентам через контекст (читается хуком `useToken`).
20
17
 
21
- Используй когда: нужно применить тему ко всему приложению или к
22
- изолированному поддереву (например, тёмная панель внутри светлого
23
- интерфейса). Вне `ThemeProvider` работает дефолтное `LIGHT` оформление.
18
+ Используй когда: приложению нужна тема Triplex Next (светлая или тёмная) либо точечное
19
+ переопределение дизайн-токенов — глобально на корне приложения или локально для части дерева.
20
+
21
+ Не используй когда: нужно поменять внешний вид одного компонента — для этого есть props
22
+ компонента (`theme`, `size`, `status`) и его собственные токены, а не подмена всей темы.
24
23
 
25
24
  ---
26
25
 
27
- ## Файловая структура
28
-
29
- ```text
30
- src/components/ThemeProvider/
31
- ├── ThemeProvider.tsx # FC<IThemeProviderProps>; updateCSS/removeCSS + ThemeProviderView
32
- ├── ThemeProviderContext.ts # IThemeProviderContext + контекст с дефолтом LIGHT
33
- ├── ETriplexNextTheme.ts # enum LIGHT/DARK
34
- ├── useToken.ts # хук-обёртка над контекстом
35
- ├── components/
36
- │ └── ThemeProviderView.tsx # классname на scopeRef + Context.Provider с merged-токенами
37
- ├── __tests__/
38
- └── index.ts
39
- ```
26
+ ## Варианты и props
40
27
 
41
- ---
28
+ ### Обязательные props
42
29
 
43
- ## Публичный API
30
+ | Prop | Тип | Описание |
31
+ |---|---|---|
32
+ | `children` | `React.ReactNode` | Контент, внутри которого действуют тема и токены |
33
+ | `scopeRef` | `React.RefObject<HTMLElement>` | Ref на элемент, на который навешивается `scopeClassName`. Именно этот элемент и его потомки видят css-переменные темы |
44
34
 
45
- ### `IThemeProviderProps`
35
+ ### Опциональные props
46
36
 
47
- | Prop | Тип | Default | Описание |
37
+ | Prop | Тип | По умолчанию | Описание |
48
38
  |---|---|---|---|
49
- | `children` | `ReactNode` | — | Дерево, использующее тему. |
50
- | `scopeRef` | `RefObject<HTMLElement>` | — | **Обязателен**. Ref на DOM-элемент, к которому добавится `scopeClassName` (обычно `html`/`body`/корневой div). |
51
- | `scopeClassName?` | `string` | `triplex-next-theme-{n}` (uniqueId) | Класс для области видимости. Если не передан — генерируется автоматически. |
52
- | `theme?` | `ETriplexNextTheme` | `LIGHT` | Тема. |
53
- | `tokens?` | `TDesignTokensPartial` | `{}` | Override отдельных токенов. |
54
-
55
- ### `ETriplexNextTheme`
56
-
57
- ```ts
58
- enum ETriplexNextTheme {
59
- LIGHT = "light",
60
- DARK = "dark",
61
- }
62
- ```
39
+ | `theme` | `ETriplexNextTheme` | `ETriplexNextTheme.LIGHT` | Дизайн-тема: `LIGHT` / `DARK` |
40
+ | `scopeClassName` | `string` | `uniqueId("triplex-next-theme-")` | Класс области видимости css-переменных. Задавай явно, если класс нужен снаружи (например, для портала, который рендерится вне `scopeRef`) |
41
+ | `tokens` | `TDesignTokensPartial` | `{}` | Частичное переопределение токенов поверх токенов темы |
42
+
43
+ ### Особенности
44
+
45
+ - `scopeRef` должен указывать на элемент **внутри** `children` (или на предка, существующего
46
+ на момент эффекта). Класс навешивается в `useEffect`, поэтому до первого эффекта переменных
47
+ на элементе ещё нет.
48
+ - `tokens` мерджится через `defaultsDeep` в порядке «переопределения потребителя → core-токены
49
+ темы → токены компонентов темы»: значения потребителя всегда выигрывают.
50
+ - `tokens` сравнивается по ссылке. Инлайн-литерал в JSX пересобирает `<style>` на каждый рендер —
51
+ выноси объект в константу или мемоизируй.
52
+ - Провайдеры вкладываются: `useToken` возвращает значения ближайшего `ThemeProvider`.
53
+ - Вне `ThemeProvider` контекст отдаёт светлую тему, токены по умолчанию и пустой `scopeClassName`.
63
54
 
64
- ### `IThemeProviderContext`
55
+ ---
65
56
 
66
- ```ts
67
- {
68
- scopeClassName: string;
69
- theme: ETriplexNextTheme;
70
- tokens: TDesignTokens; // полные токены (Core + Components, по теме + override)
71
- }
72
- ```
57
+ ## Дизайн-токены
73
58
 
74
- ### `useToken()`
59
+ Собственных токенов у компонента нет — он **источник** css-переменных для всех остальных
60
+ компонентов. В `<style>` с ключом `triplex-next-dynamic-tokens-{scopeClassName}` выводятся все
61
+ токены выбранной темы (core + компонентные) в виде:
75
62
 
76
- ```ts
77
- () => { scopeClassName: string; theme: ETriplexNextTheme; tokens: TDesignTokens }
63
+ ```
64
+ --triplex-next-{Группа}-{Токен}-{версия}: {значение};
78
65
  ```
79
66
 
80
- Чтение текущей темы и токенов из контекста.
67
+ Версия в имени переменной — версия пакета с точками, заменёнными на дефисы
68
+ (см. `DesignTokenUtils.getCSSVariableByTokenGroup`).
81
69
 
82
70
  ---
83
71
 
84
- ## Поведение
72
+ ## Инварианты
85
73
 
86
- ### Внедрение CSS
74
+ - `ThemeProvider` не рендерит собственный DOM-элемент — `forwardRef` неприменим,
75
+ ref на DOM передаётся потребителем через `scopeRef`.
76
+ - Публичные экспорты `src/components/ThemeProvider/index.ts` — `ThemeProvider`,
77
+ `IThemeProviderProps`, `ThemeProviderContext`, `IThemeProviderContext`, `ETriplexNextTheme`,
78
+ `useToken`. Ни один не удалять и не переименовывать.
79
+ - Значения `ETriplexNextTheme` (`"light"` / `"dark"`) — часть публичного API.
80
+ - Ключ тега стилей `triplex-next-dynamic-tokens-{scopeClassName}` и префикс автогенерируемого
81
+ класса `triplex-next-theme-` менять нельзя: по ключу стили обновляются и удаляются,
82
+ а класс попадает в разметку потребителя.
83
+ - Порядок аргументов `defaultsDeep` в `ThemeProviderView` менять нельзя — иначе токены темы
84
+ начнут переопределять пользовательские.
85
+ - `ThemeProviderContext` должен оставаться единственным экземпляром модуля: `useToken`
86
+ импортирует его по пути пакета (`@sberbusiness/triplex-next/components/ThemeProvider/...`).
87
+ Дублирование модуля через разные спецификаторы разорвёт связь провайдера и потребителей.
88
+ - Компоненты, рендерящиеся в портал вне `scopeRef`, сами переносят `scopeClassName`
89
+ из `useToken` на свой корневой узел — иначе css-переменные до них не доходят.
90
+ Новые портальные компоненты должны делать так же, и способ зависит от контейнера:
91
+ - **собственный контейнер** (`Dropdown`, `ModalWindow`, `LightBox`) — класс просто
92
+ добавляется в `clsx` корневого элемента;
93
+ - **общий контейнер, разделяемый несколькими экземплярами** (`Tooltip`,
94
+ см. `utils/useTooltipTheme.ts`) — класс навешивается через `classList` в эффекте
95
+ со счётчиком использований в data-атрибуте и снимается с задержкой после закрытия.
96
+ Без счётчика соседние экземпляры на том же контейнере затирали бы тему друг другу,
97
+ без задержки класс снимался бы до конца анимации закрытия.
98
+ - При размонтировании и при смене `scopeClassName` тег стилей удаляется, а класс снимается
99
+ с элемента — утечка стилей на страницу недопустима.
87
100
 
88
- `useLayoutEffect` (`ThemeProvider.tsx`):
89
- 1. `DesignTokenUtils.getStyle(theme, tokens)` → строка `--var-name: value;`.
90
- 2. `updateCSS(`.${scopeCssClassName} {…}`, ключ `triplex-next-dynamic-tokens-${scopeCssClassName}`)` — вставляет/обновляет `<style>` тег (через `rc-util/Dom/dynamicCSS`).
91
- 3. SSR: `canUseDom() === false` → `console.log` warning, без вставки.
101
+ ---
92
102
 
93
- При размонтировании — `removeCSS` по тому же ключу.
103
+ ## Accessibility
94
104
 
95
- ### Применение области видимости (`ThemeProviderView`)
105
+ Компонент не рендерит DOM и не влияет на семантику, фокус и клавиатуру. Отдельных требований
106
+ нет. Контрастность цветов — ответственность значений токенов темы.
96
107
 
97
- `useEffect` на `[scopeClassName, scopeRef]`:
98
- - Добавляет `scopeClassName` в `scopeRef.current.classList`.
99
- - При cleanup — удаляет `prevScopeClassName.current` (хранит предыдущий класс для корректного removal).
108
+ ---
100
109
 
101
- ### Сборка токенов в Context
110
+ ## Связанные компоненты
102
111
 
103
- `useMemo`:
104
- ```ts
105
- defaultsDeep(
106
- {},
107
- tokensProps, // 1. user override
108
- theme === LIGHT ? DesignTokensCore : DesignTokensCoreThemeDark,
109
- theme === LIGHT ? DesignTokensComponents : DesignTokensComponentsThemeDark,
110
- );
111
- ```
112
+ - `useToken` — хук доступа к текущей теме, токенам и `scopeClassName`. Основной способ
113
+ прочитать тему внутри компонента.
114
+ - `ThemeProviderContext` — контекст темы; экспортируется для нестандартных сценариев,
115
+ в обычном коде используй `useToken`.
116
+ - `ETriplexNextTheme` — enum доступных тем.
117
+ - `ThemeProviderView` — внутренний компонент (навешивает класс, собирает значение контекста),
118
+ не экспортируется из barrel.
119
+ - `DesignTokens` (`DesignTokensCore`, `DesignTokensComponents` и их тёмные варианты) —
120
+ источник значений токенов; `DesignTokenUtils.getStyle` формирует css-переменные.
121
+ - `Dropdown`, `ModalWindow`, `LightBox`, `Tooltip` — потребители `useToken`: переносят
122
+ `scopeClassName` на свои портальные контейнеры.
123
+
124
+ ---
125
+
126
+ ## Stories
112
127
 
113
- **Порядок важен** — `defaultsDeep` НЕ перезаписывает уже определённые ключи,
114
- поэтому первые источники имеют приоритет (user > Core > Components).
128
+ Основные истории: `stories/ThemeProvider/ThemeProvider.stories.tsx`
129
+ Файлы примеров: `stories/ThemeProvider/examples/`
130
+
131
+ | Story | Example file | Что демонстрирует |
132
+ |---|---|---|
133
+ | `Playground` | `Playground.tsx` | Интерактивный подбор темы, `scopeClassName` и переопределяемых токенов |
134
+ | `Default` | `Default.tsx` | Минимальный пример: светлая тема по умолчанию |
135
+ | `DarkTheme` | `DarkTheme.tsx` | Тёмная тема |
136
+ | `CustomTokens` | `CustomTokens.tsx` | Переопределение токена поверх темы |
137
+ | `ThemeSwitcher` | `ThemeSwitcher.tsx` | Переключение темы на лету |
138
+ | `ScopedTheme` | `ScopedTheme.tsx` | Область действия темы: контент вне `scopeRef` её не видит |
115
139
 
116
140
  ---
117
141
 
118
- ## Инварианты
142
+ ## История изменений
119
143
 
120
- - **`FC, не forwardRef`** — `ThemeProvider` ref не пробрасывает; ref на DOM передаётся через `scopeRef`.
121
- - **`scopeRef` обязателен** — без него `classList.add` не выполнится → стили не применятся.
122
- - **`scopeClassName` авто-генерируется** через `uniqueId("triplex-next-theme-")`, если не передан. Стабилен между рендерами (memo по `[scopeClassName]`).
123
- - **`useLayoutEffect` для вставки CSS** — критично, чтобы стили применились ДО первого paint'а детей. Не менять на `useEffect`.
124
- - **Cleanup `removeCSS`** при размонтировании — иначе утечки `<style>` тегов в `<head>`.
125
- - **Порядок `defaultsDeep` зафиксирован комментарием** — `tokensProps` идёт первым (приоритет), не менять без обсуждения.
126
- - **`prevScopeClassName: useRef`** — нужен для удаления старого класса при изменении `scopeClassName` (обычный сценарий — не возникает, но защита от смены через props).
127
- - **SSR не поддержан** — фоллбек на `console.log`. Реальная тема применится только после hydration на клиенте.
128
- - **Дефолтный контекст содержит `LIGHT` токены** (`DesignTokensCore + DesignTokensComponents`), не пустой объект. Компоненты вне `ThemeProvider` получают дефолтную светлую тему через `useToken`.
129
- - **Контекст имеет полные токены `TDesignTokens`**, а не `Partial` — `useToken` гарантированно возвращает все ключи.
144
+ | Дата | Изменение |
145
+ |---|---|
146
+ | 2026-08-05 | Создан документ. AI-рефакторинг: JSDoc на props и экспортах, ключ тега стилей вынесен в хелпер, упрощён эффект `ThemeProviderView`, добавлены unit-тесты `ThemeProviderView` и `useToken`, stories переведены на modern pattern |
147
+ | 2026-08-05 | `related` сокращён до `useToken`. Удалён неактуальный комментарий про относительный импорт в `ThemeProviderView` |
130
148
 
131
149
  ---
132
150
 
133
- ## Использование
151
+ ## Примеры использования
152
+
153
+ ### Default
154
+
155
+ `stories/ThemeProvider/examples/Default.tsx`
134
156
 
135
157
  ```tsx
136
- const App = () => {
137
- const rootRef = useRef<HTMLDivElement>(null);
158
+ import React, { useRef } from "react";
159
+ import {
160
+ ThemeProvider,
161
+ ETriplexNextTheme,
162
+ Button,
163
+ EButtonTheme,
164
+ EComponentSize,
165
+ Text,
166
+ ETextSize,
167
+ } from "@sberbusiness/triplex-next";
168
+
169
+ export const Default = () => {
170
+ const scopeRef = useRef<HTMLDivElement>(null);
138
171
 
139
172
  return (
140
- <div ref={rootRef}>
141
- <ThemeProvider scopeRef={rootRef} theme={ETriplexNextTheme.DARK}>
142
- <YourApp />
143
- </ThemeProvider>
144
- </div>
173
+ <ThemeProvider theme={ETriplexNextTheme.LIGHT} scopeRef={scopeRef}>
174
+ <div ref={scopeRef} style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 16 }}>
175
+ <Text size={ETextSize.B1}>Светлая тема — значение по умолчанию.</Text>
176
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
177
+ Button text
178
+ </Button>
179
+ </div>
180
+ </ThemeProvider>
145
181
  );
146
182
  };
183
+ ```
184
+
185
+ ### DarkTheme
186
+
187
+ `stories/ThemeProvider/examples/DarkTheme.tsx`
147
188
 
148
- // Чтение токенов в компоненте:
149
- const { tokens, theme } = useToken();
189
+ ```tsx
190
+ import React, { useRef } from "react";
191
+ import {
192
+ ThemeProvider,
193
+ ETriplexNextTheme,
194
+ Button,
195
+ EButtonTheme,
196
+ EComponentSize,
197
+ CardStatic,
198
+ ECardContentPaddingSize,
199
+ Text,
200
+ ETextSize,
201
+ } from "@sberbusiness/triplex-next";
202
+
203
+ export const DarkTheme = () => {
204
+ const scopeRef = useRef<HTMLDivElement>(null);
205
+
206
+ return (
207
+ <ThemeProvider theme={ETriplexNextTheme.DARK} scopeRef={scopeRef}>
208
+ {/*
209
+ ThemeProvider задаёт только css-переменные и не красит подложку — фон под тёмную тему
210
+ даёт компонент с собственным токеном фона, иначе светлый текст темы будет не виден.
211
+ */}
212
+ <div ref={scopeRef}>
213
+ <CardStatic>
214
+ <CardStatic.Content paddingSize={ECardContentPaddingSize.MD}>
215
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 16 }}>
216
+ <Text size={ETextSize.B1}>Тёмная тема.</Text>
217
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
218
+ Button text
219
+ </Button>
220
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
221
+ Button text
222
+ </Button>
223
+ </div>
224
+ </CardStatic.Content>
225
+ </CardStatic>
226
+ </div>
227
+ </ThemeProvider>
228
+ );
229
+ };
150
230
  ```
151
231
 
152
- Несколько `ThemeProvider` могут быть вложены — внутренний переопределяет
153
- внешний внутри своего scope.
232
+ ### CustomTokens
154
233
 
155
- ---
234
+ `stories/ThemeProvider/examples/CustomTokens.tsx`
156
235
 
157
- ## Accessibility
236
+ ```tsx
237
+ import React, { useRef } from "react";
238
+ import {
239
+ ThemeProvider,
240
+ ETriplexNextTheme,
241
+ Button,
242
+ EButtonTheme,
243
+ EComponentSize,
244
+ Text,
245
+ ETextSize,
246
+ } from "@sberbusiness/triplex-next";
247
+
248
+ // tokens сравнивается по ссылке — объект вынесен из рендера, чтобы style-тег не пересобирался.
249
+ const CUSTOM_TOKENS = {
250
+ ColorBrand: {
251
+ 50: { value: "blue" },
252
+ },
253
+ };
158
254
 
159
- - ARIA не задействована — это инфраструктурный компонент.
160
- - При смене темы для контрастности и читаемости — обеспечивается дизайн-токенами; компонент только применяет их.
255
+ export const CustomTokens = () => {
256
+ const scopeRef = useRef<HTMLDivElement>(null);
161
257
 
162
- ---
258
+ return (
259
+ <ThemeProvider theme={ETriplexNextTheme.LIGHT} tokens={CUSTOM_TOKENS} scopeRef={scopeRef}>
260
+ <div ref={scopeRef} style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 16 }}>
261
+ <Text size={ETextSize.B1}>Токен ColorBrand.50 переопределён — кнопка перекрашена.</Text>
262
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
263
+ Button text
264
+ </Button>
265
+ </div>
266
+ </ThemeProvider>
267
+ );
268
+ };
269
+ ```
163
270
 
164
- ## Связанные компоненты
271
+ ### ThemeSwitcher
165
272
 
166
- - `DesignTokens` (`DesignTokensCore`, `DesignTokensComponents`, `*ThemeDark` варианты, `DesignTokenUtils`) — источники значений.
167
- - Все компоненты дизайн-системы — потребители токенов через CSS-переменные `--triplex-next-*`.
273
+ `stories/ThemeProvider/examples/ThemeSwitcher.tsx`
168
274
 
169
- ---
275
+ ```tsx
276
+ import React, { useRef, useState } from "react";
277
+ import {
278
+ ThemeProvider,
279
+ ETriplexNextTheme,
280
+ Button,
281
+ EButtonTheme,
282
+ EComponentSize,
283
+ CardStatic,
284
+ ECardContentPaddingSize,
285
+ Text,
286
+ ETextSize,
287
+ } from "@sberbusiness/triplex-next";
288
+
289
+ export const ThemeSwitcher = () => {
290
+ const scopeRef = useRef<HTMLDivElement>(null);
291
+ const [theme, setTheme] = useState(ETriplexNextTheme.LIGHT);
292
+
293
+ const handleToggleClick = () => {
294
+ setTheme((prevTheme) =>
295
+ prevTheme === ETriplexNextTheme.LIGHT ? ETriplexNextTheme.DARK : ETriplexNextTheme.LIGHT,
296
+ );
297
+ };
170
298
 
171
- ## Stories
299
+ return (
300
+ <ThemeProvider theme={theme} scopeRef={scopeRef}>
301
+ {/* Подложку под тему даёт компонент с собственным токеном фона — ThemeProvider её не красит. */}
302
+ <div ref={scopeRef}>
303
+ <CardStatic>
304
+ <CardStatic.Content paddingSize={ECardContentPaddingSize.MD}>
305
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 16 }}>
306
+ <Text size={ETextSize.B1}>
307
+ Текущая тема: {theme === ETriplexNextTheme.LIGHT ? "светлая" : "тёмная"}
308
+ </Text>
309
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD} onClick={handleToggleClick}>
310
+ Переключить тему
311
+ </Button>
312
+ </div>
313
+ </CardStatic.Content>
314
+ </CardStatic>
315
+ </div>
316
+ </ThemeProvider>
317
+ );
318
+ };
319
+ ```
172
320
 
173
- Отдельных stories нет — оборачивает все остальные компоненты в Storybook на уровне preview.
321
+ ### ScopedTheme
174
322
 
175
- ---
323
+ `stories/ThemeProvider/examples/ScopedTheme.tsx`
176
324
 
177
- ## История изменений
325
+ ```tsx
326
+ import React, { useRef } from "react";
327
+ import {
328
+ ThemeProvider,
329
+ ETriplexNextTheme,
330
+ Button,
331
+ EButtonTheme,
332
+ EComponentSize,
333
+ CardStatic,
334
+ ECardContentPaddingSize,
335
+ Text,
336
+ ETextSize,
337
+ } from "@sberbusiness/triplex-next";
338
+
339
+ export const ScopedTheme = () => {
340
+ const darkScopeRef = useRef<HTMLDivElement>(null);
341
+
342
+ // Обе области — одинаковая разметка. Отличается только тема: вторая обёрнута в ThemeProvider.
343
+ const content = (label: string) => (
344
+ <CardStatic>
345
+ <CardStatic.Content paddingSize={ECardContentPaddingSize.MD}>
346
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 16 }}>
347
+ <Text size={ETextSize.B1}>{label}</Text>
348
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
349
+ Button text
350
+ </Button>
351
+ </div>
352
+ </CardStatic.Content>
353
+ </CardStatic>
354
+ );
355
+
356
+ return (
357
+ <div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
358
+ {content("Область вне ThemeProvider — тема приложения.")}
359
+
360
+ <ThemeProvider theme={ETriplexNextTheme.DARK} scopeRef={darkScopeRef}>
361
+ <div ref={darkScopeRef}>
362
+ {content("Область с тёмной темой — css-переменные действуют только здесь.")}
363
+ </div>
364
+ </ThemeProvider>
365
+ </div>
366
+ );
367
+ };
368
+ ```
178
369
 
179
- | Дата | Изменение |
180
- |---|---|
181
- | 2026-04-18 | Создан документ |