chuijs 4.0.2 → 4.0.4

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 (292) hide show
  1. package/CHANGELOG.md +597 -0
  2. package/README.md +43 -644
  3. package/framework/appLayout/default/chui_app_layout.js +602 -496
  4. package/framework/appLayout/default/global_style.css +288 -97
  5. package/framework/appLayout/default/main_theme_style.css +188 -101
  6. package/framework/components/chui_accordion/accordion.js +38 -35
  7. package/framework/components/chui_accordion/styles.css +13 -12
  8. package/framework/components/chui_action_sheet/action_sheet.js +93 -64
  9. package/framework/components/chui_action_sheet/styles.css +12 -5
  10. package/framework/components/chui_actions_bar/actions_bar.js +338 -0
  11. package/framework/components/chui_actions_bar/styles.css +139 -0
  12. package/framework/components/chui_affix/affix.js +16 -11
  13. package/framework/components/chui_affix/styles.css +5 -0
  14. package/framework/components/chui_alert/alert.js +34 -19
  15. package/framework/components/chui_alert/styles.css +7 -0
  16. package/framework/components/chui_avatar/avatar.js +36 -35
  17. package/framework/components/chui_avatar/styles.css +1 -1
  18. package/framework/components/chui_avatar_group/avatar_group.js +25 -23
  19. package/framework/components/chui_avatar_group/styles.css +1 -1
  20. package/framework/components/chui_backtop/backtop.js +60 -21
  21. package/framework/components/chui_backtop/styles.css +21 -0
  22. package/framework/components/chui_badge/badge.js +30 -28
  23. package/framework/components/chui_badge/styles.css +6 -6
  24. package/framework/components/chui_banner/banner.js +222 -0
  25. package/framework/components/chui_banner/styles.css +170 -0
  26. package/framework/components/chui_breadcrumb/breadcrumb.js +15 -13
  27. package/framework/components/chui_breadcrumb/styles.css +1 -1
  28. package/framework/components/chui_button/button.js +52 -36
  29. package/framework/components/chui_button/styles.css +7 -7
  30. package/framework/components/chui_calendar/calendar.js +18 -16
  31. package/framework/components/chui_card/card.js +52 -32
  32. package/framework/components/chui_card_scroll/card_scroll.js +169 -0
  33. package/framework/components/chui_card_scroll/styles.css +86 -0
  34. package/framework/components/chui_chat/chat.js +140 -79
  35. package/framework/components/chui_chat/styles.css +18 -7
  36. package/framework/components/chui_clipboard_history/clipboard_history.js +53 -39
  37. package/framework/components/chui_clipboard_history/styles.css +13 -9
  38. package/framework/components/chui_code/code.js +10 -10
  39. package/framework/components/chui_code/styles.css +4 -4
  40. package/framework/components/chui_code_editor/code_editor.js +73 -57
  41. package/framework/components/chui_code_editor/styles.css +6 -5
  42. package/framework/components/chui_command_palette/command_palette.js +87 -64
  43. package/framework/components/chui_command_palette/styles.css +5 -5
  44. package/framework/components/chui_console/console.js +13 -13
  45. package/framework/components/chui_console/styles.css +6 -3
  46. package/framework/components/chui_content_block/content_block.js +35 -21
  47. package/framework/components/chui_content_block/styles.css +2 -2
  48. package/framework/components/chui_context_menu/context_menu.js +171 -58
  49. package/framework/components/chui_context_menu/styles.css +82 -8
  50. package/framework/components/chui_custom_element/custom_element.js +25 -18
  51. package/framework/components/chui_data_grid/data_grid.js +633 -471
  52. package/framework/components/chui_data_grid/styles.css +24 -22
  53. package/framework/components/chui_descriptions/descriptions.js +34 -17
  54. package/framework/components/chui_descriptions/styles.css +1 -1
  55. package/framework/components/chui_details/details.js +42 -36
  56. package/framework/components/chui_details/styles.css +12 -11
  57. package/framework/components/chui_diff_viewer/diff_viewer.js +172 -147
  58. package/framework/components/chui_diff_viewer/styles.css +10 -10
  59. package/framework/components/chui_divider/divider.js +19 -17
  60. package/framework/components/chui_divider/styles.css +2 -2
  61. package/framework/components/chui_drawer/drawer.js +63 -38
  62. package/framework/components/chui_drawer/styles.css +18 -4
  63. package/framework/components/chui_drop_zone/drop_zone.js +37 -27
  64. package/framework/components/chui_drop_zone/styles.css +10 -6
  65. package/framework/components/chui_empty_state/empty_state.js +20 -15
  66. package/framework/components/chui_fieldset/fieldset.js +40 -23
  67. package/framework/components/chui_fieldset/styles.css +6 -6
  68. package/framework/components/chui_file_explorer/file_explorer.js +260 -189
  69. package/framework/components/chui_file_explorer/styles.css +8 -7
  70. package/framework/components/chui_form/form.js +40 -28
  71. package/framework/components/chui_form/styles.css +1 -1
  72. package/framework/components/chui_gantt/gantt.js +102 -78
  73. package/framework/components/chui_gantt/styles.css +5 -5
  74. package/framework/components/chui_graphs/graphs.js +376 -239
  75. package/framework/components/chui_graphs/styles.css +27 -6
  76. package/framework/components/chui_hero/hero.js +98 -37
  77. package/framework/components/chui_hero/styles.css +176 -7
  78. package/framework/components/chui_html_block/html_block.js +8 -8
  79. package/framework/components/chui_html_block/styles.css +3 -3
  80. package/framework/components/chui_hx/hx.js +3 -3
  81. package/framework/components/chui_hx/styles.css +13 -6
  82. package/framework/components/chui_icons/icons.js +2115 -2115
  83. package/framework/components/chui_icons/styles.css +4 -3
  84. package/framework/components/chui_inputs/chui_check_box/check_box.js +31 -19
  85. package/framework/components/chui_inputs/chui_check_box/styles.css +30 -21
  86. package/framework/components/chui_inputs/chui_chips/chips.js +141 -70
  87. package/framework/components/chui_inputs/chui_chips/styles.css +32 -7
  88. package/framework/components/chui_inputs/chui_color_picker/color_picker.js +55 -38
  89. package/framework/components/chui_inputs/chui_color_picker/styles.css +7 -7
  90. package/framework/components/chui_inputs/chui_combo_box/combo_box.js +127 -109
  91. package/framework/components/chui_inputs/chui_combo_box/styles.css +23 -21
  92. package/framework/components/chui_inputs/chui_date/date.js +161 -127
  93. package/framework/components/chui_inputs/chui_date/styles.css +20 -19
  94. package/framework/components/chui_inputs/chui_date_range/date_range.js +61 -50
  95. package/framework/components/chui_inputs/chui_date_range/styles.css +3 -3
  96. package/framework/components/chui_inputs/chui_datetime/datetime.js +47 -39
  97. package/framework/components/chui_inputs/chui_datetime/styles.css +3 -3
  98. package/framework/components/chui_inputs/chui_email/email.js +48 -36
  99. package/framework/components/chui_inputs/chui_email/styles.css +11 -11
  100. package/framework/components/chui_inputs/chui_file/file.js +71 -44
  101. package/framework/components/chui_inputs/chui_file/styles.css +23 -19
  102. package/framework/components/chui_inputs/chui_hotkey/hotkey.js +101 -76
  103. package/framework/components/chui_inputs/chui_hotkey/styles.css +6 -6
  104. package/framework/components/chui_inputs/chui_masked/masked.js +83 -72
  105. package/framework/components/chui_inputs/chui_masked/styles.css +5 -5
  106. package/framework/components/chui_inputs/chui_mention/mention.js +87 -40
  107. package/framework/components/chui_inputs/chui_mention/styles.css +14 -8
  108. package/framework/components/chui_inputs/chui_multi_combo_box/multi_combo_box.js +242 -212
  109. package/framework/components/chui_inputs/chui_multi_combo_box/styles.css +23 -15
  110. package/framework/components/chui_inputs/chui_number/number.js +84 -70
  111. package/framework/components/chui_inputs/chui_number/styles.css +13 -13
  112. package/framework/components/chui_inputs/chui_otp/otp.js +71 -62
  113. package/framework/components/chui_inputs/chui_otp/styles.css +5 -5
  114. package/framework/components/chui_inputs/chui_password/password.js +72 -56
  115. package/framework/components/chui_inputs/chui_password/styles.css +15 -15
  116. package/framework/components/chui_inputs/chui_radio_button/radio_button.js +37 -25
  117. package/framework/components/chui_inputs/chui_radio_button/styles.css +22 -22
  118. package/framework/components/chui_inputs/chui_rating/rating.js +54 -50
  119. package/framework/components/chui_inputs/chui_rating/styles.css +3 -3
  120. package/framework/components/chui_inputs/chui_select_box/select_box.js +132 -108
  121. package/framework/components/chui_inputs/chui_select_box/styles.css +21 -21
  122. package/framework/components/chui_inputs/chui_slider/slider.js +41 -38
  123. package/framework/components/chui_inputs/chui_slider/styles.css +7 -5
  124. package/framework/components/chui_inputs/chui_text/styles.css +14 -14
  125. package/framework/components/chui_inputs/chui_text/text.js +69 -52
  126. package/framework/components/chui_inputs/chui_text_area/styles.css +14 -14
  127. package/framework/components/chui_inputs/chui_text_area/text_area.js +59 -36
  128. package/framework/components/chui_inputs/chui_time/styles.css +18 -20
  129. package/framework/components/chui_inputs/chui_time/time.js +71 -49
  130. package/framework/components/chui_inputs/chui_toggle/styles.css +13 -6
  131. package/framework/components/chui_inputs/chui_toggle/toggle.js +27 -27
  132. package/framework/components/chui_json_editor/json_editor.js +137 -127
  133. package/framework/components/chui_json_editor/styles.css +5 -5
  134. package/framework/components/chui_json_viewer/json_viewer.js +133 -114
  135. package/framework/components/chui_json_viewer/styles.css +9 -8
  136. package/framework/components/chui_kanban/kanban.js +78 -60
  137. package/framework/components/chui_kanban/styles.css +8 -4
  138. package/framework/components/chui_kbd/kbd.js +12 -10
  139. package/framework/components/chui_kbd/styles.css +1 -1
  140. package/framework/components/chui_label/label.js +31 -29
  141. package/framework/components/chui_label/styles.css +3 -3
  142. package/framework/components/chui_link/link.js +36 -29
  143. package/framework/components/chui_link/styles.css +11 -4
  144. package/framework/components/chui_list/list.js +42 -38
  145. package/framework/components/chui_list/styles.css +4 -3
  146. package/framework/components/chui_markdown_editor/markdown_editor.js +125 -109
  147. package/framework/components/chui_markdown_editor/styles.css +5 -5
  148. package/framework/components/chui_media/audio.js +457 -364
  149. package/framework/components/chui_media/audio_styles.css +74 -37
  150. package/framework/components/chui_media/image.js +99 -64
  151. package/framework/components/chui_media/image_compare.js +86 -55
  152. package/framework/components/chui_media/image_compare_styles.css +5 -5
  153. package/framework/components/chui_media/image_styles.css +4 -4
  154. package/framework/components/chui_media/video.js +264 -171
  155. package/framework/components/chui_media/video_styles.css +57 -24
  156. package/framework/components/chui_menu_bar/menu_bar.js +67 -65
  157. package/framework/components/chui_menu_bar/styles.css +14 -12
  158. package/framework/components/chui_message_bar/message_bar.js +551 -0
  159. package/framework/components/chui_message_bar/styles.css +248 -0
  160. package/framework/components/chui_modal/modal.js +81 -56
  161. package/framework/components/chui_modal/styles.css +3 -2
  162. package/framework/components/chui_notification/notification.js +107 -69
  163. package/framework/components/chui_notification/notification_download.js +28 -77
  164. package/framework/components/chui_notification/notification_download_card.js +115 -0
  165. package/framework/components/chui_notification/notification_download_progress.css +5 -4
  166. package/framework/components/chui_notification/notification_download_progress.js +73 -105
  167. package/framework/components/chui_notification/notification_update.js +44 -37
  168. package/framework/components/chui_notification/status_plate.css +3 -3
  169. package/framework/components/chui_notification/status_plate.js +2 -2
  170. package/framework/components/chui_notification/styles.css +6 -1
  171. package/framework/components/chui_notification/styles_download.css +2 -2
  172. package/framework/components/chui_notification/styles_update.css +3 -3
  173. package/framework/components/chui_org_chart/org_chart.js +101 -62
  174. package/framework/components/chui_org_chart/styles.css +2 -2
  175. package/framework/components/chui_pagination/pagination.js +65 -30
  176. package/framework/components/chui_pagination/styles.css +1 -1
  177. package/framework/components/chui_paragraph/paragraph.js +2 -2
  178. package/framework/components/chui_paragraph/styles.css +3 -3
  179. package/framework/components/chui_popover/popover.js +77 -50
  180. package/framework/components/chui_popover/styles.css +1 -1
  181. package/framework/components/chui_popups/popups.js +210 -181
  182. package/framework/components/chui_popups/styles.css +7 -7
  183. package/framework/components/chui_print_button/print_button.js +48 -33
  184. package/framework/components/chui_progress_bar/progress_bar.js +49 -19
  185. package/framework/components/chui_progress_bar/styles.css +6 -6
  186. package/framework/components/chui_progress_ring/progress_ring.js +32 -30
  187. package/framework/components/chui_progress_ring/styles.css +1 -1
  188. package/framework/components/chui_qrcode/qrcode.js +85 -41
  189. package/framework/components/chui_query_builder/query_builder.js +263 -170
  190. package/framework/components/chui_query_builder/styles.css +4 -4
  191. package/framework/components/chui_radio_group/radio_group.js +36 -26
  192. package/framework/components/chui_radio_group/styles.css +2 -2
  193. package/framework/components/chui_result/result.js +41 -31
  194. package/framework/components/chui_result/styles.css +3 -3
  195. package/framework/components/chui_scheduler/scheduler.js +126 -104
  196. package/framework/components/chui_scheduler/styles.css +11 -7
  197. package/framework/components/chui_screen_spinner/screen_spinner.js +167 -0
  198. package/framework/components/chui_screen_spinner/styles.css +49 -0
  199. package/framework/components/chui_segmented_control/segmented_control.js +90 -27
  200. package/framework/components/chui_segmented_control/styles.css +32 -6
  201. package/framework/components/chui_settings/settings.js +160 -87
  202. package/framework/components/chui_settings/styles.css +4 -4
  203. package/framework/components/chui_shortcut_help/shortcut_help.js +82 -61
  204. package/framework/components/chui_sidebar_nav/sidebar_nav.js +132 -53
  205. package/framework/components/chui_sidebar_nav/styles.css +9 -3
  206. package/framework/components/chui_skeleton/skeleton.js +29 -24
  207. package/framework/components/chui_slideshow/slideshow.js +122 -103
  208. package/framework/components/chui_slideshow/styles.css +22 -16
  209. package/framework/components/chui_spinner/spinner.js +17 -17
  210. package/framework/components/chui_spinner/styles.css +0 -1
  211. package/framework/components/chui_split_button/split_button.js +53 -30
  212. package/framework/components/chui_split_button/styles.css +22 -6
  213. package/framework/components/chui_splitter/splitter.js +46 -39
  214. package/framework/components/chui_splitter/styles.css +2 -2
  215. package/framework/components/chui_statistic/statistic.js +111 -84
  216. package/framework/components/chui_status_bar/status_bar.js +11 -9
  217. package/framework/components/chui_status_bar/styles.css +2 -2
  218. package/framework/components/chui_stepper/stepper.js +230 -54
  219. package/framework/components/chui_stepper/styles.css +79 -14
  220. package/framework/components/chui_sticky_bar/sticky_bar.js +53 -17
  221. package/framework/components/chui_sticky_bar/styles.css +11 -7
  222. package/framework/components/chui_tabs/chui_tabs.js +100 -46
  223. package/framework/components/chui_tabs/styles.css +43 -15
  224. package/framework/components/chui_text_editor/styles.css +24 -23
  225. package/framework/components/chui_text_editor/text_editor.js +103 -79
  226. package/framework/components/chui_text_editor/text_editor_panel.js +428 -367
  227. package/framework/components/chui_theme_toggle/theme_toggle.js +46 -25
  228. package/framework/components/chui_timeline/styles.css +3 -1
  229. package/framework/components/chui_timeline/timeline.js +14 -12
  230. package/framework/components/chui_toast_queue/styles.css +11 -11
  231. package/framework/components/chui_toast_queue/toast_queue.js +82 -73
  232. package/framework/components/chui_toolbar/styles.css +2 -2
  233. package/framework/components/chui_toolbar/toolbar.js +20 -13
  234. package/framework/components/chui_tooltip/styles.css +2 -2
  235. package/framework/components/chui_tooltip/tooltip.js +40 -27
  236. package/framework/components/chui_tour/styles.css +12 -7
  237. package/framework/components/chui_tour/tour.js +132 -98
  238. package/framework/components/chui_transfer/styles.css +8 -7
  239. package/framework/components/chui_transfer/transfer.js +143 -123
  240. package/framework/components/chui_tree_table/styles.css +4 -4
  241. package/framework/components/chui_tree_table/tree_table.js +88 -74
  242. package/framework/components/chui_tree_view/styles.css +14 -10
  243. package/framework/components/chui_tree_view/tree_view.js +298 -253
  244. package/framework/components/chui_user_bar/styles.css +145 -0
  245. package/framework/components/chui_user_bar/user_bar.js +402 -0
  246. package/framework/components/chui_user_list_bar/styles.css +173 -0
  247. package/framework/components/chui_user_list_bar/user_list_bar.js +485 -0
  248. package/framework/components/chui_watermark/watermark.js +41 -33
  249. package/framework/components/chui_webview/styles.css +2 -2
  250. package/framework/components/chui_webview/webview.js +64 -52
  251. package/framework/components/chui_write_bar/styles.css +172 -0
  252. package/framework/components/chui_write_bar/write_bar.js +320 -0
  253. package/framework/main_window.js +1234 -0
  254. package/framework/modules/chui_a11y/chui_a11y.js +153 -123
  255. package/framework/modules/chui_animations/animations.js +105 -43
  256. package/framework/modules/chui_animations/motion.js +120 -0
  257. package/framework/modules/chui_animations/styles.css +37 -4
  258. package/framework/modules/chui_bridge/preload.js +36 -25
  259. package/framework/modules/chui_functions.js +69 -63
  260. package/framework/modules/chui_highlight/chui_highlight.js +25 -22
  261. package/framework/modules/chui_highlight/styles.css +13 -10
  262. package/framework/modules/chui_i18n/chui_i18n.js +12 -10
  263. package/framework/modules/chui_i18n/locales/a11y.js +5 -5
  264. package/framework/modules/chui_i18n/locales/app_layout.js +5 -5
  265. package/framework/modules/chui_i18n/locales/calendar.js +3 -3
  266. package/framework/modules/chui_i18n/locales/common.js +3 -3
  267. package/framework/modules/chui_i18n/locales/data.js +36 -26
  268. package/framework/modules/chui_i18n/locales/editors.js +40 -13
  269. package/framework/modules/chui_i18n/locales/feedback.js +65 -3
  270. package/framework/modules/chui_i18n/locales/forms.js +8 -8
  271. package/framework/modules/chui_i18n/locales/graphics.js +8 -8
  272. package/framework/modules/chui_i18n/locales/media.js +4 -4
  273. package/framework/modules/chui_i18n/locales/navigation.js +12 -8
  274. package/framework/modules/chui_i18n/locales/validate.js +3 -3
  275. package/framework/modules/chui_json/chui_json.js +11 -11
  276. package/framework/modules/chui_layout/chui_layout.js +12 -10
  277. package/framework/modules/chui_logger/chui_logger.js +47 -28
  278. package/framework/modules/chui_page/page.js +39 -33
  279. package/framework/modules/chui_palette/palette.js +61 -0
  280. package/framework/modules/chui_qr/chui_qr.js +179 -88
  281. package/framework/modules/chui_scrollbar/chui_scrollbar.js +79 -68
  282. package/framework/modules/chui_scrollbar/styles.css +4 -2
  283. package/framework/modules/chui_services/chui_channels.js +27 -13
  284. package/framework/modules/chui_services/chui_services.js +47 -39
  285. package/framework/modules/chui_services/desktop.js +108 -101
  286. package/framework/modules/chui_sortable/chui_sortable.js +33 -28
  287. package/framework/modules/chui_validate/chui_validate.js +10 -7
  288. package/framework/tray_bar/chui_menu_items.js +136 -46
  289. package/index.d.ts +5046 -0
  290. package/index.js +199 -1254
  291. package/package.json +81 -68
  292. package/framework/components/telegram_bot/chui_telegram_bot.js +0 -184
