@cloud-ru/ds-list 1.0.0

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.
Files changed (439) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +201 -0
  3. package/README.md +1413 -0
  4. package/dist/cjs/components/Items/AccordionItem/AccordionItem.d.ts +4 -0
  5. package/dist/cjs/components/Items/AccordionItem/AccordionItem.js +37 -0
  6. package/dist/cjs/components/Items/AccordionItem/index.d.ts +1 -0
  7. package/dist/cjs/components/Items/AccordionItem/index.js +17 -0
  8. package/dist/cjs/components/Items/BaseItem/BaseItem.d.ts +24 -0
  9. package/dist/cjs/components/Items/BaseItem/BaseItem.js +118 -0
  10. package/dist/cjs/components/Items/BaseItem/constants.d.ts +3 -0
  11. package/dist/cjs/components/Items/BaseItem/constants.js +8 -0
  12. package/dist/cjs/components/Items/BaseItem/index.d.ts +1 -0
  13. package/dist/cjs/components/Items/BaseItem/index.js +17 -0
  14. package/dist/cjs/components/Items/BaseItem/styles.module.css +312 -0
  15. package/dist/cjs/components/Items/GroupSelectItem/GroupSelectItem.d.ts +4 -0
  16. package/dist/cjs/components/Items/GroupSelectItem/GroupSelectItem.js +22 -0
  17. package/dist/cjs/components/Items/GroupSelectItem/index.d.ts +1 -0
  18. package/dist/cjs/components/Items/GroupSelectItem/index.js +17 -0
  19. package/dist/cjs/components/Items/NextListItem/NextListItem.d.ts +2 -0
  20. package/dist/cjs/components/Items/NextListItem/NextListItem.js +71 -0
  21. package/dist/cjs/components/Items/NextListItem/constants.d.ts +2 -0
  22. package/dist/cjs/components/Items/NextListItem/constants.js +13 -0
  23. package/dist/cjs/components/Items/NextListItem/index.d.ts +1 -0
  24. package/dist/cjs/components/Items/NextListItem/index.js +17 -0
  25. package/dist/cjs/components/Items/NextListItem/types.d.ts +4 -0
  26. package/dist/cjs/components/Items/NextListItem/types.js +2 -0
  27. package/dist/cjs/components/Items/PinGroupItem/PinGroupItem.d.ts +7 -0
  28. package/dist/cjs/components/Items/PinGroupItem/PinGroupItem.js +27 -0
  29. package/dist/cjs/components/Items/PinGroupItem/index.d.ts +1 -0
  30. package/dist/cjs/components/Items/PinGroupItem/index.js +17 -0
  31. package/dist/cjs/components/Items/PinGroupItem/styles.module.css +12 -0
  32. package/dist/cjs/components/Items/SearchItem/SearchItem.d.ts +17 -0
  33. package/dist/cjs/components/Items/SearchItem/SearchItem.js +28 -0
  34. package/dist/cjs/components/Items/SearchItem/index.d.ts +1 -0
  35. package/dist/cjs/components/Items/SearchItem/index.js +17 -0
  36. package/dist/cjs/components/Items/SearchItem/styles.module.css +13 -0
  37. package/dist/cjs/components/Items/SimpleItem/components/DragHandle/DragHandle.d.ts +7 -0
  38. package/dist/cjs/components/Items/SimpleItem/components/DragHandle/DragHandle.js +20 -0
  39. package/dist/cjs/components/Items/SimpleItem/components/DragHandle/index.d.ts +1 -0
  40. package/dist/cjs/components/Items/SimpleItem/components/DragHandle/index.js +17 -0
  41. package/dist/cjs/components/Items/SimpleItem/components/OverlayDragHandle/OverlayDragHandle.d.ts +2 -0
  42. package/dist/cjs/components/Items/SimpleItem/components/OverlayDragHandle/OverlayDragHandle.js +9 -0
  43. package/dist/cjs/components/Items/SimpleItem/components/OverlayDragHandle/index.d.ts +1 -0
  44. package/dist/cjs/components/Items/SimpleItem/components/OverlayDragHandle/index.js +17 -0
  45. package/dist/cjs/components/Items/SimpleItem/components/SimpleGroupBlock/SimpleGroupBlock.d.ts +19 -0
  46. package/dist/cjs/components/Items/SimpleItem/components/SimpleGroupBlock/SimpleGroupBlock.js +52 -0
  47. package/dist/cjs/components/Items/SimpleItem/components/SimpleGroupBlock/index.d.ts +1 -0
  48. package/dist/cjs/components/Items/SimpleItem/components/SimpleGroupBlock/index.js +17 -0
  49. package/dist/cjs/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/SimpleGroupBlockOverlay.d.ts +15 -0
  50. package/dist/cjs/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/SimpleGroupBlockOverlay.js +19 -0
  51. package/dist/cjs/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/index.d.ts +1 -0
  52. package/dist/cjs/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/index.js +17 -0
  53. package/dist/cjs/components/Items/SimpleItem/components/SimpleItem/SimpleItem.d.ts +11 -0
  54. package/dist/cjs/components/Items/SimpleItem/components/SimpleItem/SimpleItem.js +48 -0
  55. package/dist/cjs/components/Items/SimpleItem/components/SimpleItem/index.d.ts +1 -0
  56. package/dist/cjs/components/Items/SimpleItem/components/SimpleItem/index.js +17 -0
  57. package/dist/cjs/components/Items/SimpleItem/components/SimpleItemOverlay/SimpleItemOverlay.d.ts +15 -0
  58. package/dist/cjs/components/Items/SimpleItem/components/SimpleItemOverlay/SimpleItemOverlay.js +19 -0
  59. package/dist/cjs/components/Items/SimpleItem/components/SimpleItemOverlay/index.d.ts +1 -0
  60. package/dist/cjs/components/Items/SimpleItem/components/SimpleItemOverlay/index.js +17 -0
  61. package/dist/cjs/components/Items/SimpleItem/components/index.d.ts +4 -0
  62. package/dist/cjs/components/Items/SimpleItem/components/index.js +20 -0
  63. package/dist/cjs/components/Items/SimpleItem/constants.d.ts +7 -0
  64. package/dist/cjs/components/Items/SimpleItem/constants.js +9 -0
  65. package/dist/cjs/components/Items/SimpleItem/index.d.ts +2 -0
  66. package/dist/cjs/components/Items/SimpleItem/index.js +18 -0
  67. package/dist/cjs/components/Items/SimpleItem/styles.module.css +96 -0
  68. package/dist/cjs/components/Items/SimpleItem/types.d.ts +3 -0
  69. package/dist/cjs/components/Items/SimpleItem/types.js +2 -0
  70. package/dist/cjs/components/Items/SimpleItem/utils.d.ts +6 -0
  71. package/dist/cjs/components/Items/SimpleItem/utils.js +11 -0
  72. package/dist/cjs/components/Items/hooks.d.ts +23 -0
  73. package/dist/cjs/components/Items/hooks.js +130 -0
  74. package/dist/cjs/components/Items/index.d.ts +5 -0
  75. package/dist/cjs/components/Items/index.js +21 -0
  76. package/dist/cjs/components/Items/styles.module.css +168 -0
  77. package/dist/cjs/components/Items/types.d.ts +168 -0
  78. package/dist/cjs/components/Items/types.js +2 -0
  79. package/dist/cjs/components/Items/utils.d.ts +52 -0
  80. package/dist/cjs/components/Items/utils.js +218 -0
  81. package/dist/cjs/components/Lists/Droplist/DropList.d.ts +21 -0
  82. package/dist/cjs/components/Lists/Droplist/DropList.js +36 -0
  83. package/dist/cjs/components/Lists/Droplist/constants.d.ts +8 -0
  84. package/dist/cjs/components/Lists/Droplist/constants.js +10 -0
  85. package/dist/cjs/components/Lists/Droplist/index.d.ts +1 -0
  86. package/dist/cjs/components/Lists/Droplist/index.js +17 -0
  87. package/dist/cjs/components/Lists/List/List.d.ts +61 -0
  88. package/dist/cjs/components/Lists/List/List.js +62 -0
  89. package/dist/cjs/components/Lists/List/index.d.ts +1 -0
  90. package/dist/cjs/components/Lists/List/index.js +17 -0
  91. package/dist/cjs/components/Lists/ListPrivate/ListPrivate.d.ts +18 -0
  92. package/dist/cjs/components/Lists/ListPrivate/ListPrivate.js +301 -0
  93. package/dist/cjs/components/Lists/ListPrivate/constants.d.ts +7 -0
  94. package/dist/cjs/components/Lists/ListPrivate/constants.js +19 -0
  95. package/dist/cjs/components/Lists/ListPrivate/index.d.ts +1 -0
  96. package/dist/cjs/components/Lists/ListPrivate/index.js +17 -0
  97. package/dist/cjs/components/Lists/ListPrivate/styles.module.css +80 -0
  98. package/dist/cjs/components/Lists/contexts/CollapseProvider.d.ts +17 -0
  99. package/dist/cjs/components/Lists/contexts/CollapseProvider.js +10 -0
  100. package/dist/cjs/components/Lists/contexts/FocusListProvider.d.ts +9 -0
  101. package/dist/cjs/components/Lists/contexts/FocusListProvider.js +9 -0
  102. package/dist/cjs/components/Lists/contexts/NewListProvider.d.ts +33 -0
  103. package/dist/cjs/components/Lists/contexts/NewListProvider.js +23 -0
  104. package/dist/cjs/components/Lists/contexts/OpenListProvider.d.ts +6 -0
  105. package/dist/cjs/components/Lists/contexts/OpenListProvider.js +10 -0
  106. package/dist/cjs/components/Lists/contexts/SelectionProvider.d.ts +74 -0
  107. package/dist/cjs/components/Lists/contexts/SelectionProvider.js +90 -0
  108. package/dist/cjs/components/Lists/contexts/index.d.ts +5 -0
  109. package/dist/cjs/components/Lists/contexts/index.js +21 -0
  110. package/dist/cjs/components/Lists/hooks.d.ts +83 -0
  111. package/dist/cjs/components/Lists/hooks.js +242 -0
  112. package/dist/cjs/components/Lists/index.d.ts +5 -0
  113. package/dist/cjs/components/Lists/index.js +21 -0
  114. package/dist/cjs/components/Lists/styles.module.css +65 -0
  115. package/dist/cjs/components/Lists/types.d.ts +200 -0
  116. package/dist/cjs/components/Lists/types.js +2 -0
  117. package/dist/cjs/components/Lists/utils.d.ts +8 -0
  118. package/dist/cjs/components/Lists/utils.js +31 -0
  119. package/dist/cjs/components/index.d.ts +2 -0
  120. package/dist/cjs/components/index.js +18 -0
  121. package/dist/cjs/constants.d.ts +48 -0
  122. package/dist/cjs/constants.js +51 -0
  123. package/dist/cjs/helperComponents/CollapseBlockPrivate/CollapseBlockPrivate.d.ts +9 -0
  124. package/dist/cjs/helperComponents/CollapseBlockPrivate/CollapseBlockPrivate.js +15 -0
  125. package/dist/cjs/helperComponents/CollapseBlockPrivate/index.d.ts +1 -0
  126. package/dist/cjs/helperComponents/CollapseBlockPrivate/index.js +17 -0
  127. package/dist/cjs/helperComponents/CollapseBlockPrivate/styles.module.css +27 -0
  128. package/dist/cjs/helperComponents/DesktopDroplist/DesktopDroplist.d.ts +7 -0
  129. package/dist/cjs/helperComponents/DesktopDroplist/DesktopDroplist.js +142 -0
  130. package/dist/cjs/helperComponents/DesktopDroplist/index.d.ts +1 -0
  131. package/dist/cjs/helperComponents/DesktopDroplist/index.js +17 -0
  132. package/dist/cjs/helperComponents/HiddenTabButton/HiddenTabButton.d.ts +7 -0
  133. package/dist/cjs/helperComponents/HiddenTabButton/HiddenTabButton.js +20 -0
  134. package/dist/cjs/helperComponents/HiddenTabButton/index.d.ts +1 -0
  135. package/dist/cjs/helperComponents/HiddenTabButton/index.js +17 -0
  136. package/dist/cjs/helperComponents/HiddenTabButton/styles.module.css +9 -0
  137. package/dist/cjs/helperComponents/ItemContent/ItemContent.d.ts +17 -0
  138. package/dist/cjs/helperComponents/ItemContent/ItemContent.js +27 -0
  139. package/dist/cjs/helperComponents/ItemContent/index.d.ts +1 -0
  140. package/dist/cjs/helperComponents/ItemContent/index.js +17 -0
  141. package/dist/cjs/helperComponents/ItemContent/styles.module.css +139 -0
  142. package/dist/cjs/helperComponents/ListEmptyState/ListEmptyState.d.ts +16 -0
  143. package/dist/cjs/helperComponents/ListEmptyState/ListEmptyState.js +25 -0
  144. package/dist/cjs/helperComponents/ListEmptyState/hooks.d.ts +10 -0
  145. package/dist/cjs/helperComponents/ListEmptyState/hooks.js +30 -0
  146. package/dist/cjs/helperComponents/ListEmptyState/index.d.ts +2 -0
  147. package/dist/cjs/helperComponents/ListEmptyState/index.js +20 -0
  148. package/dist/cjs/helperComponents/ListEmptyState/styles.module.css +8 -0
  149. package/dist/cjs/helperComponents/MobileDroplist/MobileDroplist.d.ts +9 -0
  150. package/dist/cjs/helperComponents/MobileDroplist/MobileDroplist.js +143 -0
  151. package/dist/cjs/helperComponents/MobileDroplist/MobileDroplist.module.css +16 -0
  152. package/dist/cjs/helperComponents/MobileDroplist/index.d.ts +1 -0
  153. package/dist/cjs/helperComponents/MobileDroplist/index.js +17 -0
  154. package/dist/cjs/helperComponents/MobileDroplist/utils.d.ts +11 -0
  155. package/dist/cjs/helperComponents/MobileDroplist/utils.js +65 -0
  156. package/dist/cjs/helperComponents/Separator/Separator.d.ts +20 -0
  157. package/dist/cjs/helperComponents/Separator/Separator.js +40 -0
  158. package/dist/cjs/helperComponents/Separator/constants.d.ts +3 -0
  159. package/dist/cjs/helperComponents/Separator/constants.js +10 -0
  160. package/dist/cjs/helperComponents/Separator/index.d.ts +1 -0
  161. package/dist/cjs/helperComponents/Separator/index.js +17 -0
  162. package/dist/cjs/helperComponents/Separator/styles.module.css +168 -0
  163. package/dist/cjs/helperComponents/index.d.ts +5 -0
  164. package/dist/cjs/helperComponents/index.js +21 -0
  165. package/dist/cjs/index.d.ts +9 -0
  166. package/dist/cjs/index.js +37 -0
  167. package/dist/cjs/locale/index.d.ts +79 -0
  168. package/dist/cjs/locale/index.js +44 -0
  169. package/dist/cjs/package.json +3 -0
  170. package/dist/cjs/style.css +1113 -0
  171. package/dist/cjs/types.d.ts +38 -0
  172. package/dist/cjs/types.js +2 -0
  173. package/dist/cjs/utils.d.ts +18 -0
  174. package/dist/cjs/utils.js +26 -0
  175. package/dist/esm/components/Items/AccordionItem/AccordionItem.d.ts +4 -0
  176. package/dist/esm/components/Items/AccordionItem/AccordionItem.js +34 -0
  177. package/dist/esm/components/Items/AccordionItem/index.d.ts +1 -0
  178. package/dist/esm/components/Items/AccordionItem/index.js +1 -0
  179. package/dist/esm/components/Items/BaseItem/BaseItem.d.ts +24 -0
  180. package/dist/esm/components/Items/BaseItem/BaseItem.js +112 -0
  181. package/dist/esm/components/Items/BaseItem/constants.d.ts +3 -0
  182. package/dist/esm/components/Items/BaseItem/constants.js +5 -0
  183. package/dist/esm/components/Items/BaseItem/index.d.ts +1 -0
  184. package/dist/esm/components/Items/BaseItem/index.js +1 -0
  185. package/dist/esm/components/Items/BaseItem/styles.module.css +312 -0
  186. package/dist/esm/components/Items/GroupSelectItem/GroupSelectItem.d.ts +4 -0
  187. package/dist/esm/components/Items/GroupSelectItem/GroupSelectItem.js +19 -0
  188. package/dist/esm/components/Items/GroupSelectItem/index.d.ts +1 -0
  189. package/dist/esm/components/Items/GroupSelectItem/index.js +1 -0
  190. package/dist/esm/components/Items/NextListItem/NextListItem.d.ts +2 -0
  191. package/dist/esm/components/Items/NextListItem/NextListItem.js +68 -0
  192. package/dist/esm/components/Items/NextListItem/constants.d.ts +2 -0
  193. package/dist/esm/components/Items/NextListItem/constants.js +10 -0
  194. package/dist/esm/components/Items/NextListItem/index.d.ts +1 -0
  195. package/dist/esm/components/Items/NextListItem/index.js +1 -0
  196. package/dist/esm/components/Items/NextListItem/types.d.ts +4 -0
  197. package/dist/esm/components/Items/NextListItem/types.js +1 -0
  198. package/dist/esm/components/Items/PinGroupItem/PinGroupItem.d.ts +7 -0
  199. package/dist/esm/components/Items/PinGroupItem/PinGroupItem.js +20 -0
  200. package/dist/esm/components/Items/PinGroupItem/index.d.ts +1 -0
  201. package/dist/esm/components/Items/PinGroupItem/index.js +1 -0
  202. package/dist/esm/components/Items/PinGroupItem/styles.module.css +12 -0
  203. package/dist/esm/components/Items/SearchItem/SearchItem.d.ts +17 -0
  204. package/dist/esm/components/Items/SearchItem/SearchItem.js +22 -0
  205. package/dist/esm/components/Items/SearchItem/index.d.ts +1 -0
  206. package/dist/esm/components/Items/SearchItem/index.js +1 -0
  207. package/dist/esm/components/Items/SearchItem/styles.module.css +13 -0
  208. package/dist/esm/components/Items/SimpleItem/components/DragHandle/DragHandle.d.ts +7 -0
  209. package/dist/esm/components/Items/SimpleItem/components/DragHandle/DragHandle.js +14 -0
  210. package/dist/esm/components/Items/SimpleItem/components/DragHandle/index.d.ts +1 -0
  211. package/dist/esm/components/Items/SimpleItem/components/DragHandle/index.js +1 -0
  212. package/dist/esm/components/Items/SimpleItem/components/OverlayDragHandle/OverlayDragHandle.d.ts +2 -0
  213. package/dist/esm/components/Items/SimpleItem/components/OverlayDragHandle/OverlayDragHandle.js +6 -0
  214. package/dist/esm/components/Items/SimpleItem/components/OverlayDragHandle/index.d.ts +1 -0
  215. package/dist/esm/components/Items/SimpleItem/components/OverlayDragHandle/index.js +1 -0
  216. package/dist/esm/components/Items/SimpleItem/components/SimpleGroupBlock/SimpleGroupBlock.d.ts +19 -0
  217. package/dist/esm/components/Items/SimpleItem/components/SimpleGroupBlock/SimpleGroupBlock.js +46 -0
  218. package/dist/esm/components/Items/SimpleItem/components/SimpleGroupBlock/index.d.ts +1 -0
  219. package/dist/esm/components/Items/SimpleItem/components/SimpleGroupBlock/index.js +1 -0
  220. package/dist/esm/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/SimpleGroupBlockOverlay.d.ts +15 -0
  221. package/dist/esm/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/SimpleGroupBlockOverlay.js +13 -0
  222. package/dist/esm/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/index.d.ts +1 -0
  223. package/dist/esm/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/index.js +1 -0
  224. package/dist/esm/components/Items/SimpleItem/components/SimpleItem/SimpleItem.d.ts +11 -0
  225. package/dist/esm/components/Items/SimpleItem/components/SimpleItem/SimpleItem.js +42 -0
  226. package/dist/esm/components/Items/SimpleItem/components/SimpleItem/index.d.ts +1 -0
  227. package/dist/esm/components/Items/SimpleItem/components/SimpleItem/index.js +1 -0
  228. package/dist/esm/components/Items/SimpleItem/components/SimpleItemOverlay/SimpleItemOverlay.d.ts +15 -0
  229. package/dist/esm/components/Items/SimpleItem/components/SimpleItemOverlay/SimpleItemOverlay.js +13 -0
  230. package/dist/esm/components/Items/SimpleItem/components/SimpleItemOverlay/index.d.ts +1 -0
  231. package/dist/esm/components/Items/SimpleItem/components/SimpleItemOverlay/index.js +1 -0
  232. package/dist/esm/components/Items/SimpleItem/components/index.d.ts +4 -0
  233. package/dist/esm/components/Items/SimpleItem/components/index.js +4 -0
  234. package/dist/esm/components/Items/SimpleItem/constants.d.ts +7 -0
  235. package/dist/esm/components/Items/SimpleItem/constants.js +6 -0
  236. package/dist/esm/components/Items/SimpleItem/index.d.ts +2 -0
  237. package/dist/esm/components/Items/SimpleItem/index.js +2 -0
  238. package/dist/esm/components/Items/SimpleItem/styles.module.css +96 -0
  239. package/dist/esm/components/Items/SimpleItem/types.d.ts +3 -0
  240. package/dist/esm/components/Items/SimpleItem/types.js +1 -0
  241. package/dist/esm/components/Items/SimpleItem/utils.d.ts +6 -0
  242. package/dist/esm/components/Items/SimpleItem/utils.js +8 -0
  243. package/dist/esm/components/Items/hooks.d.ts +23 -0
  244. package/dist/esm/components/Items/hooks.js +125 -0
  245. package/dist/esm/components/Items/index.d.ts +5 -0
  246. package/dist/esm/components/Items/index.js +5 -0
  247. package/dist/esm/components/Items/styles.module.css +168 -0
  248. package/dist/esm/components/Items/types.d.ts +168 -0
  249. package/dist/esm/components/Items/types.js +1 -0
  250. package/dist/esm/components/Items/utils.d.ts +52 -0
  251. package/dist/esm/components/Items/utils.js +203 -0
  252. package/dist/esm/components/Lists/Droplist/DropList.d.ts +21 -0
  253. package/dist/esm/components/Lists/Droplist/DropList.js +32 -0
  254. package/dist/esm/components/Lists/Droplist/constants.d.ts +8 -0
  255. package/dist/esm/components/Lists/Droplist/constants.js +7 -0
  256. package/dist/esm/components/Lists/Droplist/index.d.ts +1 -0
  257. package/dist/esm/components/Lists/Droplist/index.js +1 -0
  258. package/dist/esm/components/Lists/List/List.d.ts +61 -0
  259. package/dist/esm/components/Lists/List/List.js +56 -0
  260. package/dist/esm/components/Lists/List/index.d.ts +1 -0
  261. package/dist/esm/components/Lists/List/index.js +1 -0
  262. package/dist/esm/components/Lists/ListPrivate/ListPrivate.d.ts +18 -0
  263. package/dist/esm/components/Lists/ListPrivate/ListPrivate.js +295 -0
  264. package/dist/esm/components/Lists/ListPrivate/constants.d.ts +7 -0
  265. package/dist/esm/components/Lists/ListPrivate/constants.js +16 -0
  266. package/dist/esm/components/Lists/ListPrivate/index.d.ts +1 -0
  267. package/dist/esm/components/Lists/ListPrivate/index.js +1 -0
  268. package/dist/esm/components/Lists/ListPrivate/styles.module.css +80 -0
  269. package/dist/esm/components/Lists/contexts/CollapseProvider.d.ts +17 -0
  270. package/dist/esm/components/Lists/contexts/CollapseProvider.js +5 -0
  271. package/dist/esm/components/Lists/contexts/FocusListProvider.d.ts +9 -0
  272. package/dist/esm/components/Lists/contexts/FocusListProvider.js +5 -0
  273. package/dist/esm/components/Lists/contexts/NewListProvider.d.ts +33 -0
  274. package/dist/esm/components/Lists/contexts/NewListProvider.js +18 -0
  275. package/dist/esm/components/Lists/contexts/OpenListProvider.d.ts +6 -0
  276. package/dist/esm/components/Lists/contexts/OpenListProvider.js +6 -0
  277. package/dist/esm/components/Lists/contexts/SelectionProvider.d.ts +74 -0
  278. package/dist/esm/components/Lists/contexts/SelectionProvider.js +83 -0
  279. package/dist/esm/components/Lists/contexts/index.d.ts +5 -0
  280. package/dist/esm/components/Lists/contexts/index.js +5 -0
  281. package/dist/esm/components/Lists/hooks.d.ts +83 -0
  282. package/dist/esm/components/Lists/hooks.js +238 -0
  283. package/dist/esm/components/Lists/index.d.ts +5 -0
  284. package/dist/esm/components/Lists/index.js +5 -0
  285. package/dist/esm/components/Lists/styles.module.css +65 -0
  286. package/dist/esm/components/Lists/types.d.ts +200 -0
  287. package/dist/esm/components/Lists/types.js +1 -0
  288. package/dist/esm/components/Lists/utils.d.ts +8 -0
  289. package/dist/esm/components/Lists/utils.js +28 -0
  290. package/dist/esm/components/index.d.ts +2 -0
  291. package/dist/esm/components/index.js +2 -0
  292. package/dist/esm/constants.d.ts +48 -0
  293. package/dist/esm/constants.js +48 -0
  294. package/dist/esm/helperComponents/CollapseBlockPrivate/CollapseBlockPrivate.d.ts +9 -0
  295. package/dist/esm/helperComponents/CollapseBlockPrivate/CollapseBlockPrivate.js +9 -0
  296. package/dist/esm/helperComponents/CollapseBlockPrivate/index.d.ts +1 -0
  297. package/dist/esm/helperComponents/CollapseBlockPrivate/index.js +1 -0
  298. package/dist/esm/helperComponents/CollapseBlockPrivate/styles.module.css +27 -0
  299. package/dist/esm/helperComponents/DesktopDroplist/DesktopDroplist.d.ts +7 -0
  300. package/dist/esm/helperComponents/DesktopDroplist/DesktopDroplist.js +136 -0
  301. package/dist/esm/helperComponents/DesktopDroplist/index.d.ts +1 -0
  302. package/dist/esm/helperComponents/DesktopDroplist/index.js +1 -0
  303. package/dist/esm/helperComponents/HiddenTabButton/HiddenTabButton.d.ts +7 -0
  304. package/dist/esm/helperComponents/HiddenTabButton/HiddenTabButton.js +14 -0
  305. package/dist/esm/helperComponents/HiddenTabButton/index.d.ts +1 -0
  306. package/dist/esm/helperComponents/HiddenTabButton/index.js +1 -0
  307. package/dist/esm/helperComponents/HiddenTabButton/styles.module.css +9 -0
  308. package/dist/esm/helperComponents/ItemContent/ItemContent.d.ts +17 -0
  309. package/dist/esm/helperComponents/ItemContent/ItemContent.js +21 -0
  310. package/dist/esm/helperComponents/ItemContent/index.d.ts +1 -0
  311. package/dist/esm/helperComponents/ItemContent/index.js +1 -0
  312. package/dist/esm/helperComponents/ItemContent/styles.module.css +139 -0
  313. package/dist/esm/helperComponents/ListEmptyState/ListEmptyState.d.ts +16 -0
  314. package/dist/esm/helperComponents/ListEmptyState/ListEmptyState.js +19 -0
  315. package/dist/esm/helperComponents/ListEmptyState/hooks.d.ts +10 -0
  316. package/dist/esm/helperComponents/ListEmptyState/hooks.js +27 -0
  317. package/dist/esm/helperComponents/ListEmptyState/index.d.ts +2 -0
  318. package/dist/esm/helperComponents/ListEmptyState/index.js +2 -0
  319. package/dist/esm/helperComponents/ListEmptyState/styles.module.css +8 -0
  320. package/dist/esm/helperComponents/MobileDroplist/MobileDroplist.d.ts +9 -0
  321. package/dist/esm/helperComponents/MobileDroplist/MobileDroplist.js +137 -0
  322. package/dist/esm/helperComponents/MobileDroplist/MobileDroplist.module.css +16 -0
  323. package/dist/esm/helperComponents/MobileDroplist/index.d.ts +1 -0
  324. package/dist/esm/helperComponents/MobileDroplist/index.js +1 -0
  325. package/dist/esm/helperComponents/MobileDroplist/utils.d.ts +11 -0
  326. package/dist/esm/helperComponents/MobileDroplist/utils.js +61 -0
  327. package/dist/esm/helperComponents/Separator/Separator.d.ts +20 -0
  328. package/dist/esm/helperComponents/Separator/Separator.js +34 -0
  329. package/dist/esm/helperComponents/Separator/constants.d.ts +3 -0
  330. package/dist/esm/helperComponents/Separator/constants.js +7 -0
  331. package/dist/esm/helperComponents/Separator/index.d.ts +1 -0
  332. package/dist/esm/helperComponents/Separator/index.js +1 -0
  333. package/dist/esm/helperComponents/Separator/styles.module.css +168 -0
  334. package/dist/esm/helperComponents/index.d.ts +5 -0
  335. package/dist/esm/helperComponents/index.js +5 -0
  336. package/dist/esm/index.d.ts +9 -0
  337. package/dist/esm/index.js +14 -0
  338. package/dist/esm/locale/index.d.ts +79 -0
  339. package/dist/esm/locale/index.js +41 -0
  340. package/dist/esm/style.css +1113 -0
  341. package/dist/esm/types.d.ts +38 -0
  342. package/dist/esm/types.js +1 -0
  343. package/dist/esm/utils.d.ts +18 -0
  344. package/dist/esm/utils.js +19 -0
  345. package/dist/tsconfig.cjs.tsbuildinfo +1 -0
  346. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  347. package/package.json +70 -0
  348. package/src/_iconSizes.scss +7 -0
  349. package/src/components/Items/AccordionItem/AccordionItem.tsx +79 -0
  350. package/src/components/Items/AccordionItem/index.ts +1 -0
  351. package/src/components/Items/BaseItem/BaseItem.tsx +274 -0
  352. package/src/components/Items/BaseItem/constants.ts +9 -0
  353. package/src/components/Items/BaseItem/index.ts +1 -0
  354. package/src/components/Items/BaseItem/styles.module.scss +330 -0
  355. package/src/components/Items/GroupSelectItem/GroupSelectItem.tsx +48 -0
  356. package/src/components/Items/GroupSelectItem/index.ts +1 -0
  357. package/src/components/Items/NextListItem/NextListItem.tsx +157 -0
  358. package/src/components/Items/NextListItem/constants.ts +12 -0
  359. package/src/components/Items/NextListItem/index.ts +1 -0
  360. package/src/components/Items/NextListItem/types.ts +3 -0
  361. package/src/components/Items/PinGroupItem/PinGroupItem.tsx +38 -0
  362. package/src/components/Items/PinGroupItem/index.ts +1 -0
  363. package/src/components/Items/PinGroupItem/styles.module.scss +14 -0
  364. package/src/components/Items/SearchItem/SearchItem.tsx +55 -0
  365. package/src/components/Items/SearchItem/index.ts +1 -0
  366. package/src/components/Items/SearchItem/styles.module.scss +26 -0
  367. package/src/components/Items/SimpleItem/components/DragHandle/DragHandle.tsx +27 -0
  368. package/src/components/Items/SimpleItem/components/DragHandle/index.ts +1 -0
  369. package/src/components/Items/SimpleItem/components/OverlayDragHandle/OverlayDragHandle.tsx +6 -0
  370. package/src/components/Items/SimpleItem/components/OverlayDragHandle/index.ts +1 -0
  371. package/src/components/Items/SimpleItem/components/SimpleGroupBlock/SimpleGroupBlock.tsx +87 -0
  372. package/src/components/Items/SimpleItem/components/SimpleGroupBlock/index.ts +1 -0
  373. package/src/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/SimpleGroupBlockOverlay.tsx +33 -0
  374. package/src/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/index.ts +1 -0
  375. package/src/components/Items/SimpleItem/components/SimpleItem/SimpleItem.tsx +70 -0
  376. package/src/components/Items/SimpleItem/components/SimpleItem/index.ts +1 -0
  377. package/src/components/Items/SimpleItem/components/SimpleItemOverlay/SimpleItemOverlay.tsx +26 -0
  378. package/src/components/Items/SimpleItem/components/SimpleItemOverlay/index.ts +1 -0
  379. package/src/components/Items/SimpleItem/components/index.ts +4 -0
  380. package/src/components/Items/SimpleItem/constants.ts +8 -0
  381. package/src/components/Items/SimpleItem/index.ts +2 -0
  382. package/src/components/Items/SimpleItem/styles.module.scss +149 -0
  383. package/src/components/Items/SimpleItem/types.ts +7 -0
  384. package/src/components/Items/SimpleItem/utils.ts +8 -0
  385. package/src/components/Items/hooks.tsx +233 -0
  386. package/src/components/Items/index.ts +5 -0
  387. package/src/components/Items/styles.module.scss +107 -0
  388. package/src/components/Items/types.ts +203 -0
  389. package/src/components/Items/utils.ts +302 -0
  390. package/src/components/Lists/Droplist/DropList.tsx +62 -0
  391. package/src/components/Lists/Droplist/constants.ts +11 -0
  392. package/src/components/Lists/Droplist/index.ts +1 -0
  393. package/src/components/Lists/List/List.tsx +155 -0
  394. package/src/components/Lists/List/index.ts +1 -0
  395. package/src/components/Lists/ListPrivate/ListPrivate.tsx +560 -0
  396. package/src/components/Lists/ListPrivate/constants.ts +22 -0
  397. package/src/components/Lists/ListPrivate/index.ts +1 -0
  398. package/src/components/Lists/ListPrivate/styles.module.scss +90 -0
  399. package/src/components/Lists/contexts/CollapseProvider.tsx +22 -0
  400. package/src/components/Lists/contexts/FocusListProvider.tsx +15 -0
  401. package/src/components/Lists/contexts/NewListProvider.tsx +76 -0
  402. package/src/components/Lists/contexts/OpenListProvider.tsx +13 -0
  403. package/src/components/Lists/contexts/SelectionProvider.tsx +214 -0
  404. package/src/components/Lists/contexts/index.ts +5 -0
  405. package/src/components/Lists/hooks.ts +351 -0
  406. package/src/components/Lists/index.ts +5 -0
  407. package/src/components/Lists/styles.module.scss +95 -0
  408. package/src/components/Lists/types.ts +231 -0
  409. package/src/components/Lists/utils.ts +32 -0
  410. package/src/components/index.ts +2 -0
  411. package/src/constants.ts +53 -0
  412. package/src/helperComponents/CollapseBlockPrivate/CollapseBlockPrivate.tsx +32 -0
  413. package/src/helperComponents/CollapseBlockPrivate/index.ts +1 -0
  414. package/src/helperComponents/CollapseBlockPrivate/styles.module.scss +32 -0
  415. package/src/helperComponents/DesktopDroplist/DesktopDroplist.tsx +313 -0
  416. package/src/helperComponents/DesktopDroplist/index.ts +1 -0
  417. package/src/helperComponents/HiddenTabButton/HiddenTabButton.tsx +35 -0
  418. package/src/helperComponents/HiddenTabButton/index.ts +1 -0
  419. package/src/helperComponents/HiddenTabButton/styles.module.scss +11 -0
  420. package/src/helperComponents/ItemContent/ItemContent.tsx +80 -0
  421. package/src/helperComponents/ItemContent/index.ts +1 -0
  422. package/src/helperComponents/ItemContent/styles.module.scss +100 -0
  423. package/src/helperComponents/ListEmptyState/ListEmptyState.tsx +52 -0
  424. package/src/helperComponents/ListEmptyState/hooks.ts +42 -0
  425. package/src/helperComponents/ListEmptyState/index.ts +2 -0
  426. package/src/helperComponents/ListEmptyState/styles.module.scss +11 -0
  427. package/src/helperComponents/MobileDroplist/MobileDroplist.module.scss +25 -0
  428. package/src/helperComponents/MobileDroplist/MobileDroplist.tsx +230 -0
  429. package/src/helperComponents/MobileDroplist/index.ts +1 -0
  430. package/src/helperComponents/MobileDroplist/utils.tsx +76 -0
  431. package/src/helperComponents/Separator/Separator.tsx +115 -0
  432. package/src/helperComponents/Separator/constants.ts +11 -0
  433. package/src/helperComponents/Separator/index.ts +1 -0
  434. package/src/helperComponents/Separator/styles.module.scss +255 -0
  435. package/src/helperComponents/index.ts +5 -0
  436. package/src/index.ts +19 -0
  437. package/src/locale/index.ts +45 -0
  438. package/src/types.ts +44 -0
  439. package/src/utils.ts +26 -0
