@sberbusiness/triplex-next-mcp-server 1.43.0-beta → 1.45.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.
- package/README.md +7 -6
- package/bundle/components/AlertContext.md +11 -8
- package/bundle/components/AlertProcess.md +16 -12
- package/bundle/components/Badge.md +6 -3
- package/bundle/components/BodyPage.md +72 -4
- package/bundle/components/Button.md +46 -43
- package/bundle/components/ButtonDropdownExtended.md +3 -2
- package/bundle/components/ButtonIcon.md +7 -4
- package/bundle/components/Calendar.md +62 -59
- package/bundle/components/Caption.md +26 -23
- package/bundle/components/Checkbox.md +32 -29
- package/bundle/components/CheckboxTreeExtended.md +9 -5
- package/bundle/components/Chip.md +44 -41
- package/bundle/components/CodeText.md +26 -23
- package/bundle/components/Col.md +1 -2
- package/bundle/components/CollapsibleTree.md +8 -5
- package/bundle/components/CollapsibleTreeExtended.md +6 -1
- package/bundle/components/DatePickerExtended.md +409 -262
- package/bundle/components/Dropdown.md +33 -28
- package/bundle/components/EmptyView.md +12 -5
- package/bundle/components/ExpandAnimation.md +263 -218
- package/bundle/components/Footer.md +235 -278
- package/bundle/components/FooterPage.md +6 -2
- package/bundle/components/FormField.md +44 -41
- package/bundle/components/FormGroup.md +192 -52
- package/bundle/components/Header.md +491 -211
- package/bundle/components/HeaderPage.md +6 -2
- package/bundle/components/ImageGallery.md +33 -34
- package/bundle/components/ImageGalleryExtended.md +33 -34
- package/bundle/components/Island.md +52 -14
- package/bundle/components/LightBox.md +25 -18
- package/bundle/components/Link.md +211 -96
- package/bundle/components/ListActionItem.md +7 -4
- package/bundle/components/ListItem.md +11 -8
- package/bundle/components/ListItemControlsButton.md +16 -13
- package/bundle/components/ListItemControlsButtonDropdown.md +9 -6
- package/bundle/components/ListItemSelectable.md +7 -4
- package/bundle/components/ListMasterFooter.md +8 -5
- package/bundle/components/ListMasterHeader.md +8 -5
- package/bundle/components/ListSortableItem.md +11 -8
- package/bundle/components/ListTableItem.md +7 -4
- package/bundle/components/LoaderMiddle.md +7 -4
- package/bundle/components/LoaderScreen.md +11 -8
- package/bundle/components/LoaderSmall.md +7 -4
- package/bundle/components/Marker.md +184 -69
- package/bundle/components/MarkerStatus.md +206 -88
- package/bundle/components/MaskedField.md +2 -4
- package/bundle/components/MasterTable.md +602 -0
- package/bundle/components/ModalWindow.md +7 -4
- package/bundle/components/MultiselectField.md +2183 -132
- package/bundle/components/Notification.md +12 -9
- package/bundle/components/Overlay.md +33 -5
- package/bundle/components/Page.md +2 -2
- package/bundle/components/Pagination.md +27 -21
- package/bundle/components/Row.md +1 -1
- package/bundle/components/SelectExtendedField.md +3 -3
- package/bundle/components/SelectField.md +466 -148
- package/bundle/components/SliderExtended.md +39 -36
- package/bundle/components/SmallInput.md +194 -51
- package/bundle/components/Suggest.md +535 -99
- package/bundle/components/SwipeableArea.md +428 -79
- package/bundle/components/TableBasic.md +2302 -0
- package/bundle/components/TabsExtended.md +24 -21
- package/bundle/components/Tag.md +263 -78
- package/bundle/components/Text.md +28 -25
- package/bundle/components/TextField.md +3 -4
- package/bundle/components/ThemeProvider.md +50 -1
- package/bundle/components/Title.md +26 -23
- package/bundle/components/Tooltip.md +24 -21
- package/bundle/components/TopOverlay.md +368 -89
- package/bundle/components/TreeView.md +343 -166
- package/bundle/components/TreeViewNode.md +436 -0
- package/bundle/guides/codestyle.md +3 -0
- package/bundle/guides/commits.md +106 -26
- package/bundle/guides/context.md +84 -12
- package/bundle/guides/guidelines.md +321 -0
- package/bundle/guides/template.md +22 -12
- package/bundle/manifest.json +54 -20
- package/bundle/release-notes/1.44.0.mdx +113 -0
- package/bundle/release-notes/1.45.0.mdx +124 -0
- package/bundle/release-notes/1.46.0.mdx +7 -0
- package/bundle/tokens.json +6362 -0
- package/dist/index.js +177 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: TreeViewNode
|
|
3
|
+
category: TreeView
|
|
4
|
+
related: [TreeView]
|
|
5
|
+
tokens: []
|
|
6
|
+
stories: stories/TreeView/TreeView.stories.tsx
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# TreeViewNode
|
|
11
|
+
|
|
12
|
+
## Назначение
|
|
13
|
+
|
|
14
|
+
Узел визуального дерева `TreeView` (доступен также как `TreeView.Node`). Рендерит
|
|
15
|
+
`<li role="treeitem">` с `aria-expanded` и `tabIndex`, регистрирует себя в
|
|
16
|
+
абстрактном дереве `TreeView`, обрабатывает `focus`/`blur` для перемещения
|
|
17
|
+
активной ноды с клавиатуры и отдаёт потребителю пять provide-props через
|
|
18
|
+
render-функцию `children`.
|
|
19
|
+
|
|
20
|
+
Собственного оформления у ноды нет: она добавляет только семантику и сброс
|
|
21
|
+
маркеров списка — всю разметку узла (шеврон, подпись, линии-коннекторы,
|
|
22
|
+
подсветку активного состояния) рисует потребитель.
|
|
23
|
+
|
|
24
|
+
Используй когда: строишь дерево на `TreeView` и нужен полный контроль над
|
|
25
|
+
разметкой узла.
|
|
26
|
+
|
|
27
|
+
Не используй когда:
|
|
28
|
+
- нужно типовое дерево «папка/файл» с шевроном, hover и focus-visible — бери
|
|
29
|
+
`CollapsibleTree`;
|
|
30
|
+
- нужен свой UI узла, но с анимацией раскрытия и controlled/uncontrolled
|
|
31
|
+
состоянием — бери `CollapsibleTreeExtended`, он оборачивает `TreeView.Node`
|
|
32
|
+
и добавляет accordion-логику;
|
|
33
|
+
- узел нужен вне `TreeView` — вне провайдера контекста нода получает
|
|
34
|
+
`initialTreeContext`, где `registerNode` и остальные методы пустые: `<li>`
|
|
35
|
+
отрисуется, но в дерево нода не попадёт, `tabIndex` останется `-1`,
|
|
36
|
+
`hasChildNodes` всегда будет `false` (дети не регистрируются), а
|
|
37
|
+
`isLastNode` — всегда `true` (у ноды нет родителя).
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Варианты и props
|
|
42
|
+
|
|
43
|
+
Экспортируемый `TreeViewNode` — это `withTreeViewContext(TreeViewNodeWithContext)`,
|
|
44
|
+
то есть class-компонент, обёрнутый HOC-ом контекста. Prop `treeViewContext`
|
|
45
|
+
подставляет HOC, снаружи его передавать не нужно.
|
|
46
|
+
|
|
47
|
+
### Обязательные props
|
|
48
|
+
|
|
49
|
+
| Prop | Тип | Описание |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `id` | `string` | Идентификатор ноды, уникальный в пределах дерева. По нему абстрактное дерево ищет и связывает ноды, на него ссылаются `prevNodeId` / `nextNodeId` соседей |
|
|
52
|
+
| `children` | `(props: ITreeViewNodeProvideProps) => JSX.Element` | Render-функция разметки узла. Не ReactNode: нода отдаёт своё состояние только через аргумент |
|
|
53
|
+
|
|
54
|
+
### Опциональные props
|
|
55
|
+
|
|
56
|
+
| Prop | Тип | По умолчанию | Описание |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| `opened` | `boolean` | `false` | Состояние раскрытия. Синхронизируется в абстрактное дерево в конструкторе и далее в `componentDidUpdate` при смене значения |
|
|
59
|
+
| `prevNodeId` | `string` | — | Id ноды, после которой вставить текущую в абстрактное дерево |
|
|
60
|
+
| `nextNodeId` | `string` | — | Id ноды, перед которой вставить текущую в абстрактное дерево |
|
|
61
|
+
| `...HTMLAttributes` | — | — | Уходят на `<li>`. `className` мерджится с собственным классом, `onFocus` / `onBlur` вызываются после внутренних обработчиков |
|
|
62
|
+
|
|
63
|
+
`ITreeViewNodeProps` расширяет `Omit<React.HTMLAttributes<HTMLElement>, "children">` —
|
|
64
|
+
`children` переопределён под render-функцию намеренно.
|
|
65
|
+
|
|
66
|
+
### Provide-props render-функции (`ITreeViewNodeProvideProps`)
|
|
67
|
+
|
|
68
|
+
| Поле | Описание |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `activeNode` | Нода активна при перемещении с клавиатуры. Подсветку рисует потребитель |
|
|
71
|
+
| `openedNode` | Нода раскрыта — значение из абстрактного дерева, а не из prop `opened` |
|
|
72
|
+
| `hasChildNodes` | У ноды есть **зарегистрированные** дочерние ноды |
|
|
73
|
+
| `isLastNode` | Нода последняя в дереве целиком (последняя на своём уровне и все её предки — последние). Используется для линий-коннекторов |
|
|
74
|
+
| `setOpenedNode(opened)` | Пишет флаг раскрытия в абстрактную ноду и инкрементит `updateCount` в `TreeView` для перерендера. Uncontrolled-путь: prop `opened` при этом не меняется |
|
|
75
|
+
|
|
76
|
+
### Порядок нод и `prevNodeId` / `nextNodeId`
|
|
77
|
+
|
|
78
|
+
Абстрактное дерево заполняется в **конструкторах** нод, то есть в порядке
|
|
79
|
+
рендера. `prevNodeId` / `nextNodeId` позволяют вставить ноду в произвольное
|
|
80
|
+
место уровня: порядок в абстрактном дереве (а значит `isLastNode` и порядок
|
|
81
|
+
обхода стрелками) может отличаться от порядка в разметке. Ссылаться можно
|
|
82
|
+
только на **уже зарегистрированную** ноду: к моменту конструктора текущей ноды
|
|
83
|
+
конструктор соседа должен был отработать — монтирование при этом не требуется,
|
|
84
|
+
оно происходит позже. Id соседей выводят из массива данных
|
|
85
|
+
`CollapsibleTree` и `CheckboxTree`; `CollapsibleTreeExtended` своего массива не
|
|
86
|
+
имеет и пробрасывает `prevNodeId` / `nextNodeId` как есть от потребителя.
|
|
87
|
+
|
|
88
|
+
Промаха по id компонент не сигнализирует, но два случая промаха ведут себя
|
|
89
|
+
по-разному. Ноду с указанным id ищут **в поддереве текущего родителя**, а не по
|
|
90
|
+
всему дереву.
|
|
91
|
+
|
|
92
|
+
- **Id не найден** — нода добавляется в конец своего уровня, то есть в порядке
|
|
93
|
+
рендера. Детерминированно и безопасно; это штатный режим, а не ошибка.
|
|
94
|
+
Именно так работает `nextNodeId` в `CollapsibleTree` и `CheckboxTree`: они
|
|
95
|
+
передают id следующей ноды из массива данных, а та на момент конструктора
|
|
96
|
+
текущей ещё не зарегистрирована — промах и откат в порядок рендера здесь
|
|
97
|
+
происходят всегда.
|
|
98
|
+
- **Id найден, но это не прямой ребёнок того же родителя** — позиция
|
|
99
|
+
определяется индексной арифметикой `splice` в `AbstractTreeNode.addChild`.
|
|
100
|
+
Это **деталь текущей реализации, а не контракт**: тестами не закреплена,
|
|
101
|
+
класс общий для всех абстрактных деревьев, полагаться нельзя.
|
|
102
|
+
|
|
103
|
+
Правильное употребление одно: ссылаться на уже зарегистрированную соседнюю ноду
|
|
104
|
+
того же уровня.
|
|
105
|
+
|
|
106
|
+
### Раскрытие и регистрация детей
|
|
107
|
+
|
|
108
|
+
`hasChildNodes` вычисляется по абстрактному дереву, поэтому дочерние ноды
|
|
109
|
+
должны быть **смонтированы**. Если размонтировать `TreeView.Group` в свёрнутом
|
|
110
|
+
состоянии, родитель получит `hasChildNodes === false` и, например, не отрисует
|
|
111
|
+
шеврон. Рабочий приём (используется в stories и в `CollapsibleTreeExtended`) —
|
|
112
|
+
держать группу в разметке всегда, скрывая её стилями.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Дизайн-токены
|
|
117
|
+
|
|
118
|
+
Собственных CSS-переменных нода не определяет. Класс `treeViewNode` в
|
|
119
|
+
`styles/TreeView.module.less` задаёт только сброс отступов и маркера списка
|
|
120
|
+
плюс `outline: none` — focus-стиль рисует потребитель.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Инварианты
|
|
125
|
+
|
|
126
|
+
- **`TreeViewNodeWithContext` — class-компонент, и `forwardRef` на нём нет.**
|
|
127
|
+
Регистрация в абстрактном дереве происходит в **конструкторе** (до маунта
|
|
128
|
+
детей), а снятие — в `componentWillUnmount`; порядок этих вызовов и есть
|
|
129
|
+
порядок нод в дереве. Перевод на функциональный компонент меняет момент
|
|
130
|
+
регистрации и ломает `prevNodeId` / `nextNodeId`. Не мигрировать без
|
|
131
|
+
отдельного решения мейнтейнера.
|
|
132
|
+
- **Корневой DOM-элемент — `<li>`.** Менять тег нельзя: `role="treeitem"`
|
|
133
|
+
валиден только внутри `<ul role="tree">` / `<ul role="group">`.
|
|
134
|
+
- **`role="treeitem"` и `aria-expanded` выставляются после `{...props}`** —
|
|
135
|
+
семантика узла часть контракта компонента, потребитель её не переопределяет.
|
|
136
|
+
`tabIndex` остаётся **до** спреда: roving tabindex — поведение, а не
|
|
137
|
+
семантика, и потребитель при желании может задать его сам. Но переданный
|
|
138
|
+
`tabIndex` побеждает навсегда: `TreeView.updateTabIndexNodes` продолжит
|
|
139
|
+
пересчитывать значение в абстрактной ноде, на DOM это уже не повлияет. Поставив
|
|
140
|
+
`tabIndex={0}` всем нодам, потребитель получит не свой roving tabindex, а
|
|
141
|
+
дерево, где каждая нода — отдельная остановка Tab. Переопределять — только если
|
|
142
|
+
берёшь управление фокусом целиком на себя.
|
|
143
|
+
- **`event.stopPropagation()` в `handleFocus` / `handleBlur` обязателен.**
|
|
144
|
+
`focusin` всплывает, и без остановки активной становилась бы не текущая нода,
|
|
145
|
+
а её родитель. Пользовательские `onFocus` / `onBlur` вызываются после
|
|
146
|
+
внутренней логики — порядок менять нельзя.
|
|
147
|
+
- **`children` — функция, а не `ReactNode`.** Смена сигнатуры — breaking change
|
|
148
|
+
для всей цепочки потребителей. Напрямую `TreeView.Node` рендерит только
|
|
149
|
+
`CollapsibleTreeExtended`; `CollapsibleTree` строится поверх него, а
|
|
150
|
+
`CheckboxTree` доходит до ноды через `CheckboxTreeExtended` →
|
|
151
|
+
`CollapsibleTreeExtended`.
|
|
152
|
+
- **Barrel `src/components/TreeView/index.ts`** реэкспортирует модуль ноды
|
|
153
|
+
целиком (`export * from "./components/TreeViewNode"`), поэтому публичны и
|
|
154
|
+
`TreeViewNode`, и класс `TreeViewNodeWithContext`, и типы
|
|
155
|
+
`ITreeViewNodeProps`, `ITreeViewNodeProvideProps`,
|
|
156
|
+
`ITreeViewNodePropsWithContext` — всё это через `src/components/index.ts`
|
|
157
|
+
доходит до корня пакета. Состав и имена менять нельзя.
|
|
158
|
+
- **`displayName` класса — `"TreeViewNodeWithContext"`**, у экспортируемой
|
|
159
|
+
обёртки — `"WithTreeViewContext(TreeViewNodeWithContext)"` (добавляет HOC).
|
|
160
|
+
Значения видны в React DevTools и в snapshot-тестах.
|
|
161
|
+
- **Абстрактная нода мутируемая.** `this.abstractNode` меняется на месте, и сам
|
|
162
|
+
по себе перерендер не вызывает: любая мутация должна идти через методы
|
|
163
|
+
контекста (`setOpenedNode`, `setActiveNode`, `registerNode`, `removeNode`),
|
|
164
|
+
которые инкрементят `updateCount` в состоянии `TreeView`. Прямая запись в
|
|
165
|
+
`abstractNode` в обход контекста оставит ноду с устаревшими
|
|
166
|
+
`hasChildNodes` / `isLastNode`.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Accessibility
|
|
171
|
+
|
|
172
|
+
Ориентир — WAI-ARIA Authoring Practices,
|
|
173
|
+
[treeview-2a](https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-2/treeview-2a.html).
|
|
174
|
+
|
|
175
|
+
- Узел — `<li role="treeitem">` с `aria-expanded`, отражающим состояние
|
|
176
|
+
раскрытия из абстрактного дерева. Вложенные ноды оборачиваются в
|
|
177
|
+
`TreeView.Group` (`<ul role="group">`).
|
|
178
|
+
- **Roving tabindex.** `tabIndex` нода читает из абстрактной ноды
|
|
179
|
+
(`getTabIndex()`): первая нода первого уровня получает `0`, остальные `-1`.
|
|
180
|
+
Пересчитывает значения `TreeView` при регистрации и удалении нод — в дерево
|
|
181
|
+
можно попасть одним Tab.
|
|
182
|
+
- **Активная нода ≠ DOM-фокус.** Активной нода становится по `focus`, а стрелки
|
|
183
|
+
(`ArrowDown` / `ArrowUp`, обрабатываются в `TreeView` на `window`) двигают
|
|
184
|
+
только флаг активности в абстрактном дереве — DOM-фокус остаётся на исходном
|
|
185
|
+
узле. Потребитель обязан визуально отрисовать активное состояние по
|
|
186
|
+
provide-prop `activeNode`, иначе клавиатурная навигация не видна.
|
|
187
|
+
- `blur` снимает активность; когда активной ноды в дереве не остаётся,
|
|
188
|
+
`TreeView` отписывает слушатель стрелок.
|
|
189
|
+
- Раскрытие/сворачивание по клавиатуре (`ArrowRight` / `ArrowLeft`,
|
|
190
|
+
`Home` / `End`) нода **не** реализует — это на потребителе, в его разметке
|
|
191
|
+
узла.
|
|
192
|
+
- Текстовые подписи и `aria-label` нода не хардкодит: библиотека мультиязычная,
|
|
193
|
+
всю разметку узла даёт потребитель. `aria-label` на дерево вешается на
|
|
194
|
+
`TreeView`.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Связанные компоненты
|
|
199
|
+
|
|
200
|
+
- `TreeView` (`src/components/TreeView`) — родительский компонент и владелец
|
|
201
|
+
абстрактного дерева: регистрирует ноды, считает `tabIndex`, обрабатывает
|
|
202
|
+
стрелки. Нода без него не работает.
|
|
203
|
+
- `TreeViewGroup` (`TreeView.Group`) — `<ul role="group">`, обёртка вложенных
|
|
204
|
+
нод. Тривиальная обёртка (`forwardRef` + `clsx` + спред), отдельного AI.md
|
|
205
|
+
нет.
|
|
206
|
+
- `CollapsibleTreeExtended` (`src/components/CollapsibleTreeExtended`) — обёртка
|
|
207
|
+
над `TreeView.Node`, добавляющая анимацию раскрытия и
|
|
208
|
+
controlled/uncontrolled состояние. Берут вместо голой ноды, когда свой UI
|
|
209
|
+
узла нужен, а возиться с состоянием — нет.
|
|
210
|
+
- `CollapsibleTree` (`src/components/CollapsibleTree`) и `CheckboxTree`
|
|
211
|
+
(`src/components/CheckboxTree`) — готовые деревья поверх ноды; оба
|
|
212
|
+
протаскивают `prevNodeId` / `nextNodeId` из массива данных.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Stories
|
|
217
|
+
|
|
218
|
+
Отдельных stories у ноды нет: без `TreeView` она не рендерится, а собственного
|
|
219
|
+
оформления у неё нет — все её состояния показаны в stories родителя. Ниже —
|
|
220
|
+
только те, что демонстрируют API самой ноды; полный список историй семейства
|
|
221
|
+
(`Playground`, `Default`) — в [TreeView-ai.md](./TreeView-ai.md).
|
|
222
|
+
|
|
223
|
+
Основные истории: `stories/TreeView/TreeView.stories.tsx`
|
|
224
|
+
Файлы примеров: `stories/TreeView/examples/`
|
|
225
|
+
|
|
226
|
+
| Story | Example file | Что демонстрирует |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| `Nested` | `Nested.tsx` | Вложенность через `TreeView.Group`, prop `opened`, provide-props `openedNode` и `hasChildNodes` |
|
|
229
|
+
| `Keyboard navigation` | `KeyboardNavigation.tsx` | Перемещение `activeNode` стрелками, подсветка активной ноды, `isLastNode` |
|
|
230
|
+
| `VisualTests` | `VisualTests.tsx` | Свёрнутая и раскрытая нода, узел с детьми и лист, `isLastNode`; активная нода снимается через `play` |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## История изменений
|
|
235
|
+
|
|
236
|
+
| Дата | Изменение |
|
|
237
|
+
|---|---|
|
|
238
|
+
| 2026-08-26 | Создан документ. Описание ноды вынесено из `TreeView-ai.md`. Добавлен unit-тест на `prevNodeId` — единственный публичный prop ноды, не покрытый тестами |
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Примеры использования
|
|
243
|
+
|
|
244
|
+
### Nested
|
|
245
|
+
|
|
246
|
+
`stories/TreeView/examples/Nested.tsx`
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
import React from "react";
|
|
250
|
+
import clsx from "clsx";
|
|
251
|
+
import { CaretrightStrokeSrvIcon24 } from "@sberbusiness/icons-next";
|
|
252
|
+
import { ETextSize, Text, TreeView } from "@sberbusiness/triplex-next";
|
|
253
|
+
import "./Nested.less";
|
|
254
|
+
|
|
255
|
+
interface INodeData {
|
|
256
|
+
id: string;
|
|
257
|
+
label: string;
|
|
258
|
+
children?: INodeData[];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const nodes: INodeData[] = [
|
|
262
|
+
{
|
|
263
|
+
id: "folder-1",
|
|
264
|
+
label: "Folder text",
|
|
265
|
+
children: [
|
|
266
|
+
{ id: "file-1-1", label: "File text" },
|
|
267
|
+
{ id: "file-1-2", label: "File text" },
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "folder-2",
|
|
272
|
+
label: "Folder text",
|
|
273
|
+
children: [
|
|
274
|
+
{
|
|
275
|
+
id: "folder-2-1",
|
|
276
|
+
label: "Folder text",
|
|
277
|
+
children: [{ id: "file-2-1-1", label: "File text" }],
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
{ id: "file-3", label: "File text" },
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
interface ITreeNodeProps {
|
|
285
|
+
node: INodeData;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const TreeNode = ({ node }: ITreeNodeProps) => {
|
|
289
|
+
const [opened, setOpened] = React.useState(false);
|
|
290
|
+
const branch = Boolean(node.children?.length);
|
|
291
|
+
|
|
292
|
+
const toggle = () => setOpened((prevOpened) => !prevOpened);
|
|
293
|
+
|
|
294
|
+
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
295
|
+
// Клик по вложенной ноде не должен раскрывать родительскую.
|
|
296
|
+
event.stopPropagation();
|
|
297
|
+
|
|
298
|
+
if (branch) {
|
|
299
|
+
toggle();
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
|
304
|
+
if (event.key !== "Enter" && event.key !== " ") {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
event.preventDefault();
|
|
309
|
+
event.stopPropagation();
|
|
310
|
+
|
|
311
|
+
if (branch) {
|
|
312
|
+
toggle();
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<TreeView.Node id={node.id} opened={opened} onClick={handleClick} onKeyDown={handleKeyDown}>
|
|
318
|
+
{({ hasChildNodes, openedNode }) => (
|
|
319
|
+
<>
|
|
320
|
+
<div className={clsx("tree-view-nested-example-row", { branch })}>
|
|
321
|
+
{hasChildNodes ? (
|
|
322
|
+
<CaretrightStrokeSrvIcon24
|
|
323
|
+
paletteIndex={5}
|
|
324
|
+
className={clsx("tree-view-nested-example-chevron", { opened: openedNode })}
|
|
325
|
+
aria-hidden
|
|
326
|
+
/>
|
|
327
|
+
) : (
|
|
328
|
+
<span className="tree-view-nested-example-chevron-placeholder" />
|
|
329
|
+
)}
|
|
330
|
+
<Text size={ETextSize.B1} tag="span">
|
|
331
|
+
{node.label}
|
|
332
|
+
</Text>
|
|
333
|
+
</div>
|
|
334
|
+
{node.children ? (
|
|
335
|
+
// Группа остаётся смонтированной: так вложенные ноды зарегистрированы в дереве
|
|
336
|
+
// и родитель знает про hasChildNodes даже в свёрнутом состоянии.
|
|
337
|
+
<TreeView.Group className={clsx("tree-view-nested-example-group", { collapsed: !openedNode })}>
|
|
338
|
+
{node.children.map((child) => (
|
|
339
|
+
<TreeNode key={child.id} node={child} />
|
|
340
|
+
))}
|
|
341
|
+
</TreeView.Group>
|
|
342
|
+
) : null}
|
|
343
|
+
</>
|
|
344
|
+
)}
|
|
345
|
+
</TreeView.Node>
|
|
346
|
+
);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export const Nested = () => (
|
|
350
|
+
<div className="tree-view-nested-example" style={{ maxWidth: 420 }}>
|
|
351
|
+
<TreeView aria-label="Tree">
|
|
352
|
+
{nodes.map((node) => (
|
|
353
|
+
<TreeNode key={node.id} node={node} />
|
|
354
|
+
))}
|
|
355
|
+
</TreeView>
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Keyboard navigation
|
|
361
|
+
|
|
362
|
+
`stories/TreeView/examples/KeyboardNavigation.tsx`
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
import React from "react";
|
|
366
|
+
import clsx from "clsx";
|
|
367
|
+
import { ECaptionSize, ETextSize, Caption, Text, TreeView } from "@sberbusiness/triplex-next";
|
|
368
|
+
import "./KeyboardNavigation.less";
|
|
369
|
+
|
|
370
|
+
interface INodeData {
|
|
371
|
+
id: string;
|
|
372
|
+
label: string;
|
|
373
|
+
children?: INodeData[];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const nodes: INodeData[] = [
|
|
377
|
+
{
|
|
378
|
+
id: "folder-1",
|
|
379
|
+
label: "Folder text",
|
|
380
|
+
children: [
|
|
381
|
+
{ id: "file-1-1", label: "File text" },
|
|
382
|
+
{ id: "file-1-2", label: "File text" },
|
|
383
|
+
],
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: "folder-2",
|
|
387
|
+
label: "Folder text",
|
|
388
|
+
children: [{ id: "file-2-1", label: "File text" }],
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
interface ITreeNodeProps {
|
|
393
|
+
node: INodeData;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const TreeNode = ({ node }: ITreeNodeProps) => (
|
|
397
|
+
// Ветки раскрыты сразу: стрелки спускаются только в раскрытые ноды.
|
|
398
|
+
<TreeView.Node id={node.id} opened>
|
|
399
|
+
{({ activeNode, isLastNode }) => (
|
|
400
|
+
<>
|
|
401
|
+
<div className={clsx("tree-view-keyboard-example-row", { active: activeNode })}>
|
|
402
|
+
<Text size={ETextSize.B1} tag="span">
|
|
403
|
+
{node.label}
|
|
404
|
+
</Text>
|
|
405
|
+
{isLastNode ? (
|
|
406
|
+
<Caption size={ECaptionSize.C2} tag="span">
|
|
407
|
+
isLastNode
|
|
408
|
+
</Caption>
|
|
409
|
+
) : null}
|
|
410
|
+
</div>
|
|
411
|
+
{node.children ? (
|
|
412
|
+
<TreeView.Group>
|
|
413
|
+
{node.children.map((child) => (
|
|
414
|
+
<TreeNode key={child.id} node={child} />
|
|
415
|
+
))}
|
|
416
|
+
</TreeView.Group>
|
|
417
|
+
) : null}
|
|
418
|
+
</>
|
|
419
|
+
)}
|
|
420
|
+
</TreeView.Node>
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
export const KeyboardNavigation = () => (
|
|
424
|
+
<div className="tree-view-keyboard-example" style={{ maxWidth: 420 }}>
|
|
425
|
+
<Caption size={ECaptionSize.C1} tag="p">
|
|
426
|
+
Поставьте фокус на любой узел и используйте стрелки вверх/вниз.
|
|
427
|
+
</Caption>
|
|
428
|
+
<TreeView aria-label="Tree">
|
|
429
|
+
{nodes.map((node) => (
|
|
430
|
+
<TreeNode key={node.id} node={node} />
|
|
431
|
+
))}
|
|
432
|
+
</TreeView>
|
|
433
|
+
</div>
|
|
434
|
+
);
|
|
435
|
+
```
|
|
436
|
+
|
|
@@ -135,6 +135,9 @@ import { Button } from "../Button";
|
|
|
135
135
|
- Имена классов — **camelCase**: `styles.secondaryLight`, `styles.iconOnly`.
|
|
136
136
|
- Никаких **inline styles** в компонентах (в stories допустимо для лейаута примеров).
|
|
137
137
|
- **Цвета** — только через CSS-переменные токенов (`var(--triplex-next-...)`). Никаких hex/rgb.
|
|
138
|
+
В стилях это правильная запись: CSS-переменные — внутренний слой токенов. А в документации
|
|
139
|
+
(`*-ai.md`, тексты Storybook) токен указывается путём `{Группа}.{Токен}` — см.
|
|
140
|
+
`docs/ai/CONTEXT.md` → «Дизайн-токены».
|
|
138
141
|
- Runtime CSS-переменные, которые компонент задаёт сам (например через `style` для динамической высоты,
|
|
139
142
|
transform или координат), называй с префиксом `--triplex-next-runtime-...`.
|
|
140
143
|
Они не являются дизайн-токенами и не должны попадать в `src/components/DesignTokens` или generated-файлы.
|
package/bundle/guides/commits.md
CHANGED
|
@@ -56,11 +56,14 @@ TRI-21-alert-ai-ready
|
|
|
56
56
|
`docs/human/release.md`. Прямой push в `release-0` проверяется CI:
|
|
57
57
|
`ci.yml` триггерится на `push: branches: [release-0]`.
|
|
58
58
|
|
|
59
|
-
**Единственное
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
**Единственное исключение** — релизный коммит (бамп версии + release notes):
|
|
60
|
+
он тоже попадает в `release-0` напрямую, без PR. В еженедельном релизе его
|
|
61
|
+
делает [`release-weekly.yml`](../../.github/workflows/release-weekly.yml),
|
|
62
|
+
при ручном выпуске — skill `release-react17`. Прогон CI на этот коммит
|
|
63
|
+
стартует, но дождаться его нельзя: релиз создаётся сразу после push. Поэтому
|
|
64
|
+
исключение компенсируется гейтами перед пушем — `npm run test-unit`
|
|
65
|
+
и `npm run build` (последний в `prebuild` гоняет `tsc -p tsconfig.build.json`;
|
|
66
|
+
полный `tsc` гейтом быть не может — он даёт давние ошибки в `stories/`).
|
|
64
67
|
|
|
65
68
|
---
|
|
66
69
|
|
|
@@ -86,6 +89,24 @@ TRI-21 Alert: AI refactoring + unit-тесты
|
|
|
86
89
|
- Тело коммита опционально (для сложных изменений).
|
|
87
90
|
- Язык: русский или английский — не смешивать в одном коммите.
|
|
88
91
|
|
|
92
|
+
**Исключение — релизные коммиты еженедельного релиза.** Их делает
|
|
93
|
+
[`release-weekly.yml`](../../.github/workflows/release-weekly.yml) в GitHub
|
|
94
|
+
Actions, и они идут **без префикса задачи**:
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
Подготовка релиза 0.44.0
|
|
98
|
+
Подготовка релиза 1.44.0
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Причина техническая: у Actions нет доступа к Linear, номер задачи взять
|
|
102
|
+
неоткуда, а выдумывать его нельзя. Связь с трекером даёт skill `release-auto`
|
|
103
|
+
— он заводит задачу «Релиз 1.Y.0 / 0.Y.0» по итогам прогона и складывает
|
|
104
|
+
туда ссылки на релизы. Коммит машинный, его авторство видно по автору
|
|
105
|
+
(`dudim-ai`), поэтому потеря префикса не мешает разбору истории.
|
|
106
|
+
|
|
107
|
+
При **ручном** выпуске (`/release`) префикс обязателен, как обычно: там
|
|
108
|
+
задача Linear заводится до начала работы.
|
|
109
|
+
|
|
89
110
|
---
|
|
90
111
|
|
|
91
112
|
## Когда запускать линтер
|
|
@@ -140,7 +161,8 @@ npx prettier --write src/components/Button/Button.tsx
|
|
|
140
161
|
|
|
141
162
|
1. делает коммит по конвенциям этого файла (skill `commit-component`);
|
|
142
163
|
2. пушит ветку;
|
|
143
|
-
3.
|
|
164
|
+
3. дожидается PR от workflow `auto-pr.yml` (см. «PR-воркфлоу»; сам
|
|
165
|
+
`gh pr create` в ветках `TRI-*` не вызывает);
|
|
144
166
|
4. если менялись stories или визуал компонентов — обновляет baseline-скриншоты
|
|
145
167
|
через skill `update-visual-baselines` (CI-workflow «Update Visual
|
|
146
168
|
Snapshots» + чистка orphan-скриншотов);
|
|
@@ -161,9 +183,9 @@ npx prettier --write src/components/Button/Button.tsx
|
|
|
161
183
|
|
|
162
184
|
## GitHub-операции от аккаунта бота
|
|
163
185
|
|
|
164
|
-
Автоматические GitHub-операции (
|
|
165
|
-
|
|
166
|
-
|
|
186
|
+
Автоматические GitHub-операции (комментарии в PR, запуск workflow,
|
|
187
|
+
авторство коммитов автофинала) выполняются от отдельного machine-аккаунта,
|
|
188
|
+
если у разработчика задан его токен:
|
|
167
189
|
|
|
168
190
|
```bash
|
|
169
191
|
# fine-grained PAT бота: contents:write, pull-requests:write, actions:write
|
|
@@ -171,11 +193,18 @@ export TRIPLEX_BOT_GH_TOKEN=... # в ~/.zshenv (не ~/.zshrc — агент
|
|
|
171
193
|
# в неинтерактивном shell), НЕ в репозитории
|
|
172
194
|
```
|
|
173
195
|
|
|
196
|
+
Тот же PAT хранится как repository secret `DUDIM_AI_GH_TOKEN` — его
|
|
197
|
+
использует workflow `auto-pr.yml`, который создаёт PR для веток `TRI-*`
|
|
198
|
+
(см. «PR-воркфлоу»). Благодаря этому автор PR один и тот же независимо от
|
|
199
|
+
того, где шла сессия агента — локально или в облаке. При ротации PAT
|
|
200
|
+
обнови оба места: `~/.zshenv` и secret
|
|
201
|
+
(`gh secret set DUDIM_AI_GH_TOKEN --body "$TRIPLEX_BOT_GH_TOKEN"`).
|
|
202
|
+
|
|
174
203
|
**Паттерн для агентов** — в начале Bash-вызова с командами `gh`:
|
|
175
204
|
|
|
176
205
|
```bash
|
|
177
206
|
if [ -n "$TRIPLEX_BOT_GH_TOKEN" ]; then export GH_TOKEN="$TRIPLEX_BOT_GH_TOKEN"; fi
|
|
178
|
-
gh pr
|
|
207
|
+
gh pr comment ... # выполнится от бота; без токена — от разработчика (fallback)
|
|
179
208
|
```
|
|
180
209
|
|
|
181
210
|
**Авторство коммитов автофинала** (identity бота выводится из токена,
|
|
@@ -217,32 +246,83 @@ fi
|
|
|
217
246
|
permitted for this session type / through this proxy» (у fine-grained
|
|
218
247
|
PAT было бы «…by personal access token»).
|
|
219
248
|
|
|
220
|
-
Что
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
`
|
|
230
|
-
`
|
|
249
|
+
Что доступно из облака — проверено зондами TRI-117 (2026-08-14) и TRI-119
|
|
250
|
+
(2026-08-21):
|
|
251
|
+
|
|
252
|
+
| Операция | Из облака |
|
|
253
|
+
|---|---|
|
|
254
|
+
| `git push` ветки, включая защищённый `main` | ✅ |
|
|
255
|
+
| Создание и мерж PR, комментарии | ✅ |
|
|
256
|
+
| Чтение REST и Actions | ✅ |
|
|
257
|
+
| Диспатч нерелизных workflow (`visual-update.yml`) | ✅ |
|
|
258
|
+
| `git push` тега | ❌ `RPC failed; HTTP 403` |
|
|
259
|
+
| `POST /git/refs`, `PATCH /git/refs/heads/*` | ❌ |
|
|
260
|
+
| `gh release create` | ❌ |
|
|
261
|
+
| `gh workflow run release.yml`, `repository_dispatch` | ❌ |
|
|
262
|
+
| Удаление веток и тегов | ❌ |
|
|
263
|
+
|
|
264
|
+
Отказы приходят с текстом `Write access to this GitHub API path is not
|
|
265
|
+
permitted through this proxy` — это ограничение прокси, а не прав токена
|
|
266
|
+
и не настроек репозитория. Менять branch protection бесполезно.
|
|
267
|
+
|
|
268
|
+
**Следствие для релиза:** выпустить его из облачной сессии нельзя ни одним
|
|
269
|
+
маршрутом. Поэтому механика релиза живёт в GitHub Actions
|
|
270
|
+
(`.github/workflows/release-weekly.yml`), расписание держит сам GitHub, а
|
|
271
|
+
за облачной routine остаётся отчёт — см.
|
|
272
|
+
`.agents/skills/release-auto/SKILL.md`.
|
|
231
273
|
|
|
232
274
|
---
|
|
233
275
|
|
|
234
276
|
## PR-воркфлоу
|
|
235
277
|
|
|
236
|
-
**Работа по задаче Linear:**
|
|
278
|
+
**Работа по задаче Linear:** PR для веток `TRI-*` создаёт workflow
|
|
279
|
+
`auto-pr.yml` (триггер `on: push`) от аккаунта бота — secret
|
|
280
|
+
`DUDIM_AI_GH_TOKEN`. Так автор PR не зависит от среды сессии: локальный
|
|
281
|
+
`gh pr create` шёл бы от PAT бота, а облачный — от GitHub App `claude`
|
|
282
|
+
(прокси подменяет авторизацию, см. «Облачные сессии»). Агент **не вызывает
|
|
283
|
+
`gh pr create` в ветках `TRI-*`** — иначе облачная сессия успеет создать
|
|
284
|
+
PR от `claude[bot]` раньше workflow.
|
|
285
|
+
|
|
286
|
+
Финальный коммит — это и есть источник PR: заголовок берётся из первой
|
|
287
|
+
строки (`TRI-XXX Краткое описание`), описание — из тела коммита. Поэтому
|
|
288
|
+
в автофинале тело коммита обязательно и содержит описание PR
|
|
289
|
+
(«Что сделано» / «Как проверить»).
|
|
237
290
|
|
|
238
291
|
```bash
|
|
239
292
|
git push -u origin TRI-XXX-feature-name
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
--
|
|
293
|
+
# PR создаст auto-pr.yml через ~20–60 с; дождись его появления:
|
|
294
|
+
for i in $(seq 1 12); do
|
|
295
|
+
pr_url=$(gh pr list --head "TRI-XXX-feature-name" --base main --state open \
|
|
296
|
+
--json url --jq '.[0].url // empty') && [ -n "$pr_url" ] && break
|
|
297
|
+
sleep 10
|
|
298
|
+
done
|
|
299
|
+
echo "${pr_url:-PR не появился}"
|
|
243
300
|
```
|
|
244
301
|
|
|
245
|
-
|
|
302
|
+
Если PR не появился за ~2 минуты — проверь прогон workflow именно для
|
|
303
|
+
текущего коммита (не по ветке: `--limit 1` по ветке может показать прогон
|
|
304
|
+
предыдущего пуша):
|
|
305
|
+
`gh run list --workflow=auto-pr.yml --commit $(git rev-parse HEAD) --limit 1`.
|
|
306
|
+
Дальше по статусу:
|
|
307
|
+
|
|
308
|
+
- `queued` / `in_progress` — прогон ещё идёт, продолжай ждать;
|
|
309
|
+
- `completed` + `success` без PR — сработал guard (маркер `[no-pr]`,
|
|
310
|
+
subject не по формату `TRI-XXX ...` или ветка без новых коммитов
|
|
311
|
+
относительно `main`) — штатное завершение без PR, fallback не нужен;
|
|
312
|
+
- `completed` + `failure` или прогона нет (например, протух secret) —
|
|
313
|
+
fallback.
|
|
314
|
+
Fallback — прежняя схема: `gh pr create` с паттерном bot-токена из
|
|
315
|
+
раздела выше.
|
|
316
|
+
Fallback безопасен: раз workflow PR не создал, гонки нет. В облачной
|
|
317
|
+
сессии fallback-PR будет от `claude[bot]` (прокси подменит PAT — см.
|
|
318
|
+
«Облачные сессии») — это осознанная деградация, лучше PR от app'а, чем
|
|
319
|
+
отсутствие PR.
|
|
320
|
+
|
|
321
|
+
Обход автосоздания (WIP-пуш ветки `TRI-*` без PR): добавь маркер `[no-pr]`
|
|
322
|
+
в первую строку HEAD-коммита. Коммиты не по формату `TRI-XXX ...`
|
|
323
|
+
(например, `chore: ...`) PR также не создают.
|
|
324
|
+
|
|
325
|
+
После появления PR финал продолжается автоматически: skill
|
|
246
326
|
`update-visual-baselines` (если менялись stories/визуал) и skill
|
|
247
327
|
`finish-task` — комментарий-резюме в задаче Linear и проверка линковки PR
|
|
248
328
|
(GitHub-интеграция Linear подхватит PR по `TRI-XXX` в имени ветки).
|