@@ -0,0 +1,173 @@
1
+ /* ── UserListBar — список разговоров ───────────────────────────
2
+ Левая колонка мессенджера: строки разговоров с человеком, последней строкой,
3
+ временем и счётчиком непрочитанных. Список не полагает, где он стоит: в карточке
4
+ он читается кромкой той же карточки, а отдельной колонкой — своей поверхностью. */
5
+
6
+ chui_user_list_bar {
7
+ display: flex;
8
+ flex-direction: column;
9
+ box-sizing: border-box;
10
+ width: 300px;
11
+ height: max-content;
12
+ margin: var(--block_margin);
13
+ overflow: hidden;
14
+ border-radius: var(--radius_sm);
15
+ background: var(--element_background);
16
+ border: 1px solid var(--border_color);
17
+ font-size: var(--font_default_size);
18
+ color: var(--text_color);
19
+ }
20
+
21
+ /* Поле поиска: своя строка над списком, как в любом мессенджере */
22
+ user_list_bar_search {
23
+ display: flex;
24
+ flex-direction: row;
25
+ align-items: center;
26
+ gap: 6px;
27
+ padding: var(--control_padding) var(--block_padding);
28
+ border-bottom: 1px solid var(--border_color);
29
+ }
30
+
31
+ user_list_bar_search_icon {
32
+ display: flex;
33
+ align-items: center;
34
+ color: var(--text_color_disabled);
35
+ }
36
+
37
+ user_list_bar_search_icon chui_icon {
38
+ color: inherit;
39
+ }
40
+
41
+ .user_list_bar_field {
42
+ flex: 1 1 auto;
43
+ min-width: 0;
44
+ box-sizing: border-box;
45
+ padding: var(--control_padding_sm) 10px;
46
+ border-radius: var(--radius_sm);
47
+ border: 1px solid var(--border_color);
48
+ background: var(--element_background_2);
49
+ color: var(--text_color);
50
+ font-family: inherit;
51
+ font-size: var(--font_default_size);
52
+ }
53
+
54
+ .user_list_bar_field::placeholder {
55
+ color: var(--text_color_disabled);
56
+ }
57
+
58
+ /* Активное поле выдаёт кромка, а не кольцо: поиск по разговорам — то же поле,
59
+ что панель ввода, и ведёт себя так же */
60
+ .user_list_bar_field:focus {
61
+ outline: none;
62
+ border-color: var(--blue_prime_background);
63
+ }
64
+
65
+ /* Список прокручивается сам: страница под ним не растёт */
66
+ user_list_bar_list {
67
+ display: flex;
68
+ flex-direction: column;
69
+ flex: 1 1 auto;
70
+ min-height: 0;
71
+ box-sizing: border-box;
72
+ height: var(--user_list_bar_height, 320px);
73
+ padding: var(--control_padding_sm);
74
+ overflow-y: auto;
75
+ overflow-x: hidden;
76
+ outline: none;
77
+ }
78
+
79
+ user_list_bar_item {
80
+ display: flex;
81
+ flex-direction: row;
82
+ align-items: center;
83
+ gap: var(--control_padding);
84
+ flex: none;
85
+ box-sizing: border-box;
86
+ padding: var(--control_padding) 6px;
87
+ border-radius: var(--radius_sm);
88
+ cursor: pointer;
89
+ }
90
+
91
+ /* Скрытая строка не показывается: авторский display перебил бы UA-правило [hidden] */
92
+ user_list_bar_item[hidden] {
93
+ display: none;
94
+ }
95
+
96
+ user_list_bar_item:hover {
97
+ background: var(--element_background_hover);
98
+ }
99
+
100
+ /* Выбранный разговор: акцентная подложка — по ней видно, о ком идёт переписка справа */
101
+ user_list_bar_item[data-selected="true"] {
102
+ background: var(--blue_prime_background_tint);
103
+ }
104
+
105
+ user_list_bar_texts {
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 2px;
109
+ flex: 1 1 auto;
110
+ min-width: 0;
111
+ }
112
+
113
+ user_list_bar_name {
114
+ display: block;
115
+ overflow: hidden;
116
+ font-weight: 600;
117
+ color: var(--text_color);
118
+ white-space: nowrap;
119
+ text-overflow: ellipsis;
120
+ }
121
+
122
+ user_list_bar_preview {
123
+ display: block;
124
+ overflow: hidden;
125
+ color: var(--text_color_disabled);
126
+ font-size: var(--font_labels_size);
127
+ white-space: nowrap;
128
+ text-overflow: ellipsis;
129
+ }
130
+
131
+ user_list_bar_preview[hidden],
132
+ user_list_bar_time[hidden],
133
+ user_list_bar_unread[hidden],
134
+ user_list_bar_empty[hidden] {
135
+ display: none;
136
+ }
137
+
138
+ user_list_bar_meta {
139
+ display: flex;
140
+ flex-direction: column;
141
+ align-items: flex-end;
142
+ gap: 4px;
143
+ flex: none;
144
+ }
145
+
146
+ user_list_bar_time {
147
+ color: var(--text_color_disabled);
148
+ font-size: var(--font_labels_size);
149
+ white-space: nowrap;
150
+ }
151
+
152
+ /* Счётчик непрочитанных: акцентом, чтобы строка читалась на бегу */
153
+ user_list_bar_unread {
154
+ box-sizing: border-box;
155
+ min-width: 18px;
156
+ padding-inline: 5px;
157
+ border-radius: var(--radius_pill);
158
+ background: var(--blue_prime_background);
159
+ color: var(--text_color_hover);
160
+ font-size: 10px;
161
+ font-weight: 600;
162
+ line-height: 18px;
163
+ text-align: center;
164
+ }
165
+
166
+ /* Заглушка: когда разговоров нет и когда поиск ничего не нашёл */
167
+ user_list_bar_empty {
168
+ margin: auto;
169
+ padding: var(--control_padding);
170
+ color: var(--text_color_disabled);
171
+ font-size: var(--font_labels_size);
172
+ text-align: center;
173
+ }
@@ -0,0 +1,485 @@
1
+ const { Icon, Icons } = require("../chui_icons/icons");
2
+ const { Avatar } = require("../chui_avatar/avatar");
3
+ const { ContextMenu } = require("../chui_context_menu/context_menu");
4
+ const { I18n } = require("../../modules/chui_i18n/chui_i18n");
5
+ const { makeKeyboardButton, RovingKeyboard } = require("../../modules/chui_a11y/chui_a11y");
6
+ const { optionOr, setStyles, getDayKey } = require("../../modules/chui_functions");
7
+
8
+ /** Высота списка по умолчанию: список прокручивается сам, а страница под ним не растёт */
9
+ const DEFAULT_HEIGHT = "320px";
10
+
11
+ /**
12
+ * Список разговоров — левая колонка мессенджера: кто, о чём и когда писал последний раз.
13
+ *
14
+ * Четвёртая часть к `UserBar`, `MessageBar` и `WriteBar`: слева список разговоров, справа
15
+ * переписка — полоса пользователя сверху, лента сообщений в середине, панель ввода снизу.
16
+ * Список самостоятельный: `onSelect` отдаёт выбранную строку, а что показать справа, решает
17
+ * приложение.
18
+ *
19
+ * Строка показывает человека (аватар с точкой состояния), имя, последнюю строку разговора,
20
+ * время и число непрочитанных. Время принимает и число (дата), и готовую строку: дата сама
21
+ * превращается в «14:20» за сегодня, «Вчера» за вчера и короткую дату дальше — приложению
22
+ * не нужно держать формат у себя. `updateItem(id, {preview, time, unread})` правит строку
23
+ * на месте: в списке разговоров последняя строка меняется постоянно, и пересборка сдвигала бы
24
+ * прокрутку.
25
+ *
26
+ * Доступность: список — `role="listbox"` с именем (опция `label`, по умолчанию «Разговоры»),
27
+ * строки — `role="option"` с `aria-selected`. У списка одна остановка Tab, стрелки водят
28
+ * клавиатуру по строкам, Home/End — края, выбор идёт по клику и по Enter/Space. Имя строки
29
+ * собирается целиком — человек, последняя строка, время и число непрочитанных: на глаз строка
30
+ * состоит из кусочков, а скринридеру нужна фраза.
31
+ *
32
+ * ```javascript
33
+ * const list = new UserListBar({
34
+ * items: [
35
+ * { id: 'ivan', name: 'Иван Петров', preview: 'Скинь отчёт', time: Date.now(), unread: 2 },
36
+ * { id: 'maria', name: 'Мария', preview: 'Договорились', time: Date.now() - 90000000 },
37
+ * ],
38
+ * selected: 'ivan',
39
+ * search: true,
40
+ * onSelect: (item) => openDialog(item.id),
41
+ * });
42
+ * mount(list); // или body.appendChild(list.set())
43
+ * ```
44
+ *
45
+ * Опции: `id`, `items`, `selected`, `search` (`true` или своя подсказка), `searchPlaceholder`,
46
+ * `label`, `empty`, `height`, `menu`, `onSelect`.
47
+ * Строка: `{ id, name, preview, time, unread, status, photo }`; фабрика — `UserListBar.Item(...)`,
48
+ * состояния — `UserListBar.STATUS` (те же, что у `Avatar`).
49
+ * Действие меню строки: `{ id, icon, label, danger, disabled, onClick(action, item) }`.
50
+ * API: `addItem(item)`, `updateItem(id, patch)`, `removeItem(id)`, `setItems(items)`,
51
+ * `getItems()`, `getVisible()`, `select(id)`, `getSelected()`, `filter(text)`, `setHeight(value)`,
52
+ * `setEmpty(text)`, `setLabel(text)`, `setMenu(menu)`, `openMenu(target)`, `closeMenu()`,
53
+ * `getMenu()`, `getList()`, `getSearch()`, `set()`, `destroy()`.
54
+ *
55
+ * **Меню строки.** `menu` — список действий или функция `(item) => действия`: правый щелчок
56
+ * по строке (и клавиша «Меню», и Shift+F10 на ней же) открывает меню разговора. Строитель
57
+ * получает строку, поэтому подпись может зависеть от её состояния («Отметить прочитанным»
58
+ * есть только у строки с непрочитанными), а `onClick(action, item)` знает и действие, и строку.
59
+ */
60
+ class UserListBar {
61
+ static STATUS = Avatar.STATUS;
62
+ /** Фабрика строки: `UserListBar.Item({ id, name, preview, time, unread, status, photo })` */
63
+ static Item = (options = {}) => Object.assign({}, options);
64
+ static #counter = 0;
65
+
66
+ #id = undefined;
67
+ #root = document.createElement("chui_user_list_bar");
68
+ #searchBox = document.createElement("user_list_bar_search");
69
+ #input = document.createElement("input");
70
+ #list = document.createElement("user_list_bar_list");
71
+ #empty = document.createElement("user_list_bar_empty");
72
+ #items = [];
73
+ #rows = new Map();
74
+ #selected = undefined;
75
+ #label = undefined;
76
+ #emptyOption = undefined;
77
+ #onSelect = undefined;
78
+ #menu = undefined;
79
+ #menuSpec = undefined;
80
+ #roving = undefined;
81
+ #query = "";
82
+
83
+ constructor(
84
+ options = {
85
+ id: String(),
86
+ items: [],
87
+ selected: String(),
88
+ search: Boolean(),
89
+ searchPlaceholder: String(),
90
+ label: String(),
91
+ empty: String(),
92
+ height: String(),
93
+ onSelect: undefined,
94
+ }
95
+ ) {
96
+ setStyles(__dirname + "/styles.css", "chUiJS_UserListBar");
97
+ UserListBar.#counter += 1;
98
+ this.#id = options.id === undefined ? `chui_user_list_bar_${UserListBar.#counter}` : options.id;
99
+ this.#root.id = this.#id;
100
+ this.#label = options.label;
101
+ this.#emptyOption = options.empty;
102
+ this.#onSelect = typeof options.onSelect === "function" ? options.onSelect : undefined;
103
+ this.setHeight(options.height);
104
+
105
+ this.#list.id = `${this.#id}_list`;
106
+ this.#list.setAttribute("role", "listbox");
107
+ this.#list.setAttribute("aria-label", this.#labelText());
108
+ // Заглушка объявляет себя: во время поиска «ничего не найдено» надо услышать
109
+ this.#empty.setAttribute("aria-live", "polite");
110
+ this.#empty.innerText = this.#emptyText();
111
+ this.#list.appendChild(this.#empty);
112
+
113
+ // Одна остановка Tab на список: стрелки водят фокус, Home/End — края
114
+ this.#roving = new RovingKeyboard(this.#list, { itemSelector: "user_list_bar_item" });
115
+
116
+ if (optionOr(options.search, false) !== false) this.#buildSearch(options.searchPlaceholder, options.search);
117
+ if (options.menu !== undefined) this.setMenu(options.menu);
118
+ this.#root.appendChild(this.#list);
119
+
120
+ this.setItems(options.items === undefined ? [] : options.items);
121
+ if (options.selected !== undefined) this.select(options.selected);
122
+ }
123
+
124
+ // GET
125
+ getId() {
126
+ return this.#id;
127
+ }
128
+ /** Копии строк: правка копии не меняет список */
129
+ getItems() {
130
+ return this.#items.map((item) => Object.assign({}, item));
131
+ }
132
+ getSelected() {
133
+ return this.#selected;
134
+ }
135
+ getList() {
136
+ return this.#list;
137
+ }
138
+ /** Видимые строки в порядке списка — те, что прошли фильтр */
139
+ getVisible() {
140
+ return [...this.#rows.values()].filter((row) => row.node.hidden === false).map((row) => row.item);
141
+ }
142
+ /** Поле поиска — для того, кто хочет повесить на него своё; `undefined`, если поиска нет */
143
+ getSearch() {
144
+ return this.#input.parentNode === null ? undefined : this.#input;
145
+ }
146
+
147
+ // SET
148
+ /** Высота списка: своя переменная, поэтому прокручивается список, а не страница */
149
+ setHeight(value = String()) {
150
+ this.#root.style.setProperty("--user_list_bar_height", optionOr(value, undefined) ?? DEFAULT_HEIGHT);
151
+ return this;
152
+ }
153
+ /** Имя списка для скринридера: по умолчанию — общая подпись из словаря */
154
+ setLabel(text = String()) {
155
+ this.#label = text;
156
+ this.#list.setAttribute("aria-label", this.#labelText());
157
+ return this;
158
+ }
159
+ /** Заглушка пустого списка */
160
+ setEmpty(text = String()) {
161
+ this.#emptyOption = text;
162
+ this.#syncEmpty();
163
+ return this;
164
+ }
165
+
166
+ // СТРОКИ
167
+ /** Полная замена списка: строки собираются заново, выбор и прокрутка сбрасываются */
168
+ setItems(items = []) {
169
+ this.#items = [];
170
+ if (this.#menu !== undefined) for (const row of this.#rows.values()) this.#menu.detach(row.node);
171
+ this.#rows = new Map();
172
+ this.#selected = undefined;
173
+ for (const row of this.#list.querySelectorAll("user_list_bar_item")) row.remove();
174
+ for (const item of items) this.#add(item, false);
175
+ this.#applyFilter();
176
+ this.#syncEmpty();
177
+ this.#roving.refresh();
178
+ return this;
179
+ }
180
+ /** Новая строка: возвращается сама строка — её `id` нужен для `updateItem()` */
181
+ addItem(item = {}) {
182
+ const added = this.#add(item, true);
183
+ this.#applyFilter();
184
+ this.#syncEmpty();
185
+ this.#roving.refresh();
186
+ return added;
187
+ }
188
+ /**
189
+ * Правка строки по `id`: последняя строка, время, счётчик непрочитанных, имя, состояние.
190
+ * Строка меняется на месте — пересборка сдвинула бы прокрутку списка
191
+ */
192
+ updateItem(id = String(), patch = {}) {
193
+ const row = this.#rows.get(String(id));
194
+ if (row === undefined) return this;
195
+ Object.assign(row.item, patch);
196
+ this.#applyRow(row);
197
+ this.#applyFilter();
198
+ this.#syncEmpty();
199
+ return this;
200
+ }
201
+ removeItem(id = String()) {
202
+ const row = this.#rows.get(String(id));
203
+ if (row === undefined) return this;
204
+ this.#items.splice(this.#items.indexOf(row.item), 1);
205
+ if (this.#menu !== undefined) this.#menu.detach(row.node);
206
+ row.node.remove();
207
+ this.#rows.delete(String(id));
208
+ if (this.#selected === String(id)) this.#selected = undefined;
209
+ this.#applyFilter();
210
+ this.#syncEmpty();
211
+ this.#roving.refresh();
212
+ return this;
213
+ }
214
+ clear() {
215
+ return this.setItems([]);
216
+ }
217
+ /** Выбор строки: клик, Enter/Space и `onSelect` идут одним путём */
218
+ select(id = String()) {
219
+ const row = this.#rows.get(String(id));
220
+ if (row === undefined) return this;
221
+ const previous = this.#selected;
222
+ this.#selected = String(id);
223
+ for (const [key, entry] of this.#rows) {
224
+ const selected = key === this.#selected;
225
+ entry.node.setAttribute("aria-selected", selected ? "true" : "false");
226
+ entry.node.dataset.selected = selected ? "true" : "false";
227
+ }
228
+ if (previous !== this.#selected && this.#onSelect !== undefined) this.#onSelect(row.item, this);
229
+ return this;
230
+ }
231
+ /** Фильтр по имени и последней строке: то же, что вводит человек в поле поиска */
232
+ filter(text = String()) {
233
+ this.#query = String(text).trim().toLowerCase();
234
+ if (this.#input.parentNode !== null && this.#input.value !== text) this.#input.value = text;
235
+ this.#applyFilter();
236
+ this.#syncEmpty();
237
+ return this;
238
+ }
239
+
240
+ // МЕНЮ СТРОКИ
241
+ /**
242
+ * Меню строки: правый щелчок по строке (и клавиша «Меню», и Shift+F10 на ней же) открывает
243
+ * меню разговора. `menu` — список действий или функция `(item) => действия`; строитель
244
+ * получает строку, поэтому подпись может зависеть от её состояния. Готовое контекстное меню
245
+ * тоже принимается — тогда его пункты правит тот, кто его собрал
246
+ */
247
+ setMenu(menu = undefined) {
248
+ if (this.#menu !== undefined) this.#menu.destroy();
249
+ this.#menu = undefined;
250
+ this.#menuSpec = undefined;
251
+ for (const row of this.#rows.values()) row.node.removeAttribute("data-menu");
252
+ if (menu === undefined || menu === null) return this;
253
+ if (menu instanceof ContextMenu) {
254
+ this.#menu = menu;
255
+ } else {
256
+ this.#menuSpec = menu;
257
+ this.#menu = new ContextMenu();
258
+ // Меню строк — слой с ролью меню: имя ему нужно, иначе читается просто списком
259
+ this.#menu.setLabel(I18n.t("feedback.actionsBar"));
260
+ }
261
+ for (const row of this.#rows.values()) this.#attachMenu(row);
262
+ return this;
263
+ }
264
+ /** Контекстное меню списка — для того, кто хочет повесить на него своё */
265
+ getMenu() {
266
+ return this.#menu;
267
+ }
268
+ /** Открыть меню строки из кода: по `id` или без аргумента — у выбранной строки */
269
+ openMenu(target = undefined) {
270
+ if (this.#menu === undefined) return this;
271
+ const id = target === undefined ? this.#selected : typeof target === "string" ? target : target.id;
272
+ const row = this.#rows.get(String(id));
273
+ if (row === undefined) return this;
274
+ if (this.#prepareMenu(row.item) === 0) return this;
275
+ const rect = row.node.getBoundingClientRect();
276
+ this.#menu.show(rect.left + 8, rect.top + rect.height / 2);
277
+ return this;
278
+ }
279
+ closeMenu() {
280
+ if (this.#menu !== undefined) this.#menu.hide();
281
+ return this;
282
+ }
283
+
284
+ set() {
285
+ return this.#root;
286
+ }
287
+ destroy() {
288
+ if (this.#menu !== undefined) this.#menu.destroy();
289
+ this.#menu = undefined;
290
+ this.#menuSpec = undefined;
291
+ this.#items = [];
292
+ this.#rows = new Map();
293
+ this.#onSelect = undefined;
294
+ this.#root.remove();
295
+ }
296
+
297
+ // === ВНУТРЕННЕЕ ===
298
+ /** Поле поиска: настоящий `input` с типом поиска — очистку и ввод даёт браузер */
299
+ #buildSearch(placeholder, option) {
300
+ this.#input.type = "search";
301
+ this.#input.className = "user_list_bar_field";
302
+ const text =
303
+ optionOr(placeholder, undefined) === undefined
304
+ ? typeof option === "string"
305
+ ? option
306
+ : I18n.t("feedback.userListBarSearch")
307
+ : String(placeholder);
308
+ this.#input.placeholder = text;
309
+ this.#input.setAttribute("aria-label", text);
310
+ const glyph = document.createElement("user_list_bar_search_icon");
311
+ glyph.setAttribute("aria-hidden", "true");
312
+ glyph.innerHTML = new Icon(Icons.ACTIONS.SEARCH, "16px").getHTML();
313
+ this.#input.addEventListener("input", this.#onSearch);
314
+ this.#searchBox.append(glyph, this.#input);
315
+ this.#root.appendChild(this.#searchBox);
316
+ }
317
+ /**
318
+ * Меню строки: свои слушатели идут первыми — они собирают пункты по строке и могут не дать
319
+ * меню открыться, если для неё действий нет (`stopImmediatePropagation` глушит слушатели
320
+ * `ContextMenu`, которые живут на том же узле)
321
+ */
322
+ #attachMenu(row) {
323
+ const prepare = (event) => {
324
+ if (this.#prepareMenu(row.item) === 0) {
325
+ event.preventDefault();
326
+ event.stopImmediatePropagation();
327
+ }
328
+ };
329
+ row.node.dataset.menu = "true";
330
+ row.node.addEventListener("contextmenu", prepare);
331
+ row.node.addEventListener("keydown", (event) => {
332
+ if (event.key !== "ContextMenu" && !(event.shiftKey === true && event.key === "F10")) return;
333
+ prepare(event);
334
+ });
335
+ this.#menu.attach(row.node);
336
+ }
337
+ /** Пункты для строки: список действий или строитель; возвращается их число */
338
+ #prepareMenu(item) {
339
+ if (this.#menuSpec === undefined) return this.#menu.set().querySelectorAll("context_menu_item").length;
340
+ const source = typeof this.#menuSpec === "function" ? this.#menuSpec(item) : this.#menuSpec;
341
+ const actions = Array.isArray(source) ? source : [];
342
+ this.#menu.setItems(
343
+ actions.map((action) =>
344
+ ContextMenu.Item({
345
+ title: action.label === undefined ? "" : String(action.label),
346
+ icon: action.icon,
347
+ danger: action.danger === true,
348
+ disabled: action.disabled === true,
349
+ // Действие знает и себя, и строку: с ней его обработчик и работает
350
+ clickEvent: () => {
351
+ if (typeof action.onClick === "function") action.onClick(action, item);
352
+ },
353
+ })
354
+ )
355
+ );
356
+ return actions.length;
357
+ }
358
+ #labelText() {
359
+ return optionOr(this.#label, undefined) === undefined ? I18n.t("feedback.userListBar") : String(this.#label);
360
+ }
361
+ #emptyText() {
362
+ return optionOr(this.#emptyOption, undefined) === undefined
363
+ ? I18n.t("feedback.userListBarEmpty")
364
+ : String(this.#emptyOption);
365
+ }
366
+ /** Новая строка: сборка разметки и запоминание её частей — по ним строка и правится */
367
+ #add(item = {}, live = true) {
368
+ const entry = UserListBar.Item(item);
369
+ if (entry.id === undefined) entry.id = require("randomstring").generate();
370
+ entry.id = String(entry.id);
371
+ const node = document.createElement("user_list_bar_item");
372
+ node.dataset.id = entry.id;
373
+ node.setAttribute("role", "option");
374
+ node.setAttribute("aria-selected", "false");
375
+ // Один путь для мыши и клавиатуры: Enter/Space кликает строку
376
+ makeKeyboardButton(node, "option");
377
+ node.addEventListener("click", () => this.select(entry.id));
378
+
379
+ const parts = {};
380
+ parts.avatar = new Avatar({
381
+ name: entry.name,
382
+ image: entry.photo === undefined ? undefined : { path: entry.photo },
383
+ status: entry.status,
384
+ size: Avatar.SIZE.DEFAULT,
385
+ decorative: true,
386
+ });
387
+ parts.texts = document.createElement("user_list_bar_texts");
388
+ parts.name = document.createElement("user_list_bar_name");
389
+ parts.preview = document.createElement("user_list_bar_preview");
390
+ parts.texts.append(parts.name, parts.preview);
391
+ parts.meta = document.createElement("user_list_bar_meta");
392
+ parts.time = document.createElement("user_list_bar_time");
393
+ parts.unread = document.createElement("user_list_bar_unread");
394
+ // Счётчик виден, но скринридеру он попадает в имя строки — второй раз не читается
395
+ parts.unread.setAttribute("aria-hidden", "true");
396
+ parts.meta.append(parts.time, parts.unread);
397
+ node.append(parts.avatar.set(), parts.texts, parts.meta);
398
+
399
+ const row = { node: node, item: entry, parts: parts };
400
+ this.#items.push(entry);
401
+ this.#rows.set(entry.id, row);
402
+ this.#list.appendChild(node);
403
+ this.#applyRow(row);
404
+ if (this.#menu !== undefined) this.#attachMenu(row);
405
+ if (live === true) {
406
+ this.#applyFilter();
407
+ this.#syncEmpty();
408
+ this.#roving.refresh();
409
+ }
410
+ return entry;
411
+ }
412
+ /** Заполнить строку по данным: текст, время, счётчик и имя для скринридера */
413
+ #applyRow(row) {
414
+ const item = row.item;
415
+ const parts = row.parts;
416
+ parts.name.innerText = item.name === undefined ? "" : String(item.name);
417
+ parts.preview.innerText = item.preview === undefined ? "" : String(item.preview);
418
+ parts.preview.hidden = parts.preview.innerText === "";
419
+ const time = this.#timeLabel(item.time);
420
+ parts.time.innerText = time;
421
+ parts.time.hidden = time === "";
422
+ const unread = Number(item.unread);
423
+ const shown = Number.isFinite(unread) && unread > 0 ? String(unread) : "";
424
+ parts.unread.innerText = shown;
425
+ parts.unread.hidden = shown === "";
426
+ parts.avatar.setName(parts.name.innerText);
427
+ parts.avatar.setStatus(item.status);
428
+ row.node.dataset.unread = shown === "" ? "false" : "true";
429
+ row.node.setAttribute("aria-label", this.#rowLabel(item, time, shown));
430
+ }
431
+ /**
432
+ * Имя строки для скринридера: на глаз строка состоит из кусочков, а читать её надо фразой —
433
+ * «Иван Петров, Скинь отчёт, 14:20, непрочитанных: 2»
434
+ */
435
+ #rowLabel(item, time, unread) {
436
+ const parts = [item.name === undefined ? "" : String(item.name)];
437
+ if (item.preview !== undefined && String(item.preview) !== "") parts.push(String(item.preview));
438
+ if (time !== "") parts.push(time);
439
+ if (unread !== "") parts.push(I18n.t("feedback.userListBarUnread", { value: unread }));
440
+ return parts.filter(Boolean).join(", ");
441
+ }
442
+ /**
443
+ * Время разговора: число (дата) превращается в «14:20» за сегодня, «Вчера» за вчера
444
+ * и короткую дату дальше; готовая строка остаётся как есть
445
+ */
446
+ #timeLabel(value) {
447
+ if (value === undefined || value === null || value === "") return "";
448
+ if (typeof value === "string") return value;
449
+ const date = value instanceof Date ? value : new Date(value);
450
+ if (isNaN(date.getTime())) return "";
451
+ const now = new Date();
452
+ const day = (entry) => getDayKey(entry);
453
+ if (day(date) === day(now)) return this.#clock(date);
454
+ const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
455
+ if (day(date) === day(yesterday)) return I18n.t("common.yesterday");
456
+ return new Intl.DateTimeFormat(I18n.getLocale(), { day: "numeric", month: "short" }).format(date);
457
+ }
458
+ #clock(date) {
459
+ return new Intl.DateTimeFormat(I18n.getLocale(), { hour: "2-digit", minute: "2-digit" }).format(date);
460
+ }
461
+ /** Фильтр: строки прячутся атрибутом, а не снятием из разметки — так их помнит клавиатура */
462
+ #applyFilter() {
463
+ for (const row of this.#rows.values()) {
464
+ const haystack = `${row.item.name === undefined ? "" : row.item.name} ${
465
+ row.item.preview === undefined ? "" : row.item.preview
466
+ }`.toLowerCase();
467
+ row.node.hidden = this.#query !== "" && haystack.includes(this.#query) === false;
468
+ }
469
+ this.#roving.refresh();
470
+ }
471
+ /** Заглушка: своя подпись, когда список пуст, и «ничего не найдено», когда пусто от фильтра */
472
+ #syncEmpty() {
473
+ const total = this.#rows.size;
474
+ let visible = 0;
475
+ for (const row of this.#rows.values()) if (row.node.hidden === false) visible += 1;
476
+ this.#empty.hidden = visible > 0;
477
+ this.#empty.innerText =
478
+ total > 0 && this.#query !== "" ? I18n.t("feedback.userListBarNoMatch") : this.#emptyText();
479
+ }
480
+ #onSearch = () => {
481
+ this.filter(this.#input.value);
482
+ };
483
+ }
484
+
485
+ exports.UserListBar = UserListBar;