package/README.md ADDED
@@ -0,0 +1,1413 @@
1
+ # List
2
+
3
+ `@cloud-ru/ds-list` — Списочный UI — компонент List для плоских/вложенных списков с выбором, группами и поиском и Droplist — тот же список в поповере.
4
+
5
+ Пакет `@cloud-ru/ds-list` собирает списочный UI дизайн-системы: плоские и вложенные списки, списки с выбором (single / multiple), группы с раскрытием, поиск, закреплённые элементы, виртуализацию и Droplist — тот же список в popover.
6
+
7
+ ## Когда использовать
8
+
9
+ | Задача | Как решить |
10
+ |--------|------------|
11
+ | Навигация/меню/settings sidebar | `List` с `items` |
12
+ | Выбор из коллекции (радио-группа/чекбоксы) | `List` + `selection={{ mode: 'single' \| 'multiple', ... }}` |
13
+ | Выпадашка-селектор у кнопки/поля | `Droplist` с `children`-триггером |
14
+ | Группы с раскрытием (inbox/starred/folders) | `items` типа `{ type: 'collapse', items: [...] }` + `collapse` |
15
+ | Длинный список (1k+) | `virtualized` на `List` |
16
+ | Закреплённые действия сверху/снизу | `pinTop` / `pinBottom` |
17
+
18
+ Когда **не** нужен `List`:
19
+
20
+ - Простой набор из 2–4 кнопок — используйте **`Button`** + layout.
21
+ - Табличные данные с сортировкой/фильтрацией — используйте `Table`.
22
+ - Многошаговая форма — используйте `Stepper`.
23
+
24
+ ## Установка
25
+
26
+ ```bash
27
+ pnpm add @cloud-ru/ds-list
28
+ ```
29
+
30
+ ```ts
31
+ import { List, Droplist, ItemContent } from '@cloud-ru/ds-list'
32
+ import '@cloud-ru/ds-list/style.css'
33
+ ```
34
+
35
+ ## Figma
36
+
37
+ Все три компонента живут в одном Figma-файле «Состояния для list / tab / toggles». Ссылки на конкретные узлы — на страницах компонентов.
38
+ ## Состав пакета
39
+
40
+ - ****List**** — основной компонент. Принимает `items` (+ `pinTop` / `pinBottom` / `footer`), управляет выбором через `selection`, раскрытием групп через `collapse`, поиском через `search`. Поддерживает виртуализацию для 1000+ элементов.
41
+ - ****Droplist**** — тот же список в popover. Оборачивает `children`-триггер и открывает список рядом с ним. Передаёт почти все пропсы `List`.
42
+ - ****ReorderableList**** — список с drag&drop-переупорядочиванием строк через `@dnd-kit` (плюс `ReorderableDroplist` — то же в поповере).
43
+ - ****ItemContent**** — каноничная разметка содержимого item: `label` (заголовок), `caption` (мета справа), `description` (подпись снизу). Используется как значение `item.content`.
44
+
45
+ ## Общие принципы
46
+
47
+ - **Contract первый, стайлинг второй.** Элементы описываются как данные (`items: Item[]`), а не как JSX. Это даёт стабильную клавиатурную навигацию, selection и поиск «из коробки».
48
+ - **ItemContent — единый слот контента.** Разметку внутри элемента задаёт не потребитель, а `ItemContent` — чтобы заголовок / caption / description выравнивались одинаково во всех пакетах.
49
+ - **Controlled/uncontrolled симметричны.** У `selection`, `collapse` и `search` одинаковая форма: `defaultValue` / `value` + `onChange`. Выбирайте по тому, где должен жить state.
50
+ - **Виртуализация — осознанный выбор.** Включайте `virtualized` только при 1k+ элементов. На коротких списках виртуализация ломает layout-assumptions (динамическая высота, focus-into-view).
51
+
52
+ ## List
53
+
54
+ Основной компонент списка. Управляет items, выбором, раскрытием групп, поиском, виртуализацией и закреплёнными элементами.
55
+
56
+ Плоский или вложенный список элементов с клавиатурной навигацией, выбором, раскрытием групп и поиском. Элементы задаются декларативно через `items`, `pinTop`, `pinBottom` — компонент сам строит DOM, управляет фокусом и эмитит изменения выбора.
57
+
58
+ ### Когда использовать
59
+
60
+ - Навигация (sidebar, меню аккаунта, breadcrumbs popover).
61
+ - Селектор-значение (часовой пояс, валюта, язык) — чаще всего вместе с **Droplist**.
62
+ - Список объектов с действиями: письма, файлы, задачи.
63
+ - Иерархия с раскрытием (folders, workspace → projects → tasks).
64
+ - Длинные списки (1k+) через `virtualized`.
65
+
66
+ Когда **не** нужен:
67
+
68
+ - Таблицы с колонками — `Table`.
69
+ - Короткий набор действий (≤ 4) — кнопки.
70
+
71
+ ### Анатомия
72
+
73
+ #### Size (default `s`)
74
+
75
+ - `s` — компактный, дефолт. Droplist'ы, плотные sidebar'ы, настройки.
76
+ - `m` — средний. Основные списки объектов (письма, задачи).
77
+ - `l` — крупный. Mobile, списки с описаниями, главные навигационные меню.
78
+
79
+ #### Selection (default off)
80
+
81
+ - Без `selection` — клик = `onClick` элемента, состояния «выбран» нет. Подходит для навигации.
82
+ - `selection={{ mode: 'single' }}` — один выбранный элемент, `value: ItemId`. Селектор значения (валюта, регион).
83
+ - `selection={{ mode: 'multiple' }}` — множественный выбор, `value: ItemId[]`. Фильтры, настройки, права доступа.
84
+
85
+ Visual signal: marker (вертикальная полоса) + фоновая заливка. `marker` можно отключить (`marker={false}`), если родительский layout уже подсвечивает активный элемент.
86
+
87
+ #### Switch-презентация выбора
88
+
89
+ У `BaseItem` есть проп `switch`. При `switch: true` элемент рисует на правом краю `Switch` вместо чекбокса/маркера, а `isChecked` управляет положением тумблера. Применяется в списках настроек, где «выбор» — это включение функции (уведомления, доступы, интеграции), а не пометка элемента.
90
+
91
+ - `switch` работает вместе с `selection` (`mode: 'multiple'` или `'single'`) — состояние тумблера берётся из выбора.
92
+ - При `switch: true` маркер и заливка не рисуются: единственный носитель состояния — сам тумблер.
93
+ - `showSwitchIcon` — флаг иконки внутри тумблера (на момент Phase 5 не передаётся в `@cloud-ru/ds-toggles`, зарезервирован).
94
+
95
+ #### Group variant (default `subtitleTertiary`)
96
+
97
+ Тип `group` / `group-select` принимает `groupVariant` — оформление заголовка группы:
98
+
99
+ - `subtitle` — заголовок-подзаголовок: контрастный текст, акцент на названии секции.
100
+ - `subtitleTertiary` — приглушённый третичный текст, для второстепенных группировок.
101
+
102
+ Дополнительно `divider: true` рисует разделитель над группой.
103
+
104
+ #### Item shape
105
+
106
+ Один элемент = одна строка данных. Слоты:
107
+
108
+ - `label` — заголовок (обязателен).
109
+ - `caption` — мета справа (счётчик, дата, badge).
110
+ - `description` — подпись под заголовком (1–2 строки).
111
+ - `beforeContent` — иконка / аватар слева.
112
+ - `afterContent` — слот справа. Иконка рендерится в квадрате размера иконки; произвольный контент (счётчик, badge, tag) — в натуральном размере, без обрезки до иконочного квадрата.
113
+
114
+ Truncate управляется через `truncate` на `ItemContent` — см. страницу **ItemContent**.
115
+
116
+ #### States
117
+
118
+ - `loading` — показывает skeleton вместо items.
119
+ - `dataFiltered` — флаг, что текущий пустой массив — результат фильтра/поиска. Компонент покажет `noResultsState`.
120
+ - `dataError` — ошибка загрузки, рендерится `errorDataState`.
121
+ - `noDataState` / `noResultsState` / `errorDataState` — конфигурация empty-state (`description`, `icon`, `footer`).
122
+
123
+ ### Примеры использования
124
+
125
+ #### Базовый плоский список
126
+
127
+ items — массив объектов с id и content.
128
+
129
+ ```tsx
130
+ import { List } from '@cloud-ru/ds-list';
131
+
132
+ import styles from './styles.module.scss';
133
+
134
+ export function BasicList() {
135
+ return (
136
+ <div className={styles.box}>
137
+ <List
138
+ size='s'
139
+ items={[
140
+ { id: 'inbox', content: { label: 'Входящие', caption: '12' } },
141
+ { id: 'sent', content: { label: 'Отправленные' } },
142
+ { id: 'archive', content: { label: 'Архив', caption: '238' } },
143
+ { id: 'trash', content: { label: 'Корзина', description: 'Удаляется через 30 дней' } },
144
+ ]}
145
+ />
146
+ </div>
147
+ );
148
+ }
149
+ ```
150
+
151
+ #### Single selection
152
+
153
+ selection={{ mode: 'single', value, onChange }} — контролируемый выбор одного элемента.
154
+
155
+ ```tsx
156
+ import { List } from '@cloud-ru/ds-list';
157
+ import { useState } from 'react';
158
+
159
+ import styles from './styles.module.scss';
160
+
161
+ export function SingleSelection() {
162
+ const [value, setValue] = useState<string | number | undefined>('usd');
163
+
164
+ return (
165
+ <div className={styles.box}>
166
+ <List
167
+ size='s'
168
+ marker
169
+ selection={{ mode: 'single', value, onChange: setValue }}
170
+ items={[
171
+ { id: 'usd', content: { label: 'Доллар США', caption: 'USD' } },
172
+ { id: 'eur', content: { label: 'Евро', caption: 'EUR' } },
173
+ { id: 'rub', content: { label: 'Российский рубль', caption: 'RUB' } },
174
+ { id: 'cny', content: { label: 'Китайский юань', caption: 'CNY' } },
175
+ ]}
176
+ />
177
+ </div>
178
+ );
179
+ }
180
+ ```
181
+
182
+ #### Multiple selection
183
+
184
+ В режиме multiple value — массив ItemId.
185
+
186
+ ```tsx
187
+ import { List } from '@cloud-ru/ds-list';
188
+ import { useState } from 'react';
189
+
190
+ import styles from './styles.module.scss';
191
+
192
+ export function MultipleSelection() {
193
+ const [value, setValue] = useState<(string | number)[]>(['email', 'push']);
194
+
195
+ return (
196
+ <div className={styles.box}>
197
+ <List
198
+ size='s'
199
+ selection={{ mode: 'multiple', value, onChange: setValue }}
200
+ items={[
201
+ { id: 'email', content: { label: 'Email' } },
202
+ { id: 'push', content: { label: 'Push-уведомления' } },
203
+ { id: 'sms', content: { label: 'SMS' } },
204
+ { id: 'telegram', content: { label: 'Telegram', description: 'Требует привязки аккаунта' } },
205
+ ]}
206
+ />
207
+ </div>
208
+ );
209
+ }
210
+ ```
211
+
212
+ #### Группы с раскрытием
213
+
214
+ type: 'collapse' + collapse.defaultValue. Подходит для folders / workspace / sidebar-групп.
215
+
216
+ ```tsx
217
+ import { List } from '@cloud-ru/ds-list';
218
+
219
+ import styles from './styles.module.scss';
220
+
221
+ export function GroupsCollapsible() {
222
+ return (
223
+ <div className={styles.box}>
224
+ <List
225
+ size='s'
226
+ collapse={{ defaultValue: ['projects'] }}
227
+ items={[
228
+ {
229
+ type: 'collapse',
230
+ id: 'projects',
231
+ content: { label: 'Проекты' },
232
+ items: [
233
+ { id: 'p-web', content: { label: 'Web-портал' } },
234
+ { id: 'p-mobile', content: { label: 'Mobile' } },
235
+ { id: 'p-admin', content: { label: 'Admin' } },
236
+ ],
237
+ },
238
+ {
239
+ type: 'collapse',
240
+ id: 'settings',
241
+ content: { label: 'Настройки' },
242
+ items: [
243
+ { id: 's-profile', content: { label: 'Профиль' } },
244
+ { id: 's-team', content: { label: 'Команда' } },
245
+ { id: 's-billing', content: { label: 'Оплата' } },
246
+ ],
247
+ },
248
+ ]}
249
+ />
250
+ </div>
251
+ );
252
+ }
253
+ ```
254
+
255
+ #### Три уровня вложенности
256
+
257
+ Группы внутри групп — подходит для каталогов, файловых деревьев.
258
+
259
+ ```tsx
260
+ import { List } from '@cloud-ru/ds-list';
261
+
262
+ import styles from './styles.module.scss';
263
+
264
+ export function NestedLevels() {
265
+ return (
266
+ <div className={styles.box}>
267
+ <List
268
+ size='s'
269
+ collapse={{ defaultValue: ['catalog', 'catalog/books'] }}
270
+ items={[
271
+ {
272
+ type: 'collapse',
273
+ id: 'catalog',
274
+ content: { label: 'Каталог' },
275
+ items: [
276
+ {
277
+ type: 'collapse',
278
+ id: 'catalog/books',
279
+ content: { label: 'Книги' },
280
+ items: [
281
+ { id: 'catalog/books/fiction', content: { label: 'Художественные' } },
282
+ { id: 'catalog/books/science', content: { label: 'Научные' } },
283
+ ],
284
+ },
285
+ {
286
+ type: 'collapse',
287
+ id: 'catalog/music',
288
+ content: { label: 'Музыка' },
289
+ items: [
290
+ { id: 'catalog/music/rock', content: { label: 'Рок' } },
291
+ { id: 'catalog/music/jazz', content: { label: 'Джаз' } },
292
+ ],
293
+ },
294
+ ],
295
+ },
296
+ ]}
297
+ />
298
+ </div>
299
+ );
300
+ }
301
+ ```
302
+
303
+ #### Поиск внутри списка
304
+
305
+ search={{ value, onChange }} — встроенная строка поиска. Фильтрация items — на стороне потребителя.
306
+
307
+ ```tsx
308
+ import { List } from '@cloud-ru/ds-list';
309
+ import { useMemo, useState } from 'react';
310
+
311
+ import styles from './styles.module.scss';
312
+
313
+ const ALL = [
314
+ 'Антон',
315
+ 'Борис',
316
+ 'Виктор',
317
+ 'Галина',
318
+ 'Дарья',
319
+ 'Евгений',
320
+ 'Жанна',
321
+ 'Зоя',
322
+ 'Игорь',
323
+ 'Ксения',
324
+ 'Лев',
325
+ 'Мария',
326
+ ];
327
+
328
+ export function WithSearch() {
329
+ const [query, setQuery] = useState('');
330
+
331
+ const items = useMemo(
332
+ () =>
333
+ ALL.filter(name => name.toLowerCase().includes(query.toLowerCase())).map(name => ({
334
+ id: name,
335
+ content: { label: name },
336
+ })),
337
+ [query],
338
+ );
339
+
340
+ return (
341
+ <div className={styles.box}>
342
+ <List size='s' search={{ value: query, onChange: setQuery, placeholder: 'Поиск по имени' }} items={items} />
343
+ </div>
344
+ );
345
+ }
346
+ ```
347
+
348
+ #### Закреплённые элементы
349
+
350
+ pinTop / pinBottom — списки, которые не участвуют в scroll основного массива.
351
+
352
+ ```tsx
353
+ import { List } from '@cloud-ru/ds-list';
354
+
355
+ import styles from './styles.module.scss';
356
+
357
+ export function PinnedItems() {
358
+ return (
359
+ <div className={styles.box}>
360
+ <List
361
+ size='s'
362
+ pinTop={[
363
+ { id: 'all', content: { label: 'Все задачи', caption: '128' } },
364
+ { id: 'mine', content: { label: 'Мои задачи', caption: '14' } },
365
+ ]}
366
+ items={[
367
+ { id: 'design', content: { label: 'Дизайн' } },
368
+ { id: 'backend', content: { label: 'Бэкенд' } },
369
+ { id: 'frontend', content: { label: 'Фронтенд' } },
370
+ { id: 'qa', content: { label: 'QA' } },
371
+ ]}
372
+ pinBottom={[{ id: 'archive', content: { label: 'Архив', description: 'Завершённые задачи' } }]}
373
+ />
374
+ </div>
375
+ );
376
+ }
377
+ ```
378
+
379
+ #### Виртуализация
380
+
381
+ virtualized — рендерит только видимые строки. Включайте при 1000+ элементов.
382
+
383
+ ```tsx
384
+ import { Button } from '@cloud-ru/ds-button';
385
+ import { List } from '@cloud-ru/ds-list';
386
+ import { useMemo, useState } from 'react';
387
+
388
+ import styles from './styles.module.scss';
389
+
390
+ const TOTAL = 10_000;
391
+
392
+ export function Virtualized() {
393
+ const items = useMemo(
394
+ () =>
395
+ Array.from({ length: TOTAL }, (_, i) => ({
396
+ id: `row-${i}`,
397
+ content: { label: `Запись #${i + 1}`, caption: i % 5 === 0 ? 'new' : undefined },
398
+ })),
399
+ [],
400
+ );
401
+
402
+ const [value, setValue] = useState<string | number | undefined>('row-0');
403
+
404
+ return (
405
+ <div className={styles.wrapper}>
406
+ <Button
407
+ size='s'
408
+ appearance='neutral'
409
+ view='outline'
410
+ label='Прыгнуть к случайной записи'
411
+ onClick={() => setValue(`row-${Math.floor(Math.random() * TOTAL)}`)}
412
+ />
413
+ <List
414
+ size='s'
415
+ virtualized
416
+ scroll
417
+ limitedScrollHeight
418
+ scrollToSelectedItem
419
+ selection={{ mode: 'single', value, onChange: setValue }}
420
+ items={items}
421
+ />
422
+ </div>
423
+ );
424
+ }
425
+ ```
426
+
427
+ #### Empty state
428
+
429
+ dataFiltered + noResultsState — контролируемое сообщение при пустом результате поиска.
430
+
431
+ ```tsx
432
+ import { List } from '@cloud-ru/ds-list';
433
+ import { useState } from 'react';
434
+
435
+ import styles from './styles.module.scss';
436
+
437
+ export function EmptyState() {
438
+ const [query, setQuery] = useState('qwerty');
439
+
440
+ const items = query.length > 3 ? [] : [{ id: 'a', content: { label: 'Пример результата' } }];
441
+
442
+ return (
443
+ <div className={styles.box}>
444
+ <List
445
+ size='s'
446
+ search={{ value: query, onChange: setQuery, placeholder: 'Поиск' }}
447
+ items={items}
448
+ dataFiltered
449
+ noResultsState={{
450
+ content: 'Ничего не нашли. Проверьте раскладку или измените запрос.',
451
+ }}
452
+ />
453
+ </div>
454
+ );
455
+ }
456
+ ```
457
+
458
+ #### Обёртка айтема и неактивный элемент
459
+
460
+ itemWrapRender оборачивает item в Tooltip; inactive выводит элемент из навигации и selection, оставляя его видимым.
461
+
462
+ ```tsx
463
+ import { List } from '@cloud-ru/ds-list';
464
+ import { ReactNode } from 'react';
465
+
466
+ import styles from './styles.module.scss';
467
+
468
+ const asExternalLink = (href: string) =>
469
+ function (item: ReactNode) {
470
+ return (
471
+ <a href={href} target='_blank' rel='noreferrer' className={styles.link}>
472
+ {item}
473
+ </a>
474
+ );
475
+ };
476
+
477
+ export function ListItemWrap() {
478
+ return (
479
+ <div className={styles.box}>
480
+ <List
481
+ size='s'
482
+ items={[
483
+ {
484
+ id: 'prod',
485
+ content: { label: 'Production', caption: 'open' },
486
+ itemWrapRender: asExternalLink('https://console.cloud.ru/prod'),
487
+ },
488
+ {
489
+ id: 'staging',
490
+ content: { label: 'Staging', caption: 'open' },
491
+ itemWrapRender: asExternalLink('https://console.cloud.ru/staging'),
492
+ },
493
+ {
494
+ id: 'legacy',
495
+ content: { label: 'Legacy', description: 'Среда выведена из эксплуатации — недоступна для перехода' },
496
+ inactive: true,
497
+ },
498
+ ]}
499
+ />
500
+ </div>
501
+ );
502
+ }
503
+ ```
504
+
505
+ ### Props
506
+
507
+ | Prop | Type | Default | Description |
508
+ |------|------|---------|-------------|
509
+ | `barHideStrategy` | `"leave"` \| `"move"` \| `"never"` \| `"scroll"` | — | Управление скрытием скролл баров: <br/> <br> - `Never` - показывать всегда <br/> <br> - `Leave` - скрывать когда курсор покидает компонент <br/> <br> - `Scroll` - показывать только когда происходит скроллинг <br/> <br> - `Move` - показывать при движении курсора над компонентом |
510
+ | `className` | `string` | — | CSS-класс |
511
+ | `collapse` | `CollapseState` | `{}` | Настройки раскрытия элементов |
512
+ | `contentRender` | `((props: ContentRenderProps) => ReactNode)` | — | Рендер функция основного контента айтема |
513
+ | `data-test-id` | `string` | — | |
514
+ | `dataError` | `boolean` | — | Загрузка данных завершилась ошибкой: показывается `errorDataState` |
515
+ | `dataFiltered` | `boolean` | — | Текущий пустой список — результат поиска/фильтра: показывается `noResultsState` вместо `noDataState` |
516
+ | `errorDataState` | `EmptyStateProps` | — | Экран при ошибке запроса |
517
+ | `footer` | `ReactNode ;` | — | Кастомизируемый элемент в конце списка |
518
+ | `footerActiveElementsRefs` | `RefObject<HTMLElement>[]` | — | Список ссылок на кастомные элементы, помещенные в специальную секцию внизу списка |
519
+ | `footerDivider` | `boolean` | — | Показывать divider между body и footer (Figma `dropdownContainer.dividerWrapper` снизу) |
520
+ | `hasListInFocusChain` | `boolean` | `true` | Флаг, отвечающий за включение самого родительского контейнера листа в цепочку фокусирующихся элементов |
521
+ | `header` | `ReactNode ;` | — | Кастомизируемый элемент в начале списка — Figma `dropdownContainer.topBar`. <br/> Подходит для заголовка / справочного блока над поиском. |
522
+ | `headerDivider` | `boolean` | — | Показывать divider между header и body (Figma `dropdownContainer.dividerWrapper` сверху) |
523
+ | `items` | `Item[]` | `[]` | Основные элементы списка |
524
+ | `keyboardNavigationRef` | `RefObject<{ focusItem(id: ItemId): void; }>` | — | Ссылка на управление навигацией листа с клавиатуры |
525
+ | `limitedScrollHeight` | `boolean` | — | Ограничить максимальную высоту скролл-контейнера в зависимости от `size` |
526
+ | `loading` | `boolean` | — | Флаг, отвечающий за состояние загрузки списка |
527
+ | `marker` | `boolean` | `true` | Отображать ли маркер у выбранного элемента списка |
528
+ | `noDataState` | `EmptyStateProps` | — | Экран при отсутствии данных |
529
+ | `noResultsState` | `EmptyStateProps` | — | Экран при отсутствии результатов поиска или фильтров |
530
+ | `onKeyDown` | `((e: KeyboardEvent<HTMLElement>) => void)` | — | Обработчик события по нажатию клавиш |
531
+ | `onScroll` | `((event?: Event) => void)` | — | Колбек на скролл прокручиваемого списка |
532
+ | `pinBottom` | `Item[]` | `[]` | Элементы списка, закрепленные снизу |
533
+ | `pinTop` | `Item[]` | `[]` | Элементы списка, закрепленные сверху |
534
+ | `scroll` | `boolean` | — | Включить ли скролл для основной части списка |
535
+ | `scrollContainerClassName` | `string` | — | CSS-класс для scroll обертки основного списка айтемов |
536
+ | `scrollContainerRef` | `Ref<HTMLElement>` | — | Ссылка на контейнер, который скроллится |
537
+ | `scrollRef` | `Ref<HTMLElement>` | — | Ссылка на элемент, обозначающий самый конец прокручиваемого списка |
538
+ | `scrollToSelectedItem` | `boolean` | — | Флаг, отвечающий за прокручивание до выбранного элемента |
539
+ | `search` | `SearchState` | — | Настройки поисковой строки |
540
+ | `selection` | `SelectionMultipleState \| SelectionSingleState` | — | Настройки выбора элементов. `mode: 'single'` — один выбранный элемент (`value: ItemId`), <br/> `mode: 'multiple'` — множественный выбор (`value: ItemId[]`). Без `selection` выбора нет — <br/> клик вызывает только `onClick` элемента. |
541
+ | `size` | `"l"` \| `"m"` \| `"s"` | `m` | Размер списка |
542
+ | `tabIndex` | `number` | `0` | `tabIndex` корневого элемента списка (для управления порядком фокуса) |
543
+ | `untouchableScrollbars` | `boolean` | — | Отключает возможность взаимодействовать со скролбарами мышью. |
544
+ | `virtualized` | `boolean` | — | Включить виртуализацию элементов списка. Рекомендуется при количестве элементов от 1000. |
545
+
546
+ ### Смотри также
547
+
548
+ - **Droplist** — этот список в popover.
549
+ - **ReorderableList** — тот же список, но с drag&drop-переупорядочиванием строк.
550
+ - **ItemContent** — каноничная разметка item.
551
+ ### Item types
552
+
553
+ - `BaseItem` — обычный элемент с `content` / `beforeContent` / `afterContent`; опционально `switch: true` для тумблер-презентации выбора.
554
+ - `{ type: 'collapse', items }` — группа, раскрывается кликом по заголовку; управление — через `collapse`.
555
+ - `{ type: 'next-list', items, placement }` — раскрытие в соседний popover (для каскадных меню).
556
+ - `{ type: 'group', items, groupVariant }` / `{ type: 'group-select', items, groupVariant }` — визуальная группа с label, `groupVariant` (`subtitle` / `subtitleTertiary`), опциональным `divider` и «выбрать всё» (`group-select`).
557
+
558
+ Типы экспортируются из пакета: `BaseItemProps`, `AccordionItemProps`, `NextListItemProps`, `GroupItemProps`, `GroupSelectItemProps`. Дискриминаторы — `isBaseItemProps`, `isAccordionItemProps` и пр.
559
+
560
+ ### Controlled vs uncontrolled
561
+
562
+ `selection`, `collapse` и `search` принимают одинаковую форму:
563
+
564
+ - **Uncontrolled** — передавайте `defaultValue`. Компонент хранит state сам, пригодно для форм/настроек, где значения потом читаются через `onChange`.
565
+ - **Controlled** — `value` + `onChange`. Нужен, когда state живёт в URL / query / внешнем сторе, или когда требуется программно менять выбор/раскрытие.
566
+
567
+ ### Виртуализация
568
+
569
+ - Включайте `virtualized` при размере `items` от ~1000 элементов.
570
+ - `scrollToSelectedItem` работает и с `virtualized`: список прокручивается до выбранной строки при открытии.
571
+ - Внутри `Droplist` виртуализация работает так же — проп `virtualized` доступен и там.
572
+
573
+ ### Доступность
574
+
575
+ - Контейнер имеет роль `menu`, элементы — `menuitem`. Это согласовано с использованием списка внутри dropdown/menu-сценариев.
576
+ - Клавиатура: `ArrowDown` / `ArrowUp` переходят между элементами (без wrap-around, пропуская `disabled`), `ArrowRight` раскрывает группу или next-list, `ArrowLeft` сворачивает, `Tab` уводит фокус с item на корневой контейнер и далее из списка. `Enter` / `Space` на сфокусированном элементе выбирают его.
577
+ - Первый `ArrowDown` от корневого контейнера переводит фокус на первый элемент; дальше навигация идёт по элементам.
578
+ - Marker и фоновая заливка выбранного элемента — два независимых носителя смысла, состояние понятно без цвета.
579
+ - Disabled-элементы не попадают в цепочку табуляции и не реагируют на клавиатуру.
580
+
581
+ ## Droplist
582
+
583
+ List в popover. Оборачивает триггер (кнопку / поле) и показывает список рядом с ним. Принимает почти все пропсы List.
584
+
585
+ Тот же `List` в popover. `Droplist` оборачивает `children`-триггер, сам управляет открытием/закрытием и показывает список рядом с триггером. Почти все пропсы `List` (`items`, `selection`, `collapse`, `search`, `virtualized`, `pinTop` / `pinBottom`) доступны здесь напрямую.
586
+
587
+ ### Когда использовать
588
+
589
+ - Селектор-значение у кнопки/поля (валюта, язык, регион, сортировка).
590
+ - Меню действий (у toolbar-кнопки, у строки таблицы).
591
+ - Вторичная навигация, которую не хочется держать на странице постоянно.
592
+
593
+ Когда **не** нужен Droplist:
594
+
595
+ - Простое меню из 2–3 действий — проще держать inline.
596
+ - Список должен быть виден всегда (sidebar) — используйте **List**.
597
+ - Сложная форма с несколькими полями — используйте **Popover** + свой layout.
598
+
599
+ ### Анатомия
600
+
601
+ #### Size (default `s`)
602
+
603
+ Размер задаёт высоту строки списка (Figma `listItem`): `s` = 40px, `m` = 52px, `l` = 66px. Совпадает с `size` у **List**.
604
+
605
+ - `s` — дефолт. Компактные селекторы у кнопок toolbar/header.
606
+ - `m` — списки объектов с описанием.
607
+ - `l` — крупные меню, mobile.
608
+
609
+ #### Selection mode (default off)
610
+
611
+ Режим выбора идентичен **List**:
612
+
613
+ - без `selection` — клик = навигация/действие, состояния «выбран» нет.
614
+ - `selection={{ mode: 'single' }}` — один выбранный элемент (`value: ItemId`); обычно с `closeDroplistOnItemClick`.
615
+ - `selection={{ mode: 'multiple' }}` — множественный выбор (`value: ItemId[]`); popover не закрывается на клик.
616
+
617
+ #### Placement (default `bottom-start`)
618
+
619
+ - `bottom-start` — дефолт. Для списков у кнопки в header/toolbar, анкорится по левому краю триггера.
620
+ - `bottom-end` — если триггер прижат к правому краю контейнера.
621
+ - `top-*` — намеренно ставьте только когда триггер физически у нижнего края страницы. У нижнего края viewport fallback отработает автоматически.
622
+
623
+ #### Trigger event (default `click`)
624
+
625
+ - `click` — дефолт. Предсказуемо, работает на touch, доступнее для клавиатуры.
626
+ - `hover` — допустимо только для навигационных меню без чувствительных действий. Не используйте для селекторов и действий с последствиями.
627
+
628
+ #### Width strategy (default `auto`)
629
+
630
+ - `widthStrategy='auto'` — дефолт. Ширина popover'а по контенту.
631
+ - `widthStrategy='eq'` — popover ровно по ширине триггера. Идеально для селектов в форме.
632
+ - `widthStrategy='gte'` — popover не уже триггера, но может быть шире по контенту.
633
+
634
+ #### Header / Footer
635
+
636
+ Шапка и подвал popover'а — кастомные слоты вокруг тела списка (Figma `dropdownContainer.topBar` / `bottomBar`):
637
+
638
+ - `header` — `ReactNode` над списком (и над полем поиска, если оно есть). Заголовок раздела, справочный блок.
639
+ - `headerDivider` — рисует разделитель между `header` (вместе с полем поиска) и телом списка.
640
+ - `footer` — `ReactNode` под списком. Сводка, ссылка на полный список.
641
+ - `footerDivider` — рисует разделитель между телом списка и `footer`.
642
+
643
+ Разделители включаются только вместе с соответствующим слотом: `headerDivider` без `header` ничего не рисует.
644
+
645
+ #### Close after selection
646
+
647
+ - В `single` + навигация → `closeDroplistOnItemClick` обычно `true`.
648
+ - В `multiple` → всегда `false` (по умолчанию), иначе пользователь не сможет проставить несколько галочек.
649
+
650
+ ### Примеры использования
651
+
652
+ #### Селектор-кнопка
653
+
654
+ Кнопка-триггер + Droplist с single selection и закрытием после выбора.
655
+
656
+ ```tsx
657
+ import { Button } from '@cloud-ru/ds-button';
658
+ import { Droplist } from '@cloud-ru/ds-list';
659
+ import { useState } from 'react';
660
+
661
+ import styles from './styles.module.scss';
662
+
663
+ export function BasicDroplist() {
664
+ const [value, setValue] = useState<string | number | undefined>('rub');
665
+
666
+ return (
667
+ <div className={styles.wrapper}>
668
+ <Droplist
669
+ trigger='click'
670
+ placement='bottom-start'
671
+ closeDroplistOnItemClick
672
+ selection={{ mode: 'single', value, onChange: setValue }}
673
+ items={[
674
+ { id: 'usd', content: { label: 'USD — Доллар США' } },
675
+ { id: 'eur', content: { label: 'EUR — Евро' } },
676
+ { id: 'rub', content: { label: 'RUB — Российский рубль' } },
677
+ { id: 'cny', content: { label: 'CNY — Китайский юань' } },
678
+ ]}
679
+ >
680
+ <Button size='s' appearance='neutral' view='outline' label={`Валюта: ${String(value).toUpperCase()}`} />
681
+ </Droplist>
682
+ </div>
683
+ );
684
+ }
685
+ ```
686
+
687
+ #### Multiple selection
688
+
689
+ Несколько отметок без закрытия. closeDroplistOnItemClick по умолчанию false.
690
+
691
+ ```tsx
692
+ import { Button } from '@cloud-ru/ds-button';
693
+ import { Droplist } from '@cloud-ru/ds-list';
694
+ import { useState } from 'react';
695
+
696
+ import styles from './styles.module.scss';
697
+
698
+ export function DroplistMultiple() {
699
+ const [value, setValue] = useState<(string | number)[]>(['email']);
700
+
701
+ return (
702
+ <div className={styles.wrapper}>
703
+ <Droplist
704
+ trigger='click'
705
+ placement='bottom-start'
706
+ selection={{ mode: 'multiple', value, onChange: setValue }}
707
+ items={[
708
+ { id: 'email', content: { label: 'Email' } },
709
+ { id: 'push', content: { label: 'Push-уведомления' } },
710
+ { id: 'sms', content: { label: 'SMS' } },
711
+ { id: 'telegram', content: { label: 'Telegram' } },
712
+ ]}
713
+ >
714
+ <Button size='s' appearance='neutral' view='outline' label={`Каналы: ${value.length}`} />
715
+ </Droplist>
716
+ </div>
717
+ );
718
+ }
719
+ ```
720
+
721
+ #### Поиск внутри Droplist
722
+
723
+ search + фильтрация items на стороне потребителя — поведение идентично List.
724
+
725
+ ```tsx
726
+ import { Button } from '@cloud-ru/ds-button';
727
+ import { Droplist } from '@cloud-ru/ds-list';
728
+ import { useMemo, useState } from 'react';
729
+
730
+ import styles from './styles.module.scss';
731
+
732
+ const COUNTRIES = [
733
+ 'Австрия',
734
+ 'Армения',
735
+ 'Беларусь',
736
+ 'Бразилия',
737
+ 'Германия',
738
+ 'Грузия',
739
+ 'Индия',
740
+ 'Казахстан',
741
+ 'Китай',
742
+ 'Россия',
743
+ 'США',
744
+ 'Турция',
745
+ ];
746
+
747
+ export function DroplistWithSearch() {
748
+ const [value, setValue] = useState<string | number | undefined>('Россия');
749
+ const [query, setQuery] = useState('');
750
+
751
+ const items = useMemo(
752
+ () =>
753
+ COUNTRIES.filter(name => name.toLowerCase().includes(query.toLowerCase())).map(name => ({
754
+ id: name,
755
+ content: { label: name },
756
+ })),
757
+ [query],
758
+ );
759
+
760
+ return (
761
+ <div className={styles.wrapper}>
762
+ <Droplist
763
+ trigger='click'
764
+ placement='bottom-start'
765
+ closeDroplistOnItemClick
766
+ selection={{ mode: 'single', value, onChange: setValue }}
767
+ search={{ value: query, onChange: setQuery, placeholder: 'Поиск страны' }}
768
+ items={items}
769
+ >
770
+ <Button size='s' appearance='neutral' view='outline' label={`Страна: ${value}`} />
771
+ </Droplist>
772
+ </div>
773
+ );
774
+ }
775
+ ```
776
+
777
+ #### Form select (widthStrategy="eq")
778
+
779
+ Popover ровно по ширине триггера — поведение нативного select.
780
+
781
+ ```tsx
782
+ import { Button } from '@cloud-ru/ds-button';
783
+ import { Droplist } from '@cloud-ru/ds-list';
784
+ import { useState } from 'react';
785
+
786
+ import styles from './styles.module.scss';
787
+
788
+ export function DroplistAsFormSelect() {
789
+ const [value, setValue] = useState<string | number | undefined>('m');
790
+
791
+ const options = [
792
+ { id: 's', content: { label: 'Small (1 vCPU, 2 GB RAM)' } },
793
+ { id: 'm', content: { label: 'Medium (2 vCPU, 4 GB RAM)' } },
794
+ { id: 'l', content: { label: 'Large (4 vCPU, 8 GB RAM)' } },
795
+ { id: 'xl', content: { label: 'X-Large (8 vCPU, 16 GB RAM)' } },
796
+ ];
797
+ const label = options.find(o => o.id === value)?.content.label ?? 'Выбрать';
798
+
799
+ return (
800
+ <div className={styles.formSelect}>
801
+ <Droplist
802
+ trigger='click'
803
+ placement='bottom-start'
804
+ closeDroplistOnItemClick
805
+ widthStrategy='eq'
806
+ selection={{ mode: 'single', value, onChange: setValue }}
807
+ items={options}
808
+ >
809
+ <Button size='s' appearance='neutral' view='outline' label={label} fullWidth />
810
+ </Droplist>
811
+ </div>
812
+ );
813
+ }
814
+ ```
815
+
816
+ #### Шапка и подвал с разделителями
817
+
818
+ header / footer — слоты над и под списком; headerDivider / footerDivider рисуют разделители.
819
+
820
+ ```tsx
821
+ import { Button } from '@cloud-ru/ds-button';
822
+ import { Droplist } from '@cloud-ru/ds-list';
823
+ import { useState } from 'react';
824
+
825
+ import styles from './styles.module.scss';
826
+
827
+ export function DroplistWithHeader() {
828
+ const [value, setValue] = useState<string | number | undefined>('relevance');
829
+
830
+ return (
831
+ <div className={styles.wrapper}>
832
+ <Droplist
833
+ trigger='click'
834
+ placement='bottom-start'
835
+ closeDroplistOnItemClick
836
+ selection={{ mode: 'single', value, onChange: setValue }}
837
+ header='Сортировать по'
838
+ headerDivider
839
+ footer='4 варианта сортировки'
840
+ footerDivider
841
+ items={[
842
+ { id: 'relevance', content: { label: 'Релевантности' } },
843
+ { id: 'date', content: { label: 'Дате создания' } },
844
+ { id: 'name', content: { label: 'Имени' } },
845
+ { id: 'size', content: { label: 'Размеру' } },
846
+ ]}
847
+ >
848
+ <Button size='s' appearance='neutral' view='outline' label='Сортировка' />
849
+ </Droplist>
850
+ </div>
851
+ );
852
+ }
853
+ ```
854
+
855
+ ### Props
856
+
857
+ **DroplistProps**
858
+
859
+ | Prop | Type | Default | Description |
860
+ |------|------|---------|-------------|
861
+ | `actionButton` | `ReactNode` | — | Только mobile (`BottomSheet`): action-кнопка справа в шапке. |
862
+ | `barHideStrategy` | `"leave"` \| `"move"` \| `"never"` \| `"scroll"` | — | Управление скрытием скролл баров: <br/> <br> - `Never` - показывать всегда <br/> <br> - `Leave` - скрывать когда курсор покидает компонент <br/> <br> - `Scroll` - показывать только когда происходит скроллинг <br/> <br> - `Move` - показывать при движении курсора над компонентом |
863
+ | `children` | `ReactNode \| ({onKeyDown}) => ReactNode * Рендер функция принимает аргументы `onKeyDown` - хендлер ввода, для поддержки управления с клавиатуры` | — | Триггер для дроплиста |
864
+ | `className` | `string` | — | CSS-класс |
865
+ | `closeDroplistOnItemClick` | `boolean` | `false` | Закрывать выпадающий список после клика на базовый айтем. <br/> Работает в режимах selection: 'none' \| 'single' |
866
+ | `closeOnPopstate` | `boolean` | — | Закрывать ли поповер при переходе по истории браузера |
867
+ | `collapse` | `CollapseState` | — | Настройки раскрытия элементов |
868
+ | `container` | `RefObject<HTMLElement \| null>` | — | Контейнер портала (ref). Переопределяет `PortalContext` для этого дроплиста <br/> (по аналогии с `container` у Modal/Drawer). По умолчанию — из `PortalContextProvider`. |
869
+ | `contentRender` | `((props: ContentRenderProps) => ReactNode)` | — | Рендер функция основного контента айтема |
870
+ | `data-test-id` | `string` | — | |
871
+ | `dataError` | `boolean` | — | Загрузка данных завершилась ошибкой: показывается `errorDataState` |
872
+ | `dataFiltered` | `boolean` | — | Текущий пустой список — результат поиска/фильтра: показывается `noResultsState` вместо `noDataState` |
873
+ | `errorDataState` | `EmptyStateProps` | — | Экран при ошибке запроса |
874
+ | `footer` | `ReactNode ;` | — | Кастомизируемый элемент в конце списка |
875
+ | `footerActiveElementsRefs` | `RefObject<HTMLElement>[]` | — | Список ссылок на кастомные элементы, помещенные в специальную секцию внизу списка |
876
+ | `footerDivider` | `boolean` | — | Показывать divider между body и footer (Figma `dropdownContainer.dividerWrapper` снизу) |
877
+ | `header` | `ReactNode ;` | — | Кастомизируемый элемент в начале списка — Figma `dropdownContainer.topBar`. <br/> Подходит для заголовка / справочного блока над поиском. |
878
+ | `headerDivider` | `boolean` | — | Показывать divider между header и body (Figma `dropdownContainer.dividerWrapper` сверху) |
879
+ | `items` | `BaseItemWithoutNonGroup` \| `CommonGroupItem` \| `Item` \| `ScrollProps` | — | Основные элементы списка |
880
+ | `label` | `string` | — | Только mobile (`BottomSheet`): заголовок шапки. |
881
+ | `limitedScrollHeight` | `boolean` | — | Ограничить максимальную высоту скролл-контейнера в зависимости от `size` |
882
+ | `listRef` | `RefObject<HTMLElement>` | — | Ссылка на элемент выпадающего списка |
883
+ | `loading` | `boolean` | — | Флаг, отвечающий за состояние загрузки списка |
884
+ | `marker` | `boolean` | — | Отображать ли маркер у выбранного элемента списка |
885
+ | `noDataState` | `EmptyStateProps` | — | Экран при отсутствии данных |
886
+ | `noResultsState` | `EmptyStateProps` | — | Экран при отсутствии результатов поиска или фильтров |
887
+ | `onBackButtonClick` | `(() => void)` | — | Только mobile (`BottomSheet`): callback back-кнопки. |
888
+ | `onOpenChange` | `((isOpen: boolean) => void)` | — | Колбек отображения компонента. Срабатывает при изменении состояния open. |
889
+ | `onScroll` | `OriginalScrollProps` | — | Колбек на скролл прокручиваемого списка |
890
+ | `open` | `boolean` | — | Управляет состоянием показан/не показан. |
891
+ | `pinBottom` | `BaseItemWithoutNonGroup` \| `CommonGroupItem` \| `Item` \| `ScrollProps` | — | Элементы списка, закрепленные снизу |
892
+ | `pinTop` | `BaseItemWithoutNonGroup` \| `CommonGroupItem` \| `Item` \| `ScrollProps` | — | Элементы списка, закрепленные сверху |
893
+ | `placement` | `"bottom"` \| `"bottom-end"` \| `"bottom-start"` \| `"left"` \| `"left-end"` \| `"left-start"` \| `"right"` \| `"right-end"` \| `"right-start"` \| `"top"` \| `"top-end"` \| `"top-start"` | `top` | Положение поповера относительно своего триггера (children). |
894
+ | `scroll` | `boolean` | — | Включить ли скролл для основной части списка |
895
+ | `scrollContainerClassName` | `string` | — | CSS-класс для scroll обертки основного списка айтемов |
896
+ | `scrollContainerRef` | `Ref<HTMLElement>` | — | Ссылка на контейнер, который скроллится |
897
+ | `scrollRef` | `Ref<HTMLElement>` | — | Ссылка на элемент, обозначающий самый конец прокручиваемого списка |
898
+ | `scrollToSelectedItem` | `boolean` | — | Флаг, отвечающий за прокручивание до выбранного элемента |
899
+ | `search` | `SearchState` | — | Настройки поисковой строки |
900
+ | `selection` | `SelectionMultipleState` \| `SelectionSingleState` | — | Настройки выбора элементов. `mode: 'single'` — один выбранный элемент (`value: ItemId`), <br/> `mode: 'multiple'` — множественный выбор (`value: ItemId[]`). Без `selection` выбора нет — <br/> клик вызывает только `onClick` элемента. |
901
+ | `size` | `"l"` \| `"m"` \| `"s"` | — | Размер списка |
902
+ | `slotAfterTitle` | `ReactNode` | — | Только mobile (`BottomSheet`): slot справа от заголовка. |
903
+ | `trigger` | `"click"` \| `"clickAndFocusVisible"` \| `"focus"` \| `"focusVisible"` \| `"hover"` \| `"hoverAndFocus"` \| `"hoverAndFocusVisible"` | — | Условие отображения поповера: <br/> - `click` - открывать по клику <br/> - `hover` - открывать по ховеру <br/> - `focusVisible` - открывать по focus-visible <br/> - `focus` - открывать по фокусу <br/> - `hoverAndFocusVisible` - открывать по ховеру и focus-visible <br/> - `hoverAndFocus` - открывать по ховеру и фокусу <br/> - `clickAndFocusVisible` - открывать по клику и focus-visible |
904
+ | `triggerClassName` | `string` | — | CSS-класс триггера |
905
+ | `triggerElemRef` | `RefObject<HTMLElement>` | — | Ссылка на элемент-триггер для дроплиста |
906
+ | `untouchableScrollbars` | `boolean` | — | Отключает возможность взаимодействовать со скролбарами мышью. |
907
+ | `virtualized` | `boolean` | — | Включить виртуализацию элементов списка. Рекомендуется при количестве элементов от 1000. |
908
+ | `widthStrategy` | `"auto"` \| `"eq"` \| `"gte"` | `auto` | Стратегия управления шириной контейнера поповера <br/> - `auto` - соответствует ширине контента, <br/> - `gte` - Great Than or Equal, равен ширине таргета или больше ее, если контент в поповере шире, <br/> - `eq` - Equal, строго равен ширине таргета. |
909
+
910
+ ##### Related types
911
+
912
+ **BaseItemWithoutNonGroup**
913
+
914
+ | Prop | Type | Default | Description |
915
+ |------|------|---------|-------------|
916
+ | `afterContent` | `string \| number \| boolean \| ReactElement<any, string \| JSXElementConstructor<any>> \| Iterable<ReactNode> \| ReactPortal \| null \| undefined` | — | Слот после основного контента |
917
+ | `beforeContent` | `string \| number \| boolean \| ReactElement<any, string \| JSXElementConstructor<any>> \| Iterable<ReactNode> \| ReactPortal \| null \| undefined` | — | Слот до основного контента |
918
+ | `checked` | `boolean \| undefined` | — | Управляемое состояние выбранности айтема |
919
+ | `className` | `string \| undefined` | — | CSS-класс |
920
+ | `content` | `ItemContent` | — | Основной контент айтема |
921
+ | `data-test-id` | `string \| undefined` | — | |
922
+ | `disabled` | `boolean \| undefined` | — | Флаг неактивности элемента |
923
+ | `hidden` | `boolean \| undefined` | — | Скрыть элемент из списка (не рендерится и выпадает из навигации) |
924
+ | `id` | `ItemId` | — | Уникальный идентификатор |
925
+ | `itemRef` | `RefObject<HTMLElement> \| undefined` | — | Ссылка на DOM-элемент айтема |
926
+ | `itemWrapRender` | `((item: ReactNode) => ReactNode) \| undefined` | — | Рендер-обёртка вокруг айтема (например, для проксирования в `Tooltip`/`Link`) |
927
+ | `onBlur` | `((e: FocusEvent<HTMLElement>) => void) \| undefined` | — | Колбек обработки блюра |
928
+ | `onClick` | `((e: MouseEvent<HTMLElement>) => void) \| undefined` | — | Колбек обработки клика |
929
+ | `onFocus` | `((e: FocusEvent<HTMLElement>) => void) \| undefined` | — | Колбек обработки фокуса |
930
+ | `onKeyDown` | `((e: KeyboardEvent<HTMLElement>) => void) \| undefined` | — | Колбек обработки нажатия клавиши |
931
+ | `onMouseDown` | `((e: MouseEvent<HTMLElement>) => void) \| undefined` | — | Колбек обработки нажатия кнопки мыши |
932
+ | `showSwitchIcon` | `boolean \| undefined` | — | Флаг отображения иконки у чекбоксов |
933
+
934
+ **CollapseState**
935
+
936
+ | Prop | Type | Default | Description |
937
+ |------|------|---------|-------------|
938
+ | `defaultValue` | `ItemId` | — | |
939
+ | `onChange` | `((value?: ItemId[]) => void) \| undefined` | — | |
940
+ | `value` | `ItemId` | — | |
941
+
942
+ **CommonGroupItem**
943
+
944
+ | Prop | Type | Default | Description |
945
+ |------|------|---------|-------------|
946
+ | `beforeContent` | `string \| number \| boolean \| ReactElement<any, string \| JSXElementConstructor<any>> \| Iterable<ReactNode> \| ReactPortal \| null \| undefined` | — | Слот иконки слева от label. |
947
+ | `divider` | `boolean \| undefined` | — | Показать разделитель над группой |
948
+ | `groupVariant` | `"subtitle"` \| `"subtitleTertiary"` | — | Визуальный стиль заголовка группы |
949
+ | `hidden` | `boolean \| undefined` | — | Скрыть группу из списка |
950
+ | `label` | `string \| undefined` | — | Заголовок группы |
951
+ | `truncate` | `TruncateStringProps` | — | Настройки усечения длинного заголовка группы |
952
+
953
+ **EmptyStateProps**
954
+
955
+ | Prop | Type | Default | Description |
956
+ |------|------|---------|-------------|
957
+ | `className` | `string \| undefined` | — | Дополнительный класс |
958
+ | `content` | `string \| number \| boolean \| ReactElement<any, string \| JSXElementConstructor<any>> \| Iterable<ReactNode> \| ReactPortal \| null \| undefined` | — | Подзаголовок |
959
+ | `data-test-id` | `string \| undefined` | — | |
960
+ | `footer` | `string \| number \| boolean \| ReactElement<any, string \| JSXElementConstructor<any>> \| Iterable<ReactNode> \| ReactPortal \| null \| undefined` | — | Вложенный контент (например ButtonGroup) |
961
+ | `icon` | `IconPredefinedProps` | — | Иконка |
962
+
963
+ - `Item` = `BaseItem | GroupItem | GroupSelectItem | NextListItem | AccordionItem`
964
+
965
+ **ItemContent**
966
+
967
+ | Prop | Type | Default | Description |
968
+ |------|------|---------|-------------|
969
+ | `caption` | `string \| undefined` | — | |
970
+ | `className` | `string \| undefined` | — | |
971
+ | `data-test-id` | `string \| undefined` | — | |
972
+ | `description` | `string \| undefined` | — | |
973
+ | `disabled` | `boolean \| undefined` | — | |
974
+ | `label` | `string \| number` | — | |
975
+ | `truncate` | `TruncateProps` | — | |
976
+
977
+ - `ItemId` = `string | number`
978
+
979
+ - `OnChangeHandler` = `(value: T) => void`
980
+
981
+ **ScrollProps**
982
+
983
+ | Prop | Type | Default | Description |
984
+ |------|------|---------|-------------|
985
+ | `barHideStrategy` | `"leave"` \| `"move"` \| `"never"` \| `"scroll"` | — | Управление скрытием скролл баров: <br/> <br> - `Never` - показывать всегда <br/> <br> - `Leave` - скрывать когда курсор покидает компонент <br/> <br> - `Scroll` - показывать только когда происходит скроллинг <br/> <br> - `Move` - показывать при движении курсора над компонентом |
986
+ | `onScroll` | `OriginalScrollProps` | — | Колбек на скролл прокручиваемого списка |
987
+ | `scroll` | `boolean \| undefined` | — | Включить ли скролл для основной части списка |
988
+ | `scrollContainerRef` | `Ref<HTMLElement> \| undefined` | — | Ссылка на контейнер, который скроллится |
989
+ | `scrollRef` | `Ref<HTMLElement> \| undefined` | — | Ссылка на элемент, обозначающий самый конец прокручиваемого списка |
990
+ | `untouchableScrollbars` | `boolean \| undefined` | — | Отключает возможность взаимодействовать со скролбарами мышью. |
991
+
992
+ **SearchState**
993
+
994
+ | Prop | Type | Default | Description |
995
+ |------|------|---------|-------------|
996
+ | `loading` | `boolean \| undefined` | — | |
997
+ | `onChange` | `(value: string, e?: ChangeEvent<HTMLInputElement>) => void` | — | |
998
+ | `onKeyDown` | `((e: KeyboardEvent<HTMLElement>) => void) \| undefined` | — | |
999
+ | `placeholder` | `string \| undefined` | — | |
1000
+ | `value` | `string \| undefined` | — | |
1001
+
1002
+ **SelectionMultipleState**
1003
+
1004
+ | Prop | Type | Default | Description |
1005
+ |------|------|---------|-------------|
1006
+ | `defaultValue` | `ItemId` | — | Начальное состояние |
1007
+ | `mode` | `"multiple"` | — | Режим выбора |
1008
+ | `onChange` | `ItemId` \| `OnChangeHandler` | — | Controlled обработчик изменения состояния — получает массив выбранных `ItemId[]` |
1009
+ | `value` | `ItemId` | — | Controlled состояние |
1010
+
1011
+ **SelectionSingleState**
1012
+
1013
+ | Prop | Type | Default | Description |
1014
+ |------|------|---------|-------------|
1015
+ | `defaultValue` | `ItemId` | — | Начальное состояние |
1016
+ | `mode` | `"single"` | — | Режим выбора |
1017
+ | `onChange` | `ItemId` \| `OnChangeHandler` | — | Controlled обработчик изменения состояния — получает выбранный `ItemId` |
1018
+ | `value` | `ItemId` | — | Controlled состояние |
1019
+
1020
+ - `Size` = `"l"` \| `"m"` \| `"s"`
1021
+
1022
+ ### Figma
1023
+
1024
+ {/* Droplist делит Figma-файл с List — отдельного droplist/dropdownContainer-узла в FIGMA_NODES нет.
1025
+ Это допустимый fallback (см. figma-integration.md): показываем общий list-узел и item-search. */}
1026
+ ### Droplist vs List
1027
+
1028
+ | Ситуация
1029
+
1030
+ ### Trigger
1031
+
1032
+ `children` — сам триггер. Поддерживаются две формы:
1033
+
1034
+ - `ReactNode` — просто вложенный элемент (кнопка / поле). Droplist навешивает на него open/close.
1035
+ - `({ onKeyDown }) => ReactNode` — render-prop с `onKeyDown`-хендлером, который нужно передать на триггер для клавиатуры. Используйте, если триггер — кастомный компонент без привычной клавиатурной поддержки.
1036
+
1037
+ ### Открытие и контроль
1038
+
1039
+ - **Uncontrolled** — компонент сам управляет `open`, ничего передавать не нужно.
1040
+ - **Controlled** — `open` + `onOpenChange`. Нужен для программного открытия (например, по keyboard shortcut) или синхронизации с URL.
1041
+ - `closeOnPopstate` автоматически закрывает popover при `popstate`-событии — полезно в SPA с router'ом.
1042
+
1043
+ ### Selection и collapse
1044
+
1045
+ Всё, что относится к содержимому списка (`items`, `pinTop`, `pinBottom`, `selection`, `collapse`, `search`, `footer`, `virtualized`, `marker`, `size`, `contentRender`), работает так же, как в **List**.
1046
+
1047
+ ### Доступность
1048
+
1049
+ - Триггер получает `aria-expanded` и `aria-haspopup='listbox'` автоматически (через `Dropdown`).
1050
+ - При открытии фокус переходит внутрь списка, при закрытии — возвращается к триггеру.
1051
+ - `Escape` закрывает popover, `Tab` переключает на следующий focusable элемент страницы.
1052
+ - Стрелки, `Home` / `End`, `Enter` / `Space` работают так же, как в List.
1053
+ - Цвет не единственный индикатор выбранного значения: используется marker и фоновая заливка.
1054
+
1055
+ ### Адаптивность
1056
+
1057
+ `Droplist` — адаптивный компонент с переключением поверхности (surface-swap). Раскладку он берёт из `AdaptiveProvider` (контекст `@cloud-ru/ds-adaptive`); публичный API единый для обеих платформ:
1058
+
1059
+ - **desktop** (по умолчанию) — анкорный popover рядом с триггером.
1060
+ - **mobile** — список рендерится в `BottomSheet` из `@cloud-ru/ds-bottom-sheet` (панель снизу с шапкой и крупными строками size `l`).
1061
+
1062
+ Верстайте под desktop и поставьте один `<AdaptiveProvider>` в корне приложения — mobile-поверхность включается автоматически (desktop-first). Пропа `layoutType` у компонента нет: источник раскладки — только контекст.
1063
+
1064
+ #### Как форсировать платформу
1065
+
1066
+ Форс — только контекстом, не пропом:
1067
+
1068
+ - Поддерево — вложенный провайдер:
1069
+ ```tsx
1070
+ import { AdaptiveProvider } from '@cloud-ru/ds-adaptive'
1071
+
1072
+ <AdaptiveProvider layoutType='mobile'>
1073
+ <Droplist items={items}>{trigger}</Droplist>
1074
+ </AdaptiveProvider>
1075
+ ```
1076
+ - Отдельный компонент — `withLayoutType` (module-scope, сахар над провайдером):
1077
+ ```tsx
1078
+ import { withLayoutType } from '@cloud-ru/ds-adaptive'
1079
+ import { Droplist } from '@cloud-ru/ds-list'
1080
+
1081
+ const MobileDroplist = withLayoutType(Droplist, 'mobile')
1082
+ ```
1083
+
1084
+ #### Платформенные пропы
1085
+
1086
+ Часть пропов привязана к одной поверхности и на другой молча игнорируется. Таблица синхронизирована с JSDoc-пометками у `DroplistProps`.
1087
+
1088
+ | Пропы | desktop | mobile |
1089
+ |-------|---------|--------|
1090
+ | `trigger`, `placement`, `widthStrategy`, `triggerElemRef`, `listRef`, `triggerClassName` | используется | игнорируется |
1091
+ | `label`, `actionButton`, `slotAfterTitle`, `onBackButtonClick` | игнорируется | используется |
1092
+ | `items`, `selection`, `collapse`, `search`, `footer`, `headerDivider`, `footerDivider` | используется | используется |
1093
+ | `open`, `onOpenChange`, `closeOnPopstate`, `size` | используется | используется |
1094
+
1095
+ #### Mobile — BottomSheet
1096
+
1097
+ Раскладка форсирована в mobile: по клику триггера список открывается в BottomSheet.
1098
+
1099
+ ```tsx
1100
+ import { AdaptiveProvider, LAYOUT_TYPE } from '@cloud-ru/ds-adaptive';
1101
+ import { Button } from '@cloud-ru/ds-button';
1102
+ import { Droplist } from '@cloud-ru/ds-list';
1103
+ import { useState } from 'react';
1104
+
1105
+ import styles from './styles.module.scss';
1106
+
1107
+ export function MobileDroplist() {
1108
+ const [value, setValue] = useState<string | number | undefined>('rub');
1109
+
1110
+ return (
1111
+ <AdaptiveProvider layoutType={LAYOUT_TYPE.Mobile}>
1112
+ <div className={styles.wrapper}>
1113
+ <Droplist
1114
+ label='Валюта'
1115
+ closeDroplistOnItemClick
1116
+ selection={{ mode: 'single', value, onChange: setValue }}
1117
+ items={[
1118
+ { id: 'usd', content: { label: 'USD — Доллар США' } },
1119
+ { id: 'eur', content: { label: 'EUR — Евро' } },
1120
+ { id: 'rub', content: { label: 'RUB — Российский рубль' } },
1121
+ { id: 'cny', content: { label: 'CNY — Китайский юань' } },
1122
+ ]}
1123
+ >
1124
+ <Button size='s' appearance='neutral' view='outline' label={`Валюта: ${String(value).toUpperCase()}`} />
1125
+ </Droplist>
1126
+ </div>
1127
+ </AdaptiveProvider>
1128
+ );
1129
+ }
1130
+ ```
1131
+
1132
+ Подробнее о модели адаптивности — [Адаптивность — паттерн](/patterns/adaptive).
1133
+
1134
+ ## ItemContent
1135
+
1136
+ Каноничная разметка содержимого элемента списка — label / caption / description с единым truncate-поведением.
1137
+
1138
+ `ItemContent` — каноничная разметка основного контента item'а: заголовок (`label`), мета справа (`caption`), подпись снизу (`description`). Используется как значение поля `item.content`, а также экспортируется отдельно для кастомного рендеринга внутри `contentRender`.
1139
+
1140
+ ### Когда использовать
1141
+
1142
+ - Всегда, когда элемент списка можно описать как «заголовок + мета-подпись + короткое описание».
1143
+ - Когда нужен одинаковый truncate-алгоритм во всех списках пакета.
1144
+ - Когда `content` задаётся как объект в `items` — ровно это и есть `ItemContent` под капотом.
1145
+
1146
+ Когда **не** нужен:
1147
+
1148
+ - Совсем кастомный layout внутри item — передавайте в `content` свой `ReactNode`, но тогда единый visual-signal и truncate ложатся на вас.
1149
+ - Элемент с одной иконкой без текста — передавайте `beforeContent` / `afterContent` и оставьте `content` пустым.
1150
+
1151
+ ### Анатомия
1152
+
1153
+ #### Slots
1154
+
1155
+ - `label` *(required)* — заголовок item'а (имя файла, название валюты, email-адрес).
1156
+ - `caption` — короткая мета справа (счётчик, дата, badge, shortcut).
1157
+ - `description` — подпись под заголовком (хвост описания, короткая справка, секундный статус).
1158
+
1159
+ Вне `ItemContent`, но рядом (поля item'а):
1160
+
1161
+ - `beforeContent` — иконка / аватар слева.
1162
+ - `afterContent` — слот справа: чеврон / trailing-иконка в иконочном квадрате; произвольный контент (счётчик, badge, tag) рендерится в натуральном размере без обрезки.
1163
+ - `switch: true` — заменяет чекбокс/маркер на `Switch` справа; состояние тумблера берётся из `selection`. Для списков-настроек, где выбор = включение функции. `showSwitchIcon` зарезервирован под иконку тумблера.
1164
+
1165
+ #### Group variant
1166
+
1167
+ У групповых item'ов (`type: 'group'` / `'group-select'`) заголовок оформляется `ItemContent`-независимо, через `groupVariant`:
1168
+
1169
+ - `subtitle` — контрастный подзаголовок секции.
1170
+ - `subtitleTertiary` *(default)* — приглушённый третичный текст.
1171
+
1172
+ #### Truncate
1173
+
1174
+ - Дефолт: `label` — 1 строка, `description` — 2 строки, вариант обрезки — `end` (многоточие в конце).
1175
+ - Переопределяется через `truncate.label`, `truncate.description`, `truncate.variant` (`end` / `middle`).
1176
+
1177
+ ### Примеры использования
1178
+
1179
+ 99% случаев — `content` передаётся как объект, компонент сам строит `<ItemContent />` под капотом:
1180
+
1181
+ #### content как объект
1182
+
1183
+ item.content = { label, caption, description } — List оборачивает в ItemContent сам.
1184
+
1185
+ ```tsx
1186
+ import { List } from '@cloud-ru/ds-list';
1187
+
1188
+ import styles from './styles.module.scss';
1189
+
1190
+ export function BasicList() {
1191
+ return (
1192
+ <div className={styles.box}>
1193
+ <List
1194
+ size='s'
1195
+ items={[
1196
+ { id: 'inbox', content: { label: 'Входящие', caption: '12' } },
1197
+ { id: 'sent', content: { label: 'Отправленные' } },
1198
+ { id: 'archive', content: { label: 'Архив', caption: '238' } },
1199
+ { id: 'trash', content: { label: 'Корзина', description: 'Удаляется через 30 дней' } },
1200
+ ]}
1201
+ />
1202
+ </div>
1203
+ );
1204
+ }
1205
+ ```
1206
+
1207
+ #### Слоты beforeContent / afterContent
1208
+
1209
+ Иконка слева, caption-счётчик справа от заголовка и trailing-иконка в afterContent.
1210
+
1211
+ ```tsx
1212
+ import { FileSVG, FolderSVG, StarSVG } from '@cloud-ru/ds-icons/interface/system';
1213
+ import { List } from '@cloud-ru/ds-list';
1214
+
1215
+ import styles from './styles.module.scss';
1216
+
1217
+ export function ItemContentSlots() {
1218
+ return (
1219
+ <div className={styles.box}>
1220
+ <List
1221
+ size='m'
1222
+ items={[
1223
+ {
1224
+ id: 'reports',
1225
+ beforeContent: <FolderSVG />,
1226
+ content: { label: 'Отчёты', caption: '24' },
1227
+ afterContent: <StarSVG />,
1228
+ },
1229
+ {
1230
+ id: 'invoice',
1231
+ beforeContent: <FileSVG />,
1232
+ content: { label: 'invoice-2024.pdf', caption: '1.2 МБ' },
1233
+ },
1234
+ ]}
1235
+ />
1236
+ </div>
1237
+ );
1238
+ }
1239
+ ```
1240
+
1241
+ #### Заголовок, caption и description вместе
1242
+
1243
+ label + caption (мета справа) + description (подпись снизу) в одном item.
1244
+
1245
+ ```tsx
1246
+ import { List } from '@cloud-ru/ds-list';
1247
+
1248
+ import styles from './styles.module.scss';
1249
+
1250
+ export function ItemContentDescription() {
1251
+ return (
1252
+ <div className={styles.box}>
1253
+ <List
1254
+ size='l'
1255
+ items={[
1256
+ {
1257
+ id: 'eu-west',
1258
+ content: {
1259
+ label: 'eu-west-1',
1260
+ caption: 'Доступно',
1261
+ description: 'Ирландия — основной регион размещения',
1262
+ },
1263
+ },
1264
+ {
1265
+ id: 'us-east',
1266
+ content: {
1267
+ label: 'us-east-1',
1268
+ caption: 'Деградация',
1269
+ description: 'Северная Виргиния — повышенная задержка отклика',
1270
+ },
1271
+ },
1272
+ ]}
1273
+ />
1274
+ </div>
1275
+ );
1276
+ }
1277
+ ```
1278
+
1279
+ #### Обрезка текста (truncate)
1280
+
1281
+ truncate.label/description задают число строк, variant — место многоточия (end/middle). В узком контейнере длинный текст обрезается.
1282
+
1283
+ ```tsx
1284
+ import { List } from '@cloud-ru/ds-list';
1285
+
1286
+ import styles from './styles.module.scss';
1287
+
1288
+ export function ItemContentTruncate() {
1289
+ return (
1290
+ <div className={styles.narrowBox}>
1291
+ <List
1292
+ size='m'
1293
+ items={[
1294
+ {
1295
+ id: 'truncated',
1296
+ content: {
1297
+ label: 'very-long-instance-name-that-overflows.example.internal',
1298
+ caption: '8',
1299
+ description:
1300
+ 'Длинное описание ресурса, которое не помещается в одну строку и обрезается по заданному числу строк',
1301
+ truncate: { label: 1, description: 2, variant: 'middle' },
1302
+ },
1303
+ },
1304
+ ]}
1305
+ />
1306
+ </div>
1307
+ );
1308
+ }
1309
+ ```
1310
+
1311
+ Если нужен кастомный слот (бейдж, превью, аватар) поверх стандартной разметки — используйте `contentRender` на `List`. Внутри возвращайте `<ItemContent {...props} />` плюс свои дополнения. Это сохраняет типографику и truncate.
1312
+
1313
+ ### Props
1314
+
1315
+ **ItemContentProps**
1316
+
1317
+ | Prop | Type | Default | Description |
1318
+ |------|------|---------|-------------|
1319
+ | `caption` | `string` | — | |
1320
+ | `className` | `string` | — | |
1321
+ | `data-test-id` | `string` | — | |
1322
+ | `description` | `string` | — | |
1323
+ | `disabled` | `boolean` | — | |
1324
+ | `label` | `string \| number` | — | |
1325
+ | `truncate` | `TruncateProps` | — | |
1326
+
1327
+ ##### Related types
1328
+
1329
+ **TruncateProps**
1330
+
1331
+ | Prop | Type | Default | Description |
1332
+ |------|------|---------|-------------|
1333
+ | `description` | `number \| undefined` | — | |
1334
+ | `label` | `number \| undefined` | — | |
1335
+ | `variant` | `"end"` \| `"middle"` | — | |
1336
+
1337
+ ### Storybook
1338
+
1339
+ `ItemContent` живёт внутри `List`, отдельной истории нет — все сценарии видны в **List playground**.
1340
+
1341
+ ### Доступность
1342
+
1343
+ - `label` остаётся текстовым заголовком — ассистивные технологии прочитают его как основное имя элемента.
1344
+ - `caption` и `description` находятся в том же DOM-узле и читаются следом за заголовком.
1345
+ - Визуальные truncate-поведения реализованы через CSS `-webkit-line-clamp`, текст не вырезается из DOM и остаётся доступен скринридеру целиком.
1346
+ - Disabled-элементы получают уменьшенный контраст + `data-disabled`, но цвет не единственный сигнал: интерактивность выключается и на уровне родительского item'а.
1347
+
1348
+ ## ReorderableList
1349
+
1350
+ ```tsx
1351
+ import { ReorderableList, SimpleItem } from '@cloud-ru/ds-list';
1352
+ import { useState } from 'react';
1353
+
1354
+ import styles from './styles.module.scss';
1355
+
1356
+ const INITIAL_ITEMS: SimpleItem[] = [
1357
+ { id: 'inbox', content: { label: 'Входящие', caption: '12' } },
1358
+ { id: 'sent', content: { label: 'Отправленные' } },
1359
+ { id: 'archive', content: { label: 'Архив', caption: '238' } },
1360
+ { id: 'trash', content: { label: 'Корзина', description: 'Удаляется через 30 дней' } },
1361
+ ];
1362
+
1363
+ export function ListReorder() {
1364
+ const [items, setItems] = useState(INITIAL_ITEMS);
1365
+
1366
+ return (
1367
+ <div className={styles.box}>
1368
+ <ReorderableList size='s' items={items} onItemsReorder={setItems} />
1369
+ </div>
1370
+ );
1371
+ }
1372
+ ```
1373
+
1374
+ ### Props
1375
+
1376
+ | Prop | Type | Default | Description |
1377
+ |------|------|---------|-------------|
1378
+ | `barHideStrategy` | `"leave"` \| `"move"` \| `"never"` \| `"scroll"` | — | Управление скрытием скролл баров: <br/> <br> - `Never` - показывать всегда <br/> <br> - `Leave` - скрывать когда курсор покидает компонент <br/> <br> - `Scroll` - показывать только когда происходит скроллинг <br/> <br> - `Move` - показывать при движении курсора над компонентом |
1379
+ | `className` | `string` | — | CSS-класс |
1380
+ | `collapse` | `CollapseState` | `{}` | Настройки раскрытия элементов |
1381
+ | `contentRender` | `((props: ContentRenderProps) => ReactNode)` | — | Рендер функция основного контента айтема |
1382
+ | `data-test-id` | `string` | — | |
1383
+ | `dataError` | `boolean` | — | Загрузка данных завершилась ошибкой: показывается `errorDataState` |
1384
+ | `dataFiltered` | `boolean` | — | Текущий пустой список — результат поиска/фильтра: показывается `noResultsState` вместо `noDataState` |
1385
+ | `errorDataState` | `EmptyStateProps` | — | Экран при ошибке запроса |
1386
+ | `footer` | `ReactNode ;` | — | Кастомизируемый элемент в конце списка |
1387
+ | `footerActiveElementsRefs` | `RefObject<HTMLElement>[]` | — | Список ссылок на кастомные элементы, помещенные в специальную секцию внизу списка |
1388
+ | `footerDivider` | `boolean` | — | Показывать divider между body и footer (Figma `dropdownContainer.dividerWrapper` снизу) |
1389
+ | `hasListInFocusChain` | `boolean` | `true` | Флаг, отвечающий за включение самого родительского контейнера листа в цепочку фокусирующихся элементов |
1390
+ | `header` | `ReactNode ;` | — | Кастомизируемый элемент в начале списка — Figma `dropdownContainer.topBar`. <br/> Подходит для заголовка / справочного блока над поиском. |
1391
+ | `headerDivider` | `boolean` | — | Показывать divider между header и body (Figma `dropdownContainer.dividerWrapper` сверху) |
1392
+ | `items` | `ReorderItem[]` | `[]` | Основные элементы списка: строки `SimpleItem` и/или группы с заголовком `SimpleGroupItem` <br/> (`type: 'group'` + `label` + сортируемые `items`). |
1393
+ | `keyboardNavigationRef` | `RefObject<{ focusItem(id: ItemId): void; }>` | — | Ссылка на управление навигацией листа с клавиатуры |
1394
+ | `limitedScrollHeight` | `boolean` | — | Ограничить максимальную высоту скролл-контейнера в зависимости от `size` |
1395
+ | `loading` | `boolean` | — | Флаг, отвечающий за состояние загрузки списка |
1396
+ | `marker` | `boolean` | `true` | Отображать ли маркер у выбранного элемента списка |
1397
+ | `noDataState` | `EmptyStateProps` | — | Экран при отсутствии данных |
1398
+ | `noResultsState` | `EmptyStateProps` | — | Экран при отсутствии результатов поиска или фильтров |
1399
+ | `onItemsReorder` | `(items: ReorderItem[]) => void` | — | Колбек по завершению drag&drop-переупорядочивания элементов списка. Список остаётся <br/> управляемым: сам не хранит порядок, а отдаёт наружу целиком обновлённое дерево `items` — <br/> потребитель обновляет свой стейт этим значением. Переупорядочивание работает только среди <br/> «братьев» одного уровня (строки без группы либо строки внутри одной группы; перенос между <br/> группами не поддерживается). |
1400
+ | `onKeyDown` | `((e: KeyboardEvent<HTMLElement>) => void)` | — | Обработчик события по нажатию клавиш |
1401
+ | `onScroll` | `((event?: Event) => void)` | — | Колбек на скролл прокручиваемого списка |
1402
+ | `pinBottom` | `Item[]` | `[]` | Элементы списка, закрепленные снизу |
1403
+ | `pinTop` | `Item[]` | `[]` | Элементы списка, закрепленные сверху |
1404
+ | `scroll` | `boolean` | — | Включить ли скролл для основной части списка |
1405
+ | `scrollContainerClassName` | `string` | — | CSS-класс для scroll обертки основного списка айтемов |
1406
+ | `scrollContainerRef` | `Ref<HTMLElement>` | — | Ссылка на контейнер, который скроллится |
1407
+ | `scrollRef` | `Ref<HTMLElement>` | — | Ссылка на элемент, обозначающий самый конец прокручиваемого списка |
1408
+ | `scrollToSelectedItem` | `boolean` | — | Флаг, отвечающий за прокручивание до выбранного элемента |
1409
+ | `search` | `SearchState` | — | Настройки поисковой строки |
1410
+ | `selection` | `SelectionMultipleState \| SelectionSingleState` | — | Настройки выбора элементов. `mode: 'single'` — один выбранный элемент (`value: ItemId`), <br/> `mode: 'multiple'` — множественный выбор (`value: ItemId[]`). Без `selection` выбора нет — <br/> клик вызывает только `onClick` элемента. |
1411
+ | `size` | `"l"` \| `"m"` \| `"s"` | `m` | Размер списка |
1412
+ | `tabIndex` | `number` | `0` | `tabIndex` корневого элемента списка (для управления порядком фокуса) |
1413
+ | `untouchableScrollbars` | `boolean` | — | Отключает возможность взаимодействовать со скролбарами мышью. |