chuijs 4.0.3 → 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 (258) hide show
  1. package/CHANGELOG.md +536 -0
  2. package/framework/appLayout/default/chui_app_layout.js +361 -303
  3. package/framework/appLayout/default/global_style.css +243 -89
  4. package/framework/appLayout/default/main_theme_style.css +188 -101
  5. package/framework/components/chui_accordion/accordion.js +17 -17
  6. package/framework/components/chui_accordion/styles.css +7 -7
  7. package/framework/components/chui_action_sheet/action_sheet.js +35 -32
  8. package/framework/components/chui_action_sheet/styles.css +10 -4
  9. package/framework/components/chui_actions_bar/actions_bar.js +338 -0
  10. package/framework/components/chui_actions_bar/styles.css +139 -0
  11. package/framework/components/chui_affix/affix.js +4 -4
  12. package/framework/components/chui_affix/styles.css +5 -0
  13. package/framework/components/chui_alert/alert.js +11 -5
  14. package/framework/components/chui_alert/styles.css +7 -0
  15. package/framework/components/chui_avatar/avatar.js +13 -13
  16. package/framework/components/chui_avatar/styles.css +1 -1
  17. package/framework/components/chui_avatar_group/avatar_group.js +4 -4
  18. package/framework/components/chui_avatar_group/styles.css +1 -1
  19. package/framework/components/chui_backtop/backtop.js +25 -2
  20. package/framework/components/chui_backtop/styles.css +21 -0
  21. package/framework/components/chui_badge/badge.js +15 -15
  22. package/framework/components/chui_badge/styles.css +6 -6
  23. package/framework/components/chui_banner/banner.js +222 -0
  24. package/framework/components/chui_banner/styles.css +170 -0
  25. package/framework/components/chui_breadcrumb/styles.css +1 -1
  26. package/framework/components/chui_button/button.js +24 -28
  27. package/framework/components/chui_button/styles.css +5 -5
  28. package/framework/components/chui_calendar/calendar.js +6 -6
  29. package/framework/components/chui_card/card.js +14 -14
  30. package/framework/components/chui_card_scroll/card_scroll.js +169 -0
  31. package/framework/components/chui_card_scroll/styles.css +86 -0
  32. package/framework/components/chui_chat/chat.js +73 -51
  33. package/framework/components/chui_chat/styles.css +18 -7
  34. package/framework/components/chui_clipboard_history/clipboard_history.js +25 -20
  35. package/framework/components/chui_clipboard_history/styles.css +10 -8
  36. package/framework/components/chui_code/code.js +8 -8
  37. package/framework/components/chui_code/styles.css +2 -2
  38. package/framework/components/chui_code_editor/code_editor.js +26 -26
  39. package/framework/components/chui_code_editor/styles.css +3 -3
  40. package/framework/components/chui_command_palette/command_palette.js +29 -29
  41. package/framework/components/chui_command_palette/styles.css +5 -5
  42. package/framework/components/chui_console/console.js +9 -9
  43. package/framework/components/chui_console/styles.css +5 -2
  44. package/framework/components/chui_content_block/content_block.js +2 -2
  45. package/framework/components/chui_context_menu/context_menu.js +162 -54
  46. package/framework/components/chui_context_menu/styles.css +82 -8
  47. package/framework/components/chui_custom_element/custom_element.js +11 -11
  48. package/framework/components/chui_data_grid/data_grid.js +268 -268
  49. package/framework/components/chui_data_grid/styles.css +14 -14
  50. package/framework/components/chui_descriptions/styles.css +1 -1
  51. package/framework/components/chui_details/details.js +12 -12
  52. package/framework/components/chui_details/styles.css +7 -7
  53. package/framework/components/chui_diff_viewer/diff_viewer.js +88 -88
  54. package/framework/components/chui_diff_viewer/styles.css +9 -9
  55. package/framework/components/chui_divider/divider.js +6 -6
  56. package/framework/components/chui_divider/styles.css +2 -2
  57. package/framework/components/chui_drawer/drawer.js +37 -14
  58. package/framework/components/chui_drawer/styles.css +18 -4
  59. package/framework/components/chui_drop_zone/drop_zone.js +6 -6
  60. package/framework/components/chui_drop_zone/styles.css +10 -6
  61. package/framework/components/chui_fieldset/fieldset.js +6 -6
  62. package/framework/components/chui_fieldset/styles.css +5 -5
  63. package/framework/components/chui_file_explorer/file_explorer.js +115 -115
  64. package/framework/components/chui_file_explorer/styles.css +6 -6
  65. package/framework/components/chui_form/form.js +11 -11
  66. package/framework/components/chui_gantt/gantt.js +41 -41
  67. package/framework/components/chui_gantt/styles.css +5 -5
  68. package/framework/components/chui_graphs/graphs.js +82 -46
  69. package/framework/components/chui_graphs/styles.css +26 -5
  70. package/framework/components/chui_hero/hero.js +63 -20
  71. package/framework/components/chui_hero/styles.css +176 -7
  72. package/framework/components/chui_html_block/html_block.js +5 -5
  73. package/framework/components/chui_html_block/styles.css +1 -1
  74. package/framework/components/chui_hx/styles.css +2 -2
  75. package/framework/components/chui_icons/icons.js +7 -7
  76. package/framework/components/chui_icons/styles.css +2 -1
  77. package/framework/components/chui_inputs/chui_check_box/styles.css +12 -3
  78. package/framework/components/chui_inputs/chui_chips/chips.js +92 -31
  79. package/framework/components/chui_inputs/chui_chips/styles.css +28 -5
  80. package/framework/components/chui_inputs/chui_color_picker/color_picker.js +20 -15
  81. package/framework/components/chui_inputs/chui_color_picker/styles.css +7 -7
  82. package/framework/components/chui_inputs/chui_combo_box/combo_box.js +58 -58
  83. package/framework/components/chui_inputs/chui_combo_box/styles.css +9 -9
  84. package/framework/components/chui_inputs/chui_date/date.js +93 -95
  85. package/framework/components/chui_inputs/chui_date/styles.css +8 -8
  86. package/framework/components/chui_inputs/chui_date_range/date_range.js +22 -22
  87. package/framework/components/chui_inputs/chui_date_range/styles.css +3 -3
  88. package/framework/components/chui_inputs/chui_datetime/datetime.js +12 -12
  89. package/framework/components/chui_inputs/chui_datetime/styles.css +3 -3
  90. package/framework/components/chui_inputs/chui_email/email.js +10 -10
  91. package/framework/components/chui_inputs/chui_email/styles.css +6 -6
  92. package/framework/components/chui_inputs/chui_file/file.js +8 -8
  93. package/framework/components/chui_inputs/chui_file/styles.css +12 -9
  94. package/framework/components/chui_inputs/chui_hotkey/hotkey.js +39 -39
  95. package/framework/components/chui_inputs/chui_hotkey/styles.css +6 -6
  96. package/framework/components/chui_inputs/chui_masked/masked.js +44 -44
  97. package/framework/components/chui_inputs/chui_masked/styles.css +5 -5
  98. package/framework/components/chui_inputs/chui_mention/mention.js +13 -13
  99. package/framework/components/chui_inputs/chui_mention/styles.css +14 -8
  100. package/framework/components/chui_inputs/chui_multi_combo_box/multi_combo_box.js +147 -137
  101. package/framework/components/chui_inputs/chui_multi_combo_box/styles.css +14 -8
  102. package/framework/components/chui_inputs/chui_number/number.js +42 -45
  103. package/framework/components/chui_inputs/chui_number/styles.css +6 -6
  104. package/framework/components/chui_inputs/chui_otp/otp.js +32 -32
  105. package/framework/components/chui_inputs/chui_otp/styles.css +5 -5
  106. package/framework/components/chui_inputs/chui_password/password.js +30 -36
  107. package/framework/components/chui_inputs/chui_password/styles.css +7 -7
  108. package/framework/components/chui_inputs/chui_radio_button/styles.css +2 -2
  109. package/framework/components/chui_inputs/chui_rating/rating.js +28 -28
  110. package/framework/components/chui_inputs/chui_rating/styles.css +3 -3
  111. package/framework/components/chui_inputs/chui_select_box/select_box.js +54 -54
  112. package/framework/components/chui_inputs/chui_select_box/styles.css +10 -10
  113. package/framework/components/chui_inputs/chui_slider/slider.js +14 -14
  114. package/framework/components/chui_inputs/chui_slider/styles.css +5 -4
  115. package/framework/components/chui_inputs/chui_text/styles.css +7 -7
  116. package/framework/components/chui_inputs/chui_text/text.js +13 -17
  117. package/framework/components/chui_inputs/chui_text_area/styles.css +8 -8
  118. package/framework/components/chui_inputs/chui_text_area/text_area.js +23 -23
  119. package/framework/components/chui_inputs/chui_time/styles.css +13 -17
  120. package/framework/components/chui_inputs/chui_time/time.js +35 -24
  121. package/framework/components/chui_inputs/chui_toggle/styles.css +10 -3
  122. package/framework/components/chui_inputs/chui_toggle/toggle.js +16 -16
  123. package/framework/components/chui_json_editor/json_editor.js +87 -87
  124. package/framework/components/chui_json_editor/styles.css +5 -5
  125. package/framework/components/chui_json_viewer/json_viewer.js +80 -80
  126. package/framework/components/chui_json_viewer/styles.css +6 -6
  127. package/framework/components/chui_kanban/kanban.js +27 -27
  128. package/framework/components/chui_kanban/styles.css +8 -4
  129. package/framework/components/chui_kbd/styles.css +1 -1
  130. package/framework/components/chui_label/label.js +17 -17
  131. package/framework/components/chui_label/styles.css +1 -1
  132. package/framework/components/chui_link/link.js +9 -9
  133. package/framework/components/chui_link/styles.css +11 -4
  134. package/framework/components/chui_list/list.js +11 -11
  135. package/framework/components/chui_list/styles.css +2 -2
  136. package/framework/components/chui_markdown_editor/markdown_editor.js +66 -66
  137. package/framework/components/chui_markdown_editor/styles.css +5 -5
  138. package/framework/components/chui_media/audio.js +240 -246
  139. package/framework/components/chui_media/audio_styles.css +41 -23
  140. package/framework/components/chui_media/image.js +49 -46
  141. package/framework/components/chui_media/image_compare.js +35 -35
  142. package/framework/components/chui_media/image_compare_styles.css +5 -5
  143. package/framework/components/chui_media/image_styles.css +1 -1
  144. package/framework/components/chui_media/video.js +148 -148
  145. package/framework/components/chui_media/video_styles.css +27 -11
  146. package/framework/components/chui_menu_bar/menu_bar.js +54 -54
  147. package/framework/components/chui_menu_bar/styles.css +10 -10
  148. package/framework/components/chui_message_bar/message_bar.js +551 -0
  149. package/framework/components/chui_message_bar/styles.css +248 -0
  150. package/framework/components/chui_modal/modal.js +37 -13
  151. package/framework/components/chui_notification/notification.js +76 -54
  152. package/framework/components/chui_notification/notification_download.js +26 -81
  153. package/framework/components/chui_notification/notification_download_card.js +115 -0
  154. package/framework/components/chui_notification/notification_download_progress.css +2 -2
  155. package/framework/components/chui_notification/notification_download_progress.js +70 -108
  156. package/framework/components/chui_notification/notification_update.js +31 -31
  157. package/framework/components/chui_notification/status_plate.css +3 -3
  158. package/framework/components/chui_notification/styles.css +6 -1
  159. package/framework/components/chui_notification/styles_download.css +1 -1
  160. package/framework/components/chui_notification/styles_update.css +2 -2
  161. package/framework/components/chui_org_chart/org_chart.js +29 -29
  162. package/framework/components/chui_org_chart/styles.css +2 -2
  163. package/framework/components/chui_pagination/pagination.js +3 -3
  164. package/framework/components/chui_pagination/styles.css +1 -1
  165. package/framework/components/chui_paragraph/styles.css +2 -2
  166. package/framework/components/chui_popover/popover.js +29 -23
  167. package/framework/components/chui_popover/styles.css +1 -1
  168. package/framework/components/chui_popups/popups.js +124 -124
  169. package/framework/components/chui_popups/styles.css +5 -5
  170. package/framework/components/chui_print_button/print_button.js +15 -10
  171. package/framework/components/chui_progress_bar/progress_bar.js +34 -12
  172. package/framework/components/chui_progress_bar/styles.css +4 -4
  173. package/framework/components/chui_progress_ring/progress_ring.js +15 -15
  174. package/framework/components/chui_progress_ring/styles.css +1 -1
  175. package/framework/components/chui_qrcode/qrcode.js +12 -12
  176. package/framework/components/chui_query_builder/query_builder.js +46 -46
  177. package/framework/components/chui_query_builder/styles.css +4 -4
  178. package/framework/components/chui_radio_group/styles.css +1 -1
  179. package/framework/components/chui_result/result.js +15 -15
  180. package/framework/components/chui_result/styles.css +3 -3
  181. package/framework/components/chui_scheduler/scheduler.js +74 -74
  182. package/framework/components/chui_scheduler/styles.css +5 -5
  183. package/framework/components/chui_screen_spinner/screen_spinner.js +167 -0
  184. package/framework/components/chui_screen_spinner/styles.css +49 -0
  185. package/framework/components/chui_segmented_control/segmented_control.js +63 -6
  186. package/framework/components/chui_segmented_control/styles.css +32 -6
  187. package/framework/components/chui_settings/settings.js +45 -46
  188. package/framework/components/chui_settings/styles.css +4 -4
  189. package/framework/components/chui_shortcut_help/shortcut_help.js +32 -34
  190. package/framework/components/chui_sidebar_nav/sidebar_nav.js +98 -29
  191. package/framework/components/chui_sidebar_nav/styles.css +9 -3
  192. package/framework/components/chui_slideshow/slideshow.js +85 -85
  193. package/framework/components/chui_slideshow/styles.css +6 -6
  194. package/framework/components/chui_spinner/spinner.js +10 -10
  195. package/framework/components/chui_split_button/split_button.js +30 -12
  196. package/framework/components/chui_split_button/styles.css +21 -5
  197. package/framework/components/chui_splitter/splitter.js +11 -11
  198. package/framework/components/chui_splitter/styles.css +2 -2
  199. package/framework/components/chui_statistic/statistic.js +47 -47
  200. package/framework/components/chui_status_bar/styles.css +2 -2
  201. package/framework/components/chui_stepper/stepper.js +202 -31
  202. package/framework/components/chui_stepper/styles.css +79 -14
  203. package/framework/components/chui_sticky_bar/sticky_bar.js +44 -13
  204. package/framework/components/chui_sticky_bar/styles.css +10 -8
  205. package/framework/components/chui_tabs/chui_tabs.js +74 -26
  206. package/framework/components/chui_tabs/styles.css +40 -12
  207. package/framework/components/chui_text_editor/styles.css +7 -7
  208. package/framework/components/chui_text_editor/text_editor.js +56 -56
  209. package/framework/components/chui_text_editor/text_editor_panel.js +173 -174
  210. package/framework/components/chui_theme_toggle/theme_toggle.js +21 -5
  211. package/framework/components/chui_timeline/styles.css +3 -1
  212. package/framework/components/chui_toast_queue/styles.css +11 -11
  213. package/framework/components/chui_toast_queue/toast_queue.js +30 -27
  214. package/framework/components/chui_toolbar/styles.css +2 -2
  215. package/framework/components/chui_tooltip/styles.css +2 -2
  216. package/framework/components/chui_tooltip/tooltip.js +3 -3
  217. package/framework/components/chui_tour/styles.css +8 -8
  218. package/framework/components/chui_tour/tour.js +72 -72
  219. package/framework/components/chui_transfer/styles.css +6 -6
  220. package/framework/components/chui_transfer/transfer.js +93 -106
  221. package/framework/components/chui_tree_table/styles.css +4 -4
  222. package/framework/components/chui_tree_table/tree_table.js +45 -45
  223. package/framework/components/chui_tree_view/styles.css +10 -8
  224. package/framework/components/chui_tree_view/tree_view.js +185 -183
  225. package/framework/components/chui_user_bar/styles.css +145 -0
  226. package/framework/components/chui_user_bar/user_bar.js +402 -0
  227. package/framework/components/chui_user_list_bar/styles.css +173 -0
  228. package/framework/components/chui_user_list_bar/user_list_bar.js +485 -0
  229. package/framework/components/chui_watermark/watermark.js +17 -17
  230. package/framework/components/chui_webview/webview.js +9 -9
  231. package/framework/components/chui_write_bar/styles.css +172 -0
  232. package/framework/components/chui_write_bar/write_bar.js +320 -0
  233. package/framework/main_window.js +1234 -0
  234. package/framework/modules/chui_a11y/chui_a11y.js +78 -79
  235. package/framework/modules/chui_animations/animations.js +77 -15
  236. package/framework/modules/chui_animations/motion.js +120 -0
  237. package/framework/modules/chui_animations/styles.css +36 -3
  238. package/framework/modules/chui_functions.js +31 -27
  239. package/framework/modules/chui_highlight/styles.css +13 -10
  240. package/framework/modules/chui_i18n/chui_i18n.js +3 -3
  241. package/framework/modules/chui_i18n/locales/feedback.js +62 -0
  242. package/framework/modules/chui_i18n/locales/navigation.js +4 -0
  243. package/framework/modules/chui_json/chui_json.js +2 -2
  244. package/framework/modules/chui_logger/chui_logger.js +23 -8
  245. package/framework/modules/chui_page/page.js +10 -10
  246. package/framework/modules/chui_palette/palette.js +61 -0
  247. package/framework/modules/chui_qr/chui_qr.js +23 -23
  248. package/framework/modules/chui_scrollbar/chui_scrollbar.js +45 -45
  249. package/framework/modules/chui_scrollbar/styles.css +3 -3
  250. package/framework/modules/chui_services/chui_services.js +8 -8
  251. package/framework/modules/chui_services/desktop.js +77 -71
  252. package/framework/modules/chui_sortable/chui_sortable.js +17 -17
  253. package/framework/modules/chui_validate/chui_validate.js +3 -3
  254. package/framework/tray_bar/chui_menu_items.js +2 -2
  255. package/index.d.ts +266 -34
  256. package/index.js +20 -1168
  257. package/package.json +6 -4
  258. package/framework/components/telegram_bot/chui_telegram_bot.js +0 -191
@@ -0,0 +1,169 @@
1
+ const { Icon, Icons } = require("../chui_icons/icons");
2
+ const { I18n } = require("../../modules/chui_i18n/chui_i18n");
3
+ const { setStyles, optionOr } = require("../../modules/chui_functions");
4
+ const { reducedMotion } = require("../../modules/chui_animations/motion");
5
+
6
+ /** Зазор между карточками по умолчанию: тот же шаг, что у отступов компонентов */
7
+ const DEFAULT_GAP = 12;
8
+
9
+ /**
10
+ * Лента карточек — горизонтальный список, который прокручивается колесом, стрелками на краях
11
+ * и клавиатурой.
12
+ *
13
+ * От `SlideShow` отличается тем, что не показывает слайды по одному: карточки идут подряд,
14
+ * лента листается свободно, а прилипание к началу карточки лишь помогает остановиться ровно.
15
+ * От `List` — направлением: строки идут вбок и не переносятся.
16
+ *
17
+ * Опции: `id`, `gap`, `snap` (прилипание к карточке, по умолчанию включено), `arrows`
18
+ * (стрелки на краях, по умолчанию включены), `label` (имя списка для скринридера),
19
+ * `items` (карточки).
20
+ *
21
+ * Методы: `addCard(component)`, `getCards()`, `scrollToCard(index)`, `scrollToEnd()`,
22
+ * `destroy()`, `set()`. В ленту принимаются компоненты фреймворка (у них есть `set()`)
23
+ * и готовые узлы.
24
+ */
25
+ class CardScroll {
26
+ #root = document.createElement("chui_card_scroll");
27
+ #track = document.createElement("card_scroll_track");
28
+ #start = undefined;
29
+ #end = undefined;
30
+ #gap = DEFAULT_GAP;
31
+ #observer = undefined;
32
+
33
+ constructor(options = {}) {
34
+ setStyles(__dirname + "/styles.css", "chUiJS_CardScroll");
35
+ if (options.id !== undefined) this.#root.id = options.id;
36
+ const gap = Number(options.gap);
37
+ this.#gap = Number.isFinite(gap) && gap >= 0 ? gap : DEFAULT_GAP;
38
+ this.#track.style.gap = `${this.#gap}px`;
39
+ // Список, а не просто лента: скринридер объявляет число карточек, а каждая —
40
+ // свой `listitem` (роль ставит `addCard`)
41
+ this.#track.setAttribute("role", "list");
42
+ this.#track.setAttribute("aria-label", optionOr(options.label, I18n.t("feedback.cardScroll")));
43
+ // Лента сама принимает фокус: стрелки клавиатуры прокручивают её, не уводя фокус в карточки
44
+ this.#track.setAttribute("tabindex", "0");
45
+ this.#track.addEventListener("keydown", (event) => this.#onKeydown(event));
46
+ // Вертикальное колесо прокручивает ленту вбок — так её листают мышью с колесом
47
+ this.#track.addEventListener("wheel", (event) => this.#onWheel(event), { passive: false });
48
+ this.#track.addEventListener("scroll", () => this.#syncArrows());
49
+ if (options.snap === false) this.#root.dataset.snap = "false";
50
+ if (options.arrows !== false) {
51
+ this.#start = this.#makeArrow(Icons.NAVIGATION.CHEVRON_LEFT, I18n.t("feedback.cardScrollPrev"), -1);
52
+ this.#end = this.#makeArrow(Icons.NAVIGATION.CHEVRON_RIGHT, I18n.t("feedback.cardScrollNext"), 1);
53
+ this.#root.append(this.#start, this.#track, this.#end);
54
+ } else {
55
+ this.#root.appendChild(this.#track);
56
+ }
57
+ if (Array.isArray(options.items)) for (const item of options.items) this.addCard(item);
58
+ // Стрелки появляются только там, где лента действительно уходит за край: следим
59
+ // за размером (карточку могли добавить после сборки) и за прокруткой
60
+ if (typeof ResizeObserver === "function") {
61
+ this.#observer = new ResizeObserver(() => this.#syncArrows());
62
+ this.#observer.observe(this.#track);
63
+ }
64
+ this.#syncArrows();
65
+ }
66
+
67
+ /** Карточка ленты: компонент фреймворка (у него есть `set()`) или готовый узел */
68
+ addCard(component = undefined) {
69
+ if (component === undefined || component === null) return this;
70
+ const item = document.createElement("card_scroll_item");
71
+ item.setAttribute("role", "listitem");
72
+ item.appendChild(typeof component.set === "function" ? component.set() : component);
73
+ this.#track.appendChild(item);
74
+ this.#syncArrows();
75
+ return this;
76
+ }
77
+
78
+ getCards() {
79
+ return [...this.#track.children];
80
+ }
81
+
82
+ /** Переход к карточке по номеру: место берут у самой карточки, а не считают по ширине */
83
+ scrollToCard(index = Number()) {
84
+ const cards = this.getCards();
85
+ const card = cards[Number(index)];
86
+ if (card === undefined) return this;
87
+ this.#scrollTo(card.offsetLeft - this.#track.offsetLeft);
88
+ return this;
89
+ }
90
+
91
+ scrollToEnd() {
92
+ this.#scrollTo(this.#track.scrollWidth);
93
+ return this;
94
+ }
95
+
96
+ set() {
97
+ return this.#root;
98
+ }
99
+ destroy() {
100
+ if (this.#observer !== undefined) this.#observer.disconnect();
101
+ this.#observer = undefined;
102
+ this.#root.remove();
103
+ }
104
+
105
+ // === ВНУТРЕННЕЕ ===
106
+ #makeArrow(icon = String(), label = String(), direction = 1) {
107
+ const button = document.createElement("button");
108
+ button.type = "button";
109
+ button.className = "card_scroll_arrow";
110
+ // Сторона видна и стилям, и замерам: край ленты — то, что здесь важно
111
+ button.dataset.side = direction < 0 ? "start" : "end";
112
+ button.innerHTML = new Icon(icon, "20px").getHTML();
113
+ button.setAttribute("aria-label", label);
114
+ button.title = label;
115
+ button.addEventListener("click", () => this.#step(direction));
116
+ return button;
117
+ }
118
+ #scrollTo(left = Number()) {
119
+ // При уменьшенном движении лента перескакивает без плавности
120
+ this.#track.scrollTo({ left: left, behavior: reducedMotion() ? "auto" : "smooth" });
121
+ }
122
+ /** Шаг — одна карточка с зазором: он берётся у самой карточки, а не из настроек */
123
+ #step(direction = 1) {
124
+ const card = this.#track.querySelector("card_scroll_item");
125
+ const step = card === null ? this.#track.clientWidth : card.getBoundingClientRect().width + this.#gap;
126
+ this.#scrollTo(this.#track.scrollLeft + step * direction);
127
+ }
128
+ #onKeydown(event = {}) {
129
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
130
+ event.preventDefault();
131
+ this.#step(event.key === "ArrowRight" ? 1 : -1);
132
+ return;
133
+ }
134
+ if (event.key === "Home") {
135
+ event.preventDefault();
136
+ this.#scrollTo(0);
137
+ }
138
+ if (event.key === "End") {
139
+ event.preventDefault();
140
+ this.scrollToEnd();
141
+ }
142
+ }
143
+ /**
144
+ * Вертикальное колесо двигает ленту вбок. Когда лента упирается, событие отдаётся странице:
145
+ * иначе список «съел» бы прокрутку, и до содержимого ниже было бы не добраться.
146
+ */
147
+ #onWheel(event = {}) {
148
+ const track = this.#track;
149
+ if (event.deltaY === 0) return;
150
+ const max = track.scrollWidth - track.clientWidth;
151
+ if (max <= 0) return;
152
+ if ((track.scrollLeft <= 0 && event.deltaY < 0) || (track.scrollLeft >= max && event.deltaY > 0)) return;
153
+ event.preventDefault();
154
+ track.scrollLeft += event.deltaY;
155
+ }
156
+ /** Стрелка видна только там, где за краем есть карточки */
157
+ #syncArrows() {
158
+ if (this.#start === undefined || this.#end === undefined) return;
159
+ const track = this.#track;
160
+ const max = track.scrollWidth - track.clientWidth;
161
+ // Отступ ленты (он держит тень карточек) сдвигает нулевую позицию: без него
162
+ // лента с прилипанием стоит на нём, и стрелка «назад» светилась бы на старте
163
+ const offset = parseFloat(getComputedStyle(track).paddingLeft) || 0;
164
+ this.#start.hidden = track.scrollLeft <= offset + 1;
165
+ this.#end.hidden = track.scrollLeft >= max - 1;
166
+ }
167
+ }
168
+
169
+ exports.CardScroll = CardScroll;
@@ -0,0 +1,86 @@
1
+ /* Лента карточек: горизонтальный список со стрелками на краях.
2
+ Своей палитры у компонента нет: карточки приносят её сами, полоса прокрутки — общая
3
+ (`chui_scrollbar`). */
4
+
5
+ chui_card_scroll {
6
+ display: flex;
7
+ flex-direction: row;
8
+ align-items: center;
9
+ gap: 8px;
10
+ /* Компонент сам задаёт ширину: в flex-колонке с `align-items: flex-start` лента
11
+ без ширины получила бы `max-content` и вылезла за содержимое */
12
+ width: -webkit-fill-available;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ card_scroll_track {
17
+ display: flex;
18
+ flex-direction: row;
19
+ flex: 1 1 auto;
20
+ min-width: 0;
21
+ box-sizing: border-box;
22
+ /* Лента идёт вбок: по вертикали содержимое не переносится и не обрезается тенью карточек */
23
+ overflow-x: auto;
24
+ overflow-y: hidden;
25
+ /* Прилипание к началу карточки помогает остановиться ровно, но не заставляет ленту
26
+ прыгать через карточку: `proximity`, а не `mandatory` */
27
+ scroll-snap-type: x proximity;
28
+ /* Отступ под тень карточек: у обрезанного слоя тень срезалась бы по краю */
29
+ padding: 4px;
30
+ margin: -4px;
31
+ }
32
+
33
+ chui_card_scroll[data-snap="false"] card_scroll_track {
34
+ scroll-snap-type: none;
35
+ }
36
+
37
+ /* Лента сама принимает фокус: стрелки клавиатуры прокручивают её, а не уводят фокус в карточку */
38
+ card_scroll_track:focus-visible {
39
+ outline: var(--focus_outline);
40
+ outline-offset: var(--focus_outline_offset);
41
+ border-radius: var(--radius_sm);
42
+ }
43
+
44
+ card_scroll_item {
45
+ flex: none;
46
+ display: flex;
47
+ scroll-snap-align: start;
48
+ }
49
+
50
+ /* Стрелка на краю ленты — круглая кнопка со значком: имя ей даёт словарь, значка скринридер
51
+ не читает (`Icon` помечает его `aria-hidden`) */
52
+ .card_scroll_arrow {
53
+ flex: none;
54
+ cursor: pointer;
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ box-sizing: border-box;
59
+ width: var(--control_height);
60
+ height: var(--control_height);
61
+ padding: 0;
62
+ border: 1px solid var(--border_color);
63
+ border-radius: var(--radius_circle);
64
+ background: var(--element_background_2);
65
+ color: var(--text_color);
66
+ box-shadow: var(--box_shadow_main);
67
+ }
68
+
69
+ .card_scroll_arrow:hover {
70
+ background: var(--element_background_hover);
71
+ }
72
+
73
+ /* Нажатие — акцент, как у прочих кнопок тонкой настройки */
74
+ .card_scroll_arrow:active {
75
+ background: var(--blue_prime_background);
76
+ color: var(--text_color_hover);
77
+ }
78
+
79
+ .card_scroll_arrow chui_icon {
80
+ color: inherit;
81
+ }
82
+
83
+ /* За краем карточек нет — стрелки нет: `hidden` убирает её и из обхода клавиатурой */
84
+ .card_scroll_arrow[hidden] {
85
+ display: none;
86
+ }
@@ -76,23 +76,23 @@ class Chat {
76
76
  #typing = document.createElement("chat_typing");
77
77
  #composer = document.createElement("chat_composer");
78
78
  #input = document.createElement("textarea");
79
- #send_button = document.createElement("button");
79
+ #sendButton = document.createElement("button");
80
80
  #messages = [];
81
81
  #own = undefined;
82
82
  #placeholder = undefined;
83
- #send_text = undefined;
84
- #send_label = undefined;
85
- #date_separators = true;
86
- #composer_visible = true;
83
+ #sendText = undefined;
84
+ #sendLabel = undefined;
85
+ #dateSeparators = true;
86
+ #composerVisible = true;
87
87
  #disabled = false;
88
- #typing_name = undefined;
89
- #send_listeners = [];
90
- #on_send = undefined;
91
- #on_message_click = undefined;
88
+ #typingName = undefined;
89
+ #sendListeners = [];
90
+ #onSend = undefined;
91
+ #onMessageClick = undefined;
92
92
  // Текущий день и текущая группа — по ним решается, дописывать пузырь или начать новую;
93
93
  // группа — это {node, bubbles, avatar, author, day}
94
- #last_date = undefined;
95
- #last_group = undefined;
94
+ #lastDate = undefined;
95
+ #lastGroup = undefined;
96
96
 
97
97
  constructor(
98
98
  options = {
@@ -118,12 +118,12 @@ class Chat {
118
118
  this.#root.id = this.#id;
119
119
  this.#own = options.own;
120
120
  this.#placeholder = options.placeholder;
121
- this.#send_text = options.sendText;
122
- this.#send_label = options.sendLabel;
123
- this.#on_send = options.onSend;
124
- this.#on_message_click = options.onMessageClick;
125
- this.#date_separators = options.dateSeparators !== false;
126
- this.#composer_visible = options.composer !== false;
121
+ this.#sendText = options.sendText;
122
+ this.#sendLabel = options.sendLabel;
123
+ this.#onSend = options.onSend;
124
+ this.#onMessageClick = options.onMessageClick;
125
+ this.#dateSeparators = options.dateSeparators !== false;
126
+ this.#composerVisible = options.composer !== false;
127
127
  if (options.width !== undefined) this.#root.style.width = options.width;
128
128
  // Высота журнала: своя переменная, чтобы поле ввода и шапка оставались на месте
129
129
  this.#log.style.setProperty("--chat_height", options.height === undefined ? "320px" : options.height);
@@ -145,17 +145,17 @@ class Chat {
145
145
  this.#input.placeholder =
146
146
  this.#placeholder === undefined ? I18n.t("feedback.chatPlaceholder") : this.#placeholder;
147
147
  this.#input.setAttribute("aria-label", I18n.t("feedback.chatComposer"));
148
- this.#send_button.type = "button";
149
- this.#send_button.className = "chat_send";
150
- this.#send_button.disabled = true;
151
- this.#send_button.setAttribute("aria-label", this.#sendLabelText());
152
- this.#send_button.setAttribute("title", this.#sendTextText());
153
- this.#send_button.innerHTML = new Icon(Icons.CONTENT.SEND, "16px").getHTML();
148
+ this.#sendButton.type = "button";
149
+ this.#sendButton.className = "chat_send";
150
+ this.#sendButton.disabled = true;
151
+ this.#sendButton.setAttribute("aria-label", this.#sendLabelText());
152
+ this.#sendButton.setAttribute("title", this.#sendTextText());
153
+ this.#sendButton.innerHTML = new Icon(Icons.CONTENT.SEND, "16px").getHTML();
154
154
 
155
155
  this.#header.appendChild(this.#title);
156
156
  this.#header.appendChild(this.#subtitle);
157
157
  this.#composer.appendChild(this.#input);
158
- this.#composer.appendChild(this.#send_button);
158
+ this.#composer.appendChild(this.#sendButton);
159
159
  this.#root.appendChild(this.#header);
160
160
  this.#root.appendChild(this.#log);
161
161
  this.#root.appendChild(this.#typing);
@@ -164,12 +164,12 @@ class Chat {
164
164
  this.#subtitle.hidden = true;
165
165
  if (optionOr(options.title, undefined) !== undefined) this.setTitle(options.title);
166
166
  if (optionOr(options.subtitle, undefined) !== undefined) this.setSubtitle(options.subtitle);
167
- if (!this.#composer_visible) this.#composer.hidden = true;
167
+ if (!this.#composerVisible) this.#composer.hidden = true;
168
168
  if (this.#disabled) this.setDisabled(true);
169
169
 
170
170
  this.#input.addEventListener("input", () => this.#syncSendState());
171
171
  this.#input.addEventListener("keydown", this.#onInputKeyDown);
172
- this.#send_button.addEventListener("click", () => this.send());
172
+ this.#sendButton.addEventListener("click", () => this.send());
173
173
  this.#log.addEventListener("click", this.#onLogClick);
174
174
  this.setMessages(options.messages === undefined ? [] : options.messages);
175
175
  }
@@ -193,7 +193,7 @@ class Chat {
193
193
  return this.#messages.map((message) => Object.assign({}, message));
194
194
  }
195
195
  isTyping() {
196
- return this.#typing_name !== undefined;
196
+ return this.#typingName !== undefined;
197
197
  }
198
198
  isDisabled() {
199
199
  return this.#disabled;
@@ -238,7 +238,7 @@ class Chat {
238
238
  return this;
239
239
  }
240
240
  addSendListener(listener = () => {}) {
241
- this.#send_listeners.push(listener);
241
+ this.#sendListeners.push(listener);
242
242
  return this;
243
243
  }
244
244
  set() {
@@ -249,8 +249,8 @@ class Chat {
249
249
  /** Полная замена списка: журнал собирается заново, группы схлопываются по автору и дню */
250
250
  setMessages(messages = []) {
251
251
  this.#messages = [];
252
- this.#last_date = undefined;
253
- this.#last_group = undefined;
252
+ this.#lastDate = undefined;
253
+ this.#lastGroup = undefined;
254
254
  this.#log.innerHTML = "";
255
255
  for (const message of messages) this.#appendMessage(Object.assign({}, message), false);
256
256
  this.scrollToBottom(false);
@@ -275,18 +275,40 @@ class Chat {
275
275
  }
276
276
  clear() {
277
277
  this.#messages = [];
278
- this.#last_date = undefined;
279
- this.#last_group = undefined;
278
+ this.#lastDate = undefined;
279
+ this.#lastGroup = undefined;
280
280
  this.#log.innerHTML = "";
281
281
  return this;
282
282
  }
283
283
  /** «Иван печатает…»: имя автора; `undefined` убирает строку */
284
284
  setTyping(name = undefined) {
285
- this.#typing_name = name;
286
- this.#typing.hidden = name === undefined;
285
+ this.#typingName = name;
287
286
  this.#typing.innerText = name === undefined ? "" : I18n.t("feedback.chatTyping", { name: name });
287
+ this.#showTyping(name !== undefined);
288
288
  return this;
289
289
  }
290
+
291
+ /**
292
+ * Строка «печатает» появляется и уходит движением: `hidden` возвращается по концу кадра,
293
+ * поэтому строка не пропадает рывком. Открытие за время ухода отменяет снятие.
294
+ */
295
+ #showTyping(visible) {
296
+ if (visible === true) {
297
+ this.#typing.classList.remove("chat_typing_leaving");
298
+ this.#typing.hidden = false;
299
+ return;
300
+ }
301
+ if (this.#typing.hidden === true) return;
302
+ this.#typing.classList.add("chat_typing_leaving");
303
+ const done = (event) => {
304
+ if (event.target !== this.#typing) return;
305
+ this.#typing.removeEventListener("animationend", done);
306
+ if (this.#typing.classList.contains("chat_typing_leaving") === false) return;
307
+ this.#typing.classList.remove("chat_typing_leaving");
308
+ this.#typing.hidden = true;
309
+ };
310
+ this.#typing.addEventListener("animationend", done);
311
+ }
290
312
  scrollToBottom(smooth = true) {
291
313
  // Плавная прокрутка — движение; при `prefers-reduced-motion` она идёт сразу
292
314
  const behavior = smooth && this.#reducedMotion() === false ? "smooth" : "auto";
@@ -313,18 +335,18 @@ class Chat {
313
335
  this.#input.value = "";
314
336
  this.#syncSendState();
315
337
  const added = this.#appendMessage(message, true);
316
- if (this.#on_send !== undefined) this.#on_send(text, added);
317
- for (const listener of [...this.#send_listeners]) listener(text, added);
338
+ if (this.#onSend !== undefined) this.#onSend(text, added);
339
+ for (const listener of [...this.#sendListeners]) listener(text, added);
318
340
  return this;
319
341
  }
320
342
 
321
343
  // === ВНУТРЕННЕЕ ===
322
344
  #sendTextText() {
323
- return this.#send_text === undefined ? I18n.t("feedback.chatSend") : this.#send_text;
345
+ return this.#sendText === undefined ? I18n.t("feedback.chatSend") : this.#sendText;
324
346
  }
325
347
  /** Имя кнопки отправки: своё (`sendLabel`), иначе `sendText`, иначе словарь */
326
348
  #sendLabelText() {
327
- if (this.#send_label !== undefined) return this.#send_label;
349
+ if (this.#sendLabel !== undefined) return this.#sendLabel;
328
350
  return this.#sendTextText();
329
351
  }
330
352
  #reducedMotion() {
@@ -333,7 +355,7 @@ class Chat {
333
355
  return query.matches === true;
334
356
  }
335
357
  #syncSendState() {
336
- this.#send_button.disabled = this.#disabled || this.#input.value.trim() === "";
358
+ this.#sendButton.disabled = this.#disabled || this.#input.value.trim() === "";
337
359
  }
338
360
  /**
339
361
  * Добавить сообщение в журнал. Если автор и день совпадают с прошлой группой, пузырь
@@ -361,27 +383,27 @@ class Chat {
361
383
  }
362
384
  /** Разделитель дня: между сообщениями разных дней и в начале журнала */
363
385
  #insertDateSeparator(date) {
364
- if (date === undefined || this.#date_separators === false) return;
386
+ if (date === undefined || this.#dateSeparators === false) return;
365
387
  const key = this.#dayKey(date);
366
- if (key === this.#last_date) return;
367
- this.#last_date = key;
388
+ if (key === this.#lastDate) return;
389
+ this.#lastDate = key;
368
390
  const separator = document.createElement("chat_date");
369
391
  separator.dataset.day = key;
370
392
  const label = document.createElement("chat_date_label");
371
393
  label.innerText = this.#dayLabel(date);
372
394
  separator.appendChild(label);
373
395
  this.#log.appendChild(separator);
374
- this.#last_group = undefined;
396
+ this.#lastGroup = undefined;
375
397
  }
376
398
  /** Группа для сообщения: та же, если автор и день совпадают, иначе новая */
377
399
  #groupFor(message) {
378
400
  const author = this.#authorOf(message);
379
401
  if (
380
- this.#last_group !== undefined &&
381
- this.#last_group.author === author &&
382
- this.#last_group.day === this.#last_date
402
+ this.#lastGroup !== undefined &&
403
+ this.#lastGroup.author === author &&
404
+ this.#lastGroup.day === this.#lastDate
383
405
  ) {
384
- return this.#last_group;
406
+ return this.#lastGroup;
385
407
  }
386
408
  const node = document.createElement("chat_group");
387
409
  node.dataset.own = message.own === true ? "true" : "false";
@@ -399,8 +421,8 @@ class Chat {
399
421
  node.prepend(avatar.set());
400
422
  }
401
423
  this.#log.appendChild(node);
402
- this.#last_group = { node: node, bubbles: bubbles, avatar: avatar, author: author, day: this.#last_date };
403
- return this.#last_group;
424
+ this.#lastGroup = { node: node, bubbles: bubbles, avatar: avatar, author: author, day: this.#lastDate };
425
+ return this.#lastGroup;
404
426
  }
405
427
  #bubble(message, first) {
406
428
  const bubble = document.createElement("chat_bubble");
@@ -495,11 +517,11 @@ class Chat {
495
517
  this.send();
496
518
  };
497
519
  #onLogClick = (event) => {
498
- if (this.#on_message_click === undefined) return;
520
+ if (this.#onMessageClick === undefined) return;
499
521
  const bubble = event.target instanceof Element ? event.target.closest("chat_bubble") : null;
500
522
  if (bubble === null) return;
501
523
  const message = this.#messages.find((entry) => entry.id === bubble.dataset.id);
502
- if (message !== undefined) this.#on_message_click(message);
524
+ if (message !== undefined) this.#onMessageClick(message);
503
525
  };
504
526
  }
505
527
 
@@ -4,9 +4,9 @@ chui_chat {
4
4
  flex-direction: column;
5
5
  height: max-content;
6
6
  width: 360px;
7
- margin: var(--margin);
7
+ margin: var(--block_margin);
8
8
  overflow: hidden;
9
- border-radius: var(--border_radius);
9
+ border-radius: var(--radius_sm);
10
10
  background: var(--element_background);
11
11
  border: 1px solid var(--border_color);
12
12
  font-size: var(--font_default_size);
@@ -17,7 +17,7 @@ chat_header {
17
17
  display: flex;
18
18
  flex-direction: column;
19
19
  gap: 2px;
20
- padding: var(--control_padding) var(--main_padding);
20
+ padding: var(--control_padding) var(--block_padding);
21
21
  border-bottom: 1px solid var(--border_color);
22
22
  }
23
23
 
@@ -100,10 +100,21 @@ chat_bubble {
100
100
  flex-direction: column;
101
101
  gap: 2px;
102
102
  padding: 8px 10px;
103
- border-radius: var(--border_radius);
103
+ border-radius: var(--radius_sm);
104
104
  background: var(--element_background);
105
105
  border: 1px solid var(--border_color);
106
106
  color: var(--text_color);
107
+ /* Новое сообщение встаёт на место: кадр общий — см. `chui_appear_up` в global_style.css */
108
+ animation: chui_appear_up var(--motion_base) var(--motion_ease);
109
+ }
110
+
111
+ /* Строка «печатает…»: появляется и уходит движением, `hidden` возвращается по концу кадра */
112
+ chat_typing {
113
+ animation: chui_appear_up var(--motion_base) var(--motion_ease);
114
+ }
115
+
116
+ .chat_typing_leaving {
117
+ animation: chui_leave_up var(--motion_fast) var(--motion_ease) forwards;
107
118
  }
108
119
 
109
120
  /* Свои сообщения — акцентом и справа */
@@ -149,7 +160,7 @@ chat_meta {
149
160
  }
150
161
 
151
162
  chat_bubble[data-own="true"] chat_meta {
152
- color: var(--text_color_hover_disable);
163
+ color: var(--text_color_hover_disabled);
153
164
  }
154
165
 
155
166
  chat_status {
@@ -169,7 +180,7 @@ chat_status[data-status="failed"] {
169
180
 
170
181
  /* Строка «печатает…» */
171
182
  chat_typing {
172
- padding: 0 var(--main_padding) var(--control_padding);
183
+ padding: 0 var(--block_padding) var(--control_padding);
173
184
  color: var(--text_color_disabled);
174
185
  font-size: var(--font_labels_size);
175
186
  }
@@ -220,7 +231,7 @@ chat_composer {
220
231
  }
221
232
 
222
233
  .chat_send:hover {
223
- background: var(--blue_prime_background_trans);
234
+ background: var(--blue_prime_background_tint_strong);
224
235
  }
225
236
 
226
237
  .chat_send:disabled {