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
@@ -1,4 +1,5 @@
1
1
  const { Animation } = require("../../modules/chui_animations/animations");
2
+ const { reducedMotion, tokenTime } = require("../../modules/chui_animations/motion");
2
3
  const { Icon, Icons } = require("../../components/chui_icons/icons");
3
4
  const { Button } = require("../../components/chui_button/button");
4
5
  // НАСТРОЙКИ
@@ -8,7 +9,7 @@ const { ContentBlock } = require("../../components/chui_content_block/content_bl
8
9
  const { Popup } = require("../../components/chui_popups/popups");
9
10
  const { UpdateNotification } = require("../../components/chui_notification/notification_update");
10
11
  const { ipcRenderer } = require("electron");
11
- const chui_functions = require("../../modules/chui_functions");
12
+ const chuiFunctions = require("../../modules/chui_functions");
12
13
  const { DownloadNotification } = require("../../components/chui_notification/notification_download");
13
14
  const { Log } = require("../../../framework/modules/chui_logger/chui_logger");
14
15
  const { MenuKeyboard, makeKeyboardButton } = require("../../modules/chui_a11y/chui_a11y");
@@ -22,13 +23,13 @@ const { I18n } = require("../../modules/chui_i18n/chui_i18n");
22
23
  const fs = require("fs");
23
24
 
24
25
  // Глобальные обработчики ошибок renderer-процесса вешаются один раз на процесс
25
- let app_error_handlers_installed = false;
26
+ let appErrorHandlersInstalled = false;
26
27
 
27
28
  class Events {
28
- #route_event = undefined;
29
+ #routeEvent = undefined;
29
30
  constructor() {}
30
31
  route(page) {
31
- this.#route_event = new CustomEvent("route_event_" + page.constructor.name, {
32
+ this.#routeEvent = new CustomEvent("route_event_" + page.constructor.name, {
32
33
  bubbles: true,
33
34
  detail: {
34
35
  class: page.constructor.name,
@@ -36,7 +37,7 @@ class Events {
36
37
  page: page,
37
38
  },
38
39
  });
39
- document.dispatchEvent(this.#route_event);
40
+ document.dispatchEvent(this.#routeEvent);
40
41
  }
41
42
  }
42
43
 
@@ -52,19 +53,19 @@ const USER_DROPDOWN_EDGE = 8;
52
53
  * а рядом висело название страницы. Класс на шапке прячет page_name — см. global_style.css.
53
54
  */
54
55
  function applyPageMenuBar(header, menuBar) {
55
- const page_name = requireLayoutNode(LAYOUT_ROLE.PAGE_NAME);
56
+ const pageName = requireLayoutNode(LAYOUT_ROLE.PAGE_NAME);
56
57
  const previous = header.querySelector("chui_menu_bar_main");
57
58
  // Панели выпадающих списков живут в body (у шапки свой backdrop-filter), а не в полосе,
58
59
  // поэтому снятие полосы обязано убирать и их: иначе на каждой смене страницы в body
59
60
  // оставалась бы пачка невидимых панелей. Своя же полоса, вернувшаяся на место, не трогается.
60
61
  if (previous !== null && previous !== menuBar) MenuBar.destroy(previous);
61
62
  if (previous !== null) previous.remove();
62
- const has_menu_bar = menuBar !== undefined && menuBar !== null;
63
- header.classList.toggle(HEADER_WITH_MENU_BAR_CLASS, has_menu_bar);
64
- if (!has_menu_bar) return;
63
+ const hasMenuBar = menuBar !== undefined && menuBar !== null;
64
+ header.classList.toggle(HEADER_WITH_MENU_BAR_CLASS, hasMenuBar);
65
+ if (!hasMenuBar) return;
65
66
  new Animation(menuBar).fadeIn();
66
67
  // Полоса встаёт туда же, где стояло название — сразу за кнопкой меню маршрутов
67
- page_name.after(menuBar);
68
+ pageName.after(menuBar);
68
69
  }
69
70
 
70
71
  /**
@@ -75,9 +76,9 @@ function applyPageMenuBar(header, menuBar) {
75
76
  * многоточием (см. `page_name` в global_style.css), и прочитать его целиком можно только так.
76
77
  */
77
78
  function setPageName(title = String()) {
78
- const page_name = requireLayoutNode(LAYOUT_ROLE.PAGE_NAME);
79
- page_name.innerHTML = String(title);
80
- page_name.setAttribute("title", page_name.innerText);
79
+ const pageName = requireLayoutNode(LAYOUT_ROLE.PAGE_NAME);
80
+ pageName.innerHTML = String(title);
81
+ pageName.setAttribute("title", pageName.innerText);
81
82
  }
82
83
 
83
84
  class Route extends Events {
@@ -85,7 +86,7 @@ class Route extends Events {
85
86
  super();
86
87
  }
87
88
  go(page) {
88
- let center = requireLayoutNode(LAYOUT_ROLE.CENTER);
89
+ const center = requireLayoutNode(LAYOUT_ROLE.CENTER);
89
90
  //
90
91
  setPageName(page.getTitle());
91
92
  center.innerHTML = "";
@@ -132,22 +133,22 @@ function makeHeaderButtonBox(options = { title: undefined, icon: undefined, reve
132
133
  const button = document.createElement("header_button");
133
134
  const title = document.createElement("header_button_title");
134
135
  const icon = document.createElement("header_button_icon");
135
- const has_title = options.title !== undefined && options.title !== null && options.title !== "";
136
- const has_icon = options.icon !== undefined && options.icon !== null;
137
- if (has_title) title.innerText = options.title;
138
- if (has_icon) icon.innerHTML = new Icon(options.icon, "var(--header_icon_size)").getHTML();
139
- if (has_title && has_icon) {
136
+ const hasTitle = options.title !== undefined && options.title !== null && options.title !== "";
137
+ const hasIcon = options.icon !== undefined && options.icon !== null;
138
+ if (hasTitle) title.innerText = options.title;
139
+ if (hasIcon) icon.innerHTML = new Icon(options.icon, "var(--header_icon_size)").getHTML();
140
+ if (hasTitle && hasIcon) {
140
141
  // `reverse` — значок перед подписью; зазор 6 px в обоих случаях держит сам значок
141
142
  icon.style[options.reverse ? "marginRight" : "marginLeft"] = "6px";
142
143
  button.appendChild(options.reverse ? icon : title);
143
144
  button.appendChild(options.reverse ? title : icon);
144
- } else if (has_title) {
145
+ } else if (hasTitle) {
145
146
  button.appendChild(title);
146
- } else if (has_icon) {
147
+ } else if (hasIcon) {
147
148
  // Отступы квадратной кнопки держит CSS по признаку «подписи нет»
148
149
  button.appendChild(icon);
149
150
  }
150
- makeHeaderButton(button, has_icon ? icon : undefined, has_title ? undefined : options.label);
151
+ makeHeaderButton(button, hasIcon ? icon : undefined, hasTitle ? undefined : options.label);
151
152
  return button;
152
153
  }
153
154
 
@@ -178,9 +179,9 @@ class WindowControls {
178
179
  #minimize = document.createElement("wc_minimize");
179
180
  // Подписка на нересайзящееся окно: set() вызывается и из конструктора Header, и из
180
181
  // addWC(), поэтому в set() слушатель ipcRenderer накапливался.
181
- #resizable_false_event = () => this.#maximize.remove();
182
+ #resizableFalseEvent = () => this.#maximize.remove();
182
183
  // Номер запроса состояния гасит ответы, пришедшие не в том порядке
183
- #sync_request = 0;
184
+ #syncRequest = 0;
184
185
  constructor() {
185
186
  // Скрипт окна может быть preload'ом и выполняться до документа: здесь документ уже настоящий,
186
187
  // поэтому `dir`/`lang` (и зеркалирование логических свойств CSS) встают гарантированно
@@ -197,9 +198,9 @@ class WindowControls {
197
198
  this.#maximize.addEventListener("click", () => this.#windowAction("toggle-maximize"));
198
199
  // Разворот приходит не только от нашей кнопки (двойной клик по полосе, меню окна),
199
200
  // поэтому состояние читаем по факту изменения размера окна
200
- window.addEventListener("resize", this.#sync_maximized_event);
201
+ window.addEventListener("resize", this.#syncMaximizedEvent);
201
202
  this.#syncMaximized();
202
- ipcRenderer.on("chui_resizable_false", this.#resizable_false_event);
203
+ ipcRenderer.on("chui_resizable_false", this.#resizableFalseEvent);
203
204
  }
204
205
  /** Кнопка окна — значок с именем для скринридера, фокусом и активацией с клавиатуры.
205
206
  * Значок рисуется линиями (`WINDOW_ICONS`), размер задаёт CSS (см. global_style.css) */
@@ -236,9 +237,9 @@ class WindowControls {
236
237
  // Состояние приходит асинхронно, поэтому ответ устаревшего запроса отбрасывается по номеру:
237
238
  // иначе быстрая пара «развернуть/восстановить» могла бы применить ответ в обратном порядке
238
239
  #syncMaximized() {
239
- const request = ++this.#sync_request;
240
+ const request = ++this.#syncRequest;
240
241
  this.#windowState().then((state) => {
241
- if (request !== this.#sync_request) return;
242
+ if (request !== this.#syncRequest) return;
242
243
  const maximized = state.isMaximized === true;
243
244
  const label = maximized ? I18n.t("appLayout.window.restore") : I18n.t("appLayout.window.maximize");
244
245
  this.#maximize.classList.toggle("wc_maximized", maximized);
@@ -247,9 +248,9 @@ class WindowControls {
247
248
  this.#maximize.setAttribute("aria-label", label);
248
249
  });
249
250
  }
250
- #sync_maximized_event = () => this.#syncMaximized();
251
- set(pos_bool = Boolean()) {
252
- if (pos_bool) {
251
+ #syncMaximizedEvent = () => this.#syncMaximized();
252
+ set(isEnabled = Boolean()) {
253
+ if (isEnabled) {
253
254
  this.#box.appendChild(this.#close);
254
255
  this.#box.appendChild(this.#maximize);
255
256
  this.#box.appendChild(this.#minimize);
@@ -263,23 +264,23 @@ class WindowControls {
263
264
  }
264
265
 
265
266
  class Header {
266
- #wc_box = new WindowControls();
267
+ #wcBox = new WindowControls();
267
268
  #header = document.createElement("header");
268
- #header_main = document.createElement("header_main");
269
- #header_left_box = document.createElement("header_left_box");
270
- #header_right_box = document.createElement("header_right_box");
269
+ #headerMain = document.createElement("header_main");
270
+ #headerLeftBox = document.createElement("header_left_box");
271
+ #headerRightBox = document.createElement("header_right_box");
271
272
  // Высота полосы измеряется, а не предполагается — см. #observeHeight
272
- #height_observer = undefined;
273
- #height_frame = 0;
274
- #published_height = 0;
273
+ #heightObserver = undefined;
274
+ #heightFrame = 0;
275
+ #publishedHeight = 0;
275
276
  constructor(layout) {
276
277
  layout.set(LAYOUT_ROLE.HEADER, this.#header);
277
278
  this.#header.id = "header";
278
- this.#header_main.id = "header_main";
279
- this.#header_main.appendChild(this.#header_left_box);
280
- this.#header_main.appendChild(this.#header_right_box);
281
- this.#header_main.appendChild(this.#wc_box.set(false));
282
- this.#header.appendChild(this.#header_main);
279
+ this.#headerMain.id = "header_main";
280
+ this.#headerMain.appendChild(this.#headerLeftBox);
281
+ this.#headerMain.appendChild(this.#headerRightBox);
282
+ this.#headerMain.appendChild(this.#wcBox.set(false));
283
+ this.#header.appendChild(this.#headerMain);
283
284
  this.#observeHeight();
284
285
  }
285
286
  /*
@@ -292,8 +293,8 @@ class Header {
292
293
  * Теперь высота измеряется и публикуется, поэтому токен верен при любой полосе.
293
294
  */
294
295
  #observeHeight() {
295
- this.#height_observer = new ResizeObserver(() => this.#publishHeight());
296
- this.#height_observer.observe(this.#header);
296
+ this.#heightObserver = new ResizeObserver(() => this.#publishHeight());
297
+ this.#heightObserver.observe(this.#header);
297
298
  this.#publishHeight();
298
299
  }
299
300
  /*
@@ -305,42 +306,41 @@ class Header {
305
306
  * записи без проверки крутят пересчёт на ровном месте.
306
307
  */
307
308
  #publishHeight() {
308
- if (this.#height_frame !== 0) return;
309
- this.#height_frame = requestAnimationFrame(() => {
310
- this.#height_frame = 0;
309
+ if (this.#heightFrame !== 0) return;
310
+ this.#heightFrame = requestAnimationFrame(() => {
311
+ this.#heightFrame = 0;
311
312
  // offsetHeight включает черту снизу: она принадлежит полосе, от неё и считается отступ
312
313
  const height = this.#header.offsetHeight;
313
- if (height === 0 || height === this.#published_height) return;
314
- this.#published_height = height;
314
+ if (height === 0 || height === this.#publishedHeight) return;
315
+ this.#publishedHeight = height;
315
316
  document.documentElement.style.setProperty("--header_height", `${height}px`);
316
317
  });
317
318
  }
318
319
  addWC(boolean = Boolean()) {
319
- this.#header_main.insertBefore(this.#wc_box.set(boolean), this.#header_main.firstChild);
320
+ this.#headerMain.insertBefore(this.#wcBox.set(boolean), this.#headerMain.firstChild);
320
321
  }
321
322
  addToLeftBeforeTitle(...components) {
322
- for (let component of components)
323
- this.#header_left_box.insertBefore(component, this.#header_left_box.firstChild);
323
+ for (const component of components) this.#headerLeftBox.insertBefore(component, this.#headerLeftBox.firstChild);
324
324
  }
325
325
  /** Прежнее имя с опечаткой (`Tittle`): оставлено синонимом, чтобы не ломать чужие вызовы */
326
326
  addToLeftBeforeTittle(...components) {
327
327
  this.addToLeftBeforeTitle(...components);
328
328
  }
329
329
  addToLeft(...components) {
330
- for (let component of components) {
331
- this.#header_left_box.appendChild(component);
330
+ for (const component of components) {
331
+ this.#headerLeftBox.appendChild(component);
332
332
  }
333
333
  }
334
334
  addToRight(...components) {
335
- for (let component of components)
336
- this.#header_right_box.insertBefore(component, this.#header_right_box.firstChild);
335
+ for (const component of components)
336
+ this.#headerRightBox.insertBefore(component, this.#headerRightBox.firstChild);
337
337
  }
338
338
  removeToRight(...components) {
339
- for (let component of components) {
339
+ for (const component of components) {
340
340
  // У меню профиля панель лежит в body, а не в шапке (см. UserProfile),
341
341
  // поэтому снятие кнопки обязано убирать и панель: иначе в body копились бы невидимые меню
342
342
  UserProfile.destroy(component);
343
- this.#header_right_box.removeChild(component);
343
+ this.#headerRightBox.removeChild(component);
344
344
  }
345
345
  }
346
346
  set() {
@@ -370,23 +370,23 @@ class AppMenu {
370
370
  #routeViews = document.createElement("route_views");
371
371
  #appMenuButton = document.createElement("app_menu_button");
372
372
  // Шапка панели: плитка со значком и заголовок — панель читается выдвижным ящиком
373
- #app_menu_header = document.createElement("app_menu_header");
374
- #app_menu_header_icon = document.createElement("app_menu_header_icon");
375
- #app_menu_header_title = document.createElement("app_menu_header_title");
373
+ #appMenuHeader = document.createElement("app_menu_header");
374
+ #appMenuHeaderIcon = document.createElement("app_menu_header_icon");
375
+ #appMenuHeaderTitle = document.createElement("app_menu_header_title");
376
376
  // Блок поиска: своя поверхность со значком, а не одинокий ввод на фоне панели
377
- #app_menu_search_main = document.createElement("app_menu_search_main");
378
- #app_menu_search_box = document.createElement("app_menu_search_box");
379
- #app_menu_search_icon = document.createElement("app_menu_search_icon");
380
- #app_menu_search_input = document.createElement("input");
377
+ #appMenuSearchMain = document.createElement("app_menu_search_main");
378
+ #appMenuSearchBox = document.createElement("app_menu_search_box");
379
+ #appMenuSearchIcon = document.createElement("app_menu_search_icon");
380
+ #appMenuSearchInput = document.createElement("input");
381
381
  // Подсказка, когда фильтр не нашёл ни одного пункта
382
- #app_menu_empty = document.createElement("app_menu_empty");
382
+ #appMenuEmpty = document.createElement("app_menu_empty");
383
383
  // ===
384
- #auto_close = false;
385
- #search_enabled = false;
384
+ #autoClose = false;
385
+ #searchEnabled = false;
386
386
  #routeKeyboard = undefined;
387
387
  #header = undefined;
388
388
  #center = undefined;
389
- #page_name = document.createElement("page_name");
389
+ #pageName = document.createElement("page_name");
390
390
  constructor(header, center, layout) {
391
391
  this.#header = header;
392
392
  this.#center = center;
@@ -415,22 +415,22 @@ class AppMenu {
415
415
  if (this.#appMenu.style.transform === `translateX(${this.#appMenuSlide}px)`) this.#close();
416
416
  }
417
417
  });
418
- this.#page_name.id = "page_name";
419
- layout.set(LAYOUT_ROLE.PAGE_NAME, this.#page_name);
420
- header.addToLeft(this.#appMenuButton, this.#page_name);
421
- this.#app_menu_header_icon.appendChild(new Icon(Icons.NAVIGATION.MENU_OPEN, "16px").set());
418
+ this.#pageName.id = "page_name";
419
+ layout.set(LAYOUT_ROLE.PAGE_NAME, this.#pageName);
420
+ header.addToLeft(this.#appMenuButton, this.#pageName);
421
+ this.#appMenuHeaderIcon.appendChild(new Icon(Icons.NAVIGATION.MENU_OPEN, "16px").set());
422
422
  // Плитка и значок в ней — украшение: имя панели уже есть в заголовке
423
- this.#app_menu_header_icon.setAttribute("aria-hidden", "true");
424
- this.#app_menu_header_title.innerText = I18n.t("appLayout.menu");
423
+ this.#appMenuHeaderIcon.setAttribute("aria-hidden", "true");
424
+ this.#appMenuHeaderTitle.innerText = I18n.t("appLayout.menu");
425
425
  // Список пунктов назван головкой панели: скринридер читает «Меню», а не безымянный список
426
- this.#app_menu_header_title.id = "chui_app_menu_title";
427
- this.#routeViews.setAttribute("aria-labelledby", this.#app_menu_header_title.id);
428
- this.#app_menu_header.appendChild(this.#app_menu_header_icon);
429
- this.#app_menu_header.appendChild(this.#app_menu_header_title);
430
- this.#app_menu_empty.innerText = I18n.t("appLayout.search.empty");
426
+ this.#appMenuHeaderTitle.id = "chui_app_menu_title";
427
+ this.#routeViews.setAttribute("aria-labelledby", this.#appMenuHeaderTitle.id);
428
+ this.#appMenuHeader.appendChild(this.#appMenuHeaderIcon);
429
+ this.#appMenuHeader.appendChild(this.#appMenuHeaderTitle);
430
+ this.#appMenuEmpty.innerText = I18n.t("appLayout.search.empty");
431
431
  // Подсказка лежит внутри списка, но пунктом не является: клавиатура меню её не видит (itemSelector "route")
432
- this.#routeViews.appendChild(this.#app_menu_empty);
433
- this.#appMenu.appendChild(this.#app_menu_header);
432
+ this.#routeViews.appendChild(this.#appMenuEmpty);
433
+ this.#appMenu.appendChild(this.#appMenuHeader);
434
434
  this.#appMenu.appendChild(this.#routeViews);
435
435
  // Клавиатура и aria меню маршрутов: стрелки по пунктам, Escape закрывает и возвращает фокус на кнопку
436
436
  this.#routeKeyboard = new MenuKeyboard(this.#routeViews, {
@@ -471,20 +471,20 @@ class AppMenu {
471
471
  this.#routeKeyboard.setExpanded(false);
472
472
  }
473
473
  enableSearchInput() {
474
- if (this.#search_enabled) return;
475
- this.#search_enabled = true;
476
- this.#app_menu_search_input.classList.add("app_menu_search_input");
477
- this.#app_menu_search_input.placeholder = I18n.t("appLayout.search.placeholder");
478
- this.#app_menu_search_input.setAttribute("aria-label", I18n.t("appLayout.search.sections"));
474
+ if (this.#searchEnabled) return;
475
+ this.#searchEnabled = true;
476
+ this.#appMenuSearchInput.classList.add("app_menu_search_input");
477
+ this.#appMenuSearchInput.placeholder = I18n.t("appLayout.search.placeholder");
478
+ this.#appMenuSearchInput.setAttribute("aria-label", I18n.t("appLayout.search.sections"));
479
479
  // Подсветку поля даёт :focus-within на поверхности поиска — инлайновых стилей больше нет
480
- this.#app_menu_search_input.addEventListener("input", (event) => {
480
+ this.#appMenuSearchInput.addEventListener("input", (event) => {
481
481
  this.#filterRoutes(event.target.value);
482
482
  });
483
- this.#app_menu_search_icon.innerHTML = new Icon(Icons.ACTIONS.SEARCH, "15px").getHTML();
484
- this.#app_menu_search_box.appendChild(this.#app_menu_search_icon);
485
- this.#app_menu_search_box.appendChild(this.#app_menu_search_input);
486
- this.#app_menu_search_main.appendChild(this.#app_menu_search_box);
487
- this.#appMenu.insertBefore(this.#app_menu_search_main, this.#routeViews);
483
+ this.#appMenuSearchIcon.innerHTML = new Icon(Icons.ACTIONS.SEARCH, "15px").getHTML();
484
+ this.#appMenuSearchBox.appendChild(this.#appMenuSearchIcon);
485
+ this.#appMenuSearchBox.appendChild(this.#appMenuSearchInput);
486
+ this.#appMenuSearchMain.appendChild(this.#appMenuSearchBox);
487
+ this.#appMenu.insertBefore(this.#appMenuSearchMain, this.#routeViews);
488
488
  }
489
489
  // Пункты списка: подсказка о пустом результате лежит там же, поэтому фильтруем по тегу
490
490
  #routes() {
@@ -500,10 +500,10 @@ class AppMenu {
500
500
  item.style.display = matches ? "" : "none";
501
501
  if (matches) visible++;
502
502
  }
503
- this.#app_menu_empty.classList.toggle("app_menu_empty_visible", visible === 0);
503
+ this.#appMenuEmpty.classList.toggle("app_menu_empty_visible", visible === 0);
504
504
  }
505
505
  go(page) {
506
- let center = this.#center;
506
+ const center = this.#center;
507
507
  //
508
508
  setPageName(page.getTitle());
509
509
  center.innerHTML = "";
@@ -517,18 +517,18 @@ class AppMenu {
517
517
  }
518
518
  setRouteTest(page) {
519
519
  this.#routeList.push(page);
520
- let test = this.#routeList.filter((route) => route.getTitle().includes(page.getTitle()));
520
+ const test = this.#routeList.filter((route) => route.getTitle().includes(page.getTitle()));
521
521
  if (test.length === 1) {
522
- let button_route = document.createElement("route");
523
- let icon_menu = document.createElement("route_icon");
524
- icon_menu.innerHTML = page.getIcon();
525
- let title_menu = document.createElement("route_title");
526
- title_menu.innerHTML = page.getTitle();
522
+ const buttonRoute = document.createElement("route");
523
+ const iconMenu = document.createElement("route_icon");
524
+ iconMenu.innerHTML = page.getIcon();
525
+ const titleMenu = document.createElement("route_title");
526
+ titleMenu.innerHTML = page.getTitle();
527
527
  if (page.getMain()) {
528
528
  this.go(page);
529
- button_route.classList.add("route_active");
529
+ buttonRoute.classList.add("route_active");
530
530
  }
531
- button_route.addEventListener("click", () => {
531
+ buttonRoute.addEventListener("click", () => {
532
532
  const eventAwesome = new CustomEvent("route_event_" + page.constructor.name, {
533
533
  bubbles: true,
534
534
  detail: {
@@ -539,11 +539,11 @@ class AppMenu {
539
539
  });
540
540
  document.dispatchEvent(eventAwesome);
541
541
 
542
- if (!button_route.classList.contains("route_active")) {
542
+ if (!buttonRoute.classList.contains("route_active")) {
543
543
  for (const act of this.#routes()) act.classList.remove("route_active");
544
544
  this.go(page);
545
- button_route.classList.add("route_active");
546
- if (this.#auto_close) {
545
+ buttonRoute.classList.add("route_active");
546
+ if (this.#autoClose) {
547
547
  // Панель закрывается после перехода: состояние и значок меняет тот же код,
548
548
  // что и по Escape — иначе у кнопки осталось бы открытое aria-expanded
549
549
  if (this.#appMenu.style.transform === `translateX(${this.#appMenuSlide}px)`) this.#close();
@@ -551,19 +551,19 @@ class AppMenu {
551
551
  }
552
552
  });
553
553
  if (page.getIcon() !== undefined) {
554
- button_route.appendChild(icon_menu);
554
+ buttonRoute.appendChild(iconMenu);
555
555
  }
556
- button_route.appendChild(title_menu);
557
- this.#routeViews.appendChild(button_route);
556
+ buttonRoute.appendChild(titleMenu);
557
+ this.#routeViews.appendChild(buttonRoute);
558
558
  this.#routeKeyboard.refresh();
559
559
  // Новый пункт мог не пройти уже введённый фильтр — список и подсказка пересобираются
560
- this.#filterRoutes(this.#app_menu_search_input.value);
560
+ this.#filterRoutes(this.#appMenuSearchInput.value);
561
561
  } else {
562
562
  Log.error(I18n.t("appLayout.search.alreadyAdded", { title: page.getTitle() }));
563
563
  }
564
564
  }
565
565
  setAutoClose() {
566
- this.#auto_close = true;
566
+ this.#autoClose = true;
567
567
  }
568
568
  getMenu() {
569
569
  return this.#appMenu;
@@ -571,97 +571,99 @@ class AppMenu {
571
571
  }
572
572
 
573
573
  class NotificationBox {
574
- #notification_box = document.createElement("notification_box");
575
- #notification_box_main = document.createElement("notification_box_main");
574
+ #notificationBox = document.createElement("notification_box");
575
+ #notificationBoxMain = document.createElement("notification_box_main");
576
576
  // Шапка ящика: заголовок со счётчиком истории и действие «Очистить»
577
- #notification_box_header = document.createElement("notification_box_header");
578
- #notification_box_header_text = document.createElement("notification_box_header_text");
579
- #notification_box_title = document.createElement("notification_box_title");
580
- #notification_box_count = document.createElement("notification_box_count");
581
- #notification_box_empty = document.createElement("notification_box_empty");
582
- #empty_state = undefined;
583
- #clear_button = document.createElement("button");
577
+ #notificationBoxHeader = document.createElement("notification_box_header");
578
+ #notificationBoxHeaderText = document.createElement("notification_box_header_text");
579
+ #notificationBoxTitle = document.createElement("notification_box_title");
580
+ #notificationBoxCount = document.createElement("notification_box_count");
581
+ #notificationBoxEmpty = document.createElement("notification_box_empty");
582
+ // Идёт каскад очистки: подписи дней и пустое состояние ждут его конца
583
+ #clearing = false;
584
+ #emptyState = undefined;
585
+ #clearButton = document.createElement("button");
584
586
  // Состав ящика меняет Notification — за списком следит observer
585
587
  #observer = undefined;
586
- #notification_box_width = 400;
587
- #notification_box_width_test = 425;
588
- #notification_button = document.createElement("notification_button");
588
+ #notificationBoxWidth = 400;
589
+ #notificationBoxWidthTest = 425;
590
+ #notificationButton = document.createElement("notification_button");
589
591
  constructor(header, center, layout) {
590
- layout.set(LAYOUT_ROLE.NOTIFICATION_BOX, this.#notification_box);
591
- layout.set(LAYOUT_ROLE.NOTIFICATION_BOX_MAIN, this.#notification_box_main);
592
- this.#notification_box_main.id = "chui_notification_box";
592
+ layout.set(LAYOUT_ROLE.NOTIFICATION_BOX, this.#notificationBox);
593
+ layout.set(LAYOUT_ROLE.NOTIFICATION_BOX_MAIN, this.#notificationBoxMain);
594
+ this.#notificationBoxMain.id = "chui_notification_box";
593
595
  // Шапка: заголовок, счётчик записей и очистка списка
594
- this.#notification_box_title.innerText = I18n.t("appLayout.notifications.title");
595
- this.#notification_box_count.setAttribute("aria-hidden", "true");
596
- this.#notification_box_header_text.appendChild(this.#notification_box_title);
597
- this.#notification_box_header_text.appendChild(this.#notification_box_count);
596
+ this.#notificationBoxTitle.innerText = I18n.t("appLayout.notifications.title");
597
+ this.#notificationBoxCount.setAttribute("aria-hidden", "true");
598
+ this.#notificationBoxHeaderText.appendChild(this.#notificationBoxTitle);
599
+ this.#notificationBoxHeaderText.appendChild(this.#notificationBoxCount);
598
600
  // Очистка — обычная кнопка-действие, а не Button: в шапке нужна тихая ссылка-действие
599
- this.#clear_button.className = "notification_box_clear";
600
- this.#clear_button.type = "button";
601
- this.#clear_button.setAttribute("aria-label", I18n.t("appLayout.notifications.clearAll"));
602
- this.#clear_button.setAttribute("title", I18n.t("appLayout.notifications.clear"));
603
- this.#clear_button.appendChild(new Icon(Icons.ACTIONS.DELETE, "14px").set());
604
- const clear_title = document.createElement("span");
605
- clear_title.innerText = I18n.t("appLayout.notifications.clear");
606
- this.#clear_button.appendChild(clear_title);
607
- this.#clear_button.addEventListener("click", () => this.#clearAll());
608
- this.#notification_box_header.appendChild(this.#notification_box_header_text);
609
- this.#notification_box_header.appendChild(this.#clear_button);
601
+ this.#clearButton.className = "notification_box_clear";
602
+ this.#clearButton.type = "button";
603
+ this.#clearButton.setAttribute("aria-label", I18n.t("appLayout.notifications.clearAll"));
604
+ this.#clearButton.setAttribute("title", I18n.t("appLayout.notifications.clear"));
605
+ this.#clearButton.appendChild(new Icon(Icons.ACTIONS.DELETE, "14px").set());
606
+ const clearTitle = document.createElement("span");
607
+ clearTitle.innerText = I18n.t("appLayout.notifications.clear");
608
+ this.#clearButton.appendChild(clearTitle);
609
+ this.#clearButton.addEventListener("click", () => this.#clearAll());
610
+ this.#notificationBoxHeader.appendChild(this.#notificationBoxHeaderText);
611
+ this.#notificationBoxHeader.appendChild(this.#clearButton);
610
612
  // Пустое состояние — общий компонент, а не своя разметка из значка и подписи: это был
611
613
  // третий вариант одного и того же блока (здесь, в поиске меню, в таблицах). Заодно он
612
614
  // объявляется скринридеру (role="status"), а прежняя заглушка молчала
613
- this.#empty_state = new EmptyState({
615
+ this.#emptyState = new EmptyState({
614
616
  icon: Icons.SOCIAL.NOTIFICATIONS_OFF,
615
617
  title: I18n.t("appLayout.notifications.empty"),
616
618
  compact: true,
617
619
  });
618
- this.#notification_box_empty.appendChild(this.#empty_state.set());
619
- this.#notification_box_main.appendChild(this.#notification_box_empty);
620
- this.#notification_box.appendChild(this.#notification_box_header);
621
- this.#notification_box.appendChild(this.#notification_box_main);
620
+ this.#notificationBoxEmpty.appendChild(this.#emptyState.set());
621
+ this.#notificationBoxMain.appendChild(this.#notificationBoxEmpty);
622
+ this.#notificationBox.appendChild(this.#notificationBoxHeader);
623
+ this.#notificationBox.appendChild(this.#notificationBoxMain);
622
624
  // Карточки кладёт снаружи Notification, поэтому счётчик, группы и пустое состояние
623
625
  // следим по списку. Разделители дней ящик вставляет сам: на них реакция не нужна,
624
626
  // иначе пересборка дёргала бы сама себя без конца
625
627
  this.#observer = new MutationObserver((records) => this.#onBoxMutated(records));
626
- this.#observer.observe(this.#notification_box_main, { childList: true });
628
+ this.#observer.observe(this.#notificationBoxMain, { childList: true });
627
629
  this.#syncState();
628
- this.#notification_box.style.width = `${this.#notification_box_width}px`;
629
- this.#notification_box.style.right = `calc(-${this.#notification_box_width_test}px)`;
630
- this.#notification_button.addEventListener("click", () => {
631
- if (this.#notification_button.classList.contains("notification_button_active")) this.#closePanel();
630
+ this.#notificationBox.style.width = `${this.#notificationBoxWidth}px`;
631
+ this.#notificationBox.style.right = `calc(-${this.#notificationBoxWidthTest}px)`;
632
+ this.#notificationButton.addEventListener("click", () => {
633
+ if (this.#notificationButton.classList.contains("notification_button_active")) this.#closePanel();
632
634
  else this.#openPanel();
633
635
  });
634
636
  // Значок — украшение: без подписи имя кнопки складывалось из лигатуры («notifications»)
635
- const notification_glyph = new Icon(Icons.SOCIAL.NOTIFICATIONS, "var(--header_icon_size)").set();
636
- notification_glyph.setAttribute("aria-hidden", "true");
637
- this.#notification_button.appendChild(notification_glyph);
637
+ const notificationGlyph = new Icon(Icons.SOCIAL.NOTIFICATIONS, "var(--header_icon_size)").set();
638
+ notificationGlyph.setAttribute("aria-hidden", "true");
639
+ this.#notificationButton.appendChild(notificationGlyph);
638
640
  // Имя кнопки — в том числе «непрочитанных: N» — держит #syncUnread: одно место
639
641
  // Клавиатура и aria панели уведомлений
640
- makeKeyboardButton(this.#notification_button);
641
- this.#notification_button.setAttribute("aria-haspopup", "true");
642
- this.#notification_button.setAttribute("aria-expanded", "false");
643
- this.#notification_button.setAttribute("aria-controls", "chui_notification_box");
644
- this.#notification_box.setAttribute("role", "region");
645
- this.#notification_box.setAttribute("aria-label", I18n.t("appLayout.notifications.title"));
646
- this.#notification_box.addEventListener("keydown", (event) => {
642
+ makeKeyboardButton(this.#notificationButton);
643
+ this.#notificationButton.setAttribute("aria-haspopup", "true");
644
+ this.#notificationButton.setAttribute("aria-expanded", "false");
645
+ this.#notificationButton.setAttribute("aria-controls", "chui_notification_box");
646
+ this.#notificationBox.setAttribute("role", "region");
647
+ this.#notificationBox.setAttribute("aria-label", I18n.t("appLayout.notifications.title"));
648
+ this.#notificationBox.addEventListener("keydown", (event) => {
647
649
  if (event.key !== "Escape") return;
648
650
  event.preventDefault();
649
651
  this.#closePanel();
650
- this.#notification_button.focus();
652
+ this.#notificationButton.focus();
651
653
  });
652
654
  // Панель открывается с кнопки, и фокус остаётся на ней же — Escape обязан закрывать
653
655
  // ящик и оттуда, иначе выйти с клавиатуры можно было только Tab'ом внутрь панели
654
- this.#notification_button.addEventListener("keydown", (event) => {
656
+ this.#notificationButton.addEventListener("keydown", (event) => {
655
657
  if (event.key !== "Escape") return;
656
658
  event.preventDefault();
657
659
  this.#closePanel();
658
660
  });
659
661
  header.set().addEventListener("click", (e) => {
660
- if (e.target !== this.#notification_button) this.#closePanel();
662
+ if (e.target !== this.#notificationButton) this.#closePanel();
661
663
  });
662
664
  center.addEventListener("click", (e) => {
663
- if (!this.#notification_box.contains(e.target)) {
664
- if (this.#notification_box.style.transform === `translateX(-${this.#notification_box_width_test}px)`)
665
+ if (!this.#notificationBox.contains(e.target)) {
666
+ if (this.#notificationBox.style.transform === `translateX(-${this.#notificationBoxWidthTest}px)`)
665
667
  this.#closePanel();
666
668
  }
667
669
  });
@@ -670,27 +672,27 @@ class NotificationBox {
670
672
  // вид полосы целиком описан в global_style.css
671
673
  header.set().classList.toggle("header_scrolled", center.scrollTop > 15);
672
674
  };
673
- header.addToRight(this.#notification_button);
675
+ header.addToRight(this.#notificationButton);
674
676
  }
675
677
  #openPanel() {
676
- this.#notification_box.style.transform = `translateX(-${this.#notification_box_width_test}px)`;
677
- this.#notification_button.classList.add("notification_button_active");
678
- this.#notification_button.setAttribute("aria-expanded", "true");
678
+ this.#notificationBox.style.transform = `translateX(-${this.#notificationBoxWidthTest}px)`;
679
+ this.#notificationButton.classList.add("notification_button_active");
680
+ this.#notificationButton.setAttribute("aria-expanded", "true");
679
681
  // Открытый ящик — значит прочитанный: отметки снимаются все сразу, а не по одной
680
682
  this.#markRead();
681
683
  }
682
684
  #closePanel() {
683
- this.#notification_box.style.transform = `translateX(${this.#notification_box_width}px)`;
684
- this.#notification_button.classList.remove("notification_button_active");
685
- this.#notification_button.setAttribute("aria-expanded", "false");
685
+ this.#notificationBox.style.transform = `translateX(${this.#notificationBoxWidth}px)`;
686
+ this.#notificationButton.classList.remove("notification_button_active");
687
+ this.#notificationButton.setAttribute("aria-expanded", "false");
686
688
  }
687
689
  // Записи истории: карточки в ящик кладёт Notification, поэтому состав читаем из DOM
688
690
  #notifications() {
689
- return [...this.#notification_box_main.children].filter((child) => child.tagName === "NOTIFICATION");
691
+ return [...this.#notificationBoxMain.children].filter((child) => child.tagName === "NOTIFICATION");
690
692
  }
691
693
  // Разделители дней — тоже прямые дети списка, но записями не являются
692
694
  #groups() {
693
- return [...this.#notification_box_main.children].filter((child) => child.tagName === "NOTIFICATION_GROUP");
695
+ return [...this.#notificationBoxMain.children].filter((child) => child.tagName === "NOTIFICATION_GROUP");
694
696
  }
695
697
  /*
696
698
  * Реакция на изменение состава ящика.
@@ -709,10 +711,11 @@ class NotificationBox {
709
711
  #syncState() {
710
712
  const notifications = this.#notifications();
711
713
  const count = notifications.length;
712
- this.#notification_box_count.innerText = String(count);
713
- this.#notification_box_count.style.display = count === 0 ? "none" : "flex";
714
- this.#clear_button.disabled = count === 0;
715
- this.#notification_box_empty.classList.toggle("notification_box_empty_visible", count === 0);
714
+ this.#notificationBoxCount.innerText = String(count);
715
+ this.#notificationBoxCount.style.display = count === 0 ? "none" : "flex";
716
+ // Пока идёт каскад, чистить нечего: кнопка ждёт его конца
717
+ this.#clearButton.disabled = count === 0 || this.#clearing;
718
+ this.#notificationBoxEmpty.classList.toggle("notification_box_empty_visible", count === 0);
716
719
  this.#group(notifications);
717
720
  this.#syncUnread(notifications);
718
721
  }
@@ -725,12 +728,15 @@ class NotificationBox {
725
728
  * по тегу (счётчик, очистка), работает без изменений.
726
729
  */
727
730
  #group(notifications = []) {
731
+ // Идёт каскад уборки: подписи уже гаснут своим кадром, а пересборка заменила бы их
732
+ // новыми и вернула бы рывок, который каскад и убирает
733
+ if (this.#clearing) return;
728
734
  for (const separator of this.#groups()) separator.remove();
729
735
  let day = undefined;
730
736
  for (const notification of notifications) {
731
737
  if (notification.dataset.day === day) continue;
732
738
  day = notification.dataset.day;
733
- this.#notification_box_main.insertBefore(this.#makeGroup(day), notification);
739
+ this.#notificationBoxMain.insertBefore(this.#makeGroup(day), notification);
734
740
  }
735
741
  }
736
742
  /** Подпись дня: сегодня и вчера — словами, раньше — датой */
@@ -738,8 +744,8 @@ class NotificationBox {
738
744
  const yesterday = new Date();
739
745
  yesterday.setDate(yesterday.getDate() - 1);
740
746
  const group = document.createElement("notification_group");
741
- if (day === chui_functions.getDayKey()) group.innerText = I18n.t("appLayout.notifications.today");
742
- else if (day === chui_functions.getDayKey(yesterday))
747
+ if (day === chuiFunctions.getDayKey()) group.innerText = I18n.t("appLayout.notifications.today");
748
+ else if (day === chuiFunctions.getDayKey(yesterday))
743
749
  group.innerText = I18n.t("appLayout.notifications.yesterday");
744
750
  else {
745
751
  // Ключ хранится как ГГГГ-ММ-ДД, а в подписи порядок привычный: ДД.ММ.ГГГГ
@@ -757,27 +763,79 @@ class NotificationBox {
757
763
  */
758
764
  #syncUnread(notifications = []) {
759
765
  const unread = notifications.filter((notification) => notification.dataset.unread === "true").length;
760
- this.#notification_button.classList.toggle("notification_button_unread", unread > 0);
766
+ this.#notificationButton.classList.toggle("notification_button_unread", unread > 0);
761
767
  const label =
762
768
  unread === 0
763
769
  ? I18n.t("appLayout.notifications.title")
764
770
  : I18n.t("appLayout.notifications.unreadCount", { count: unread });
765
- this.#notification_button.setAttribute("aria-label", label);
766
- this.#notification_button.setAttribute("title", label);
771
+ this.#notificationButton.setAttribute("aria-label", label);
772
+ this.#notificationButton.setAttribute("title", label);
767
773
  }
768
774
  #markRead() {
769
775
  for (const notification of this.#notifications()) notification.dataset.unread = "false";
770
776
  this.#syncUnread(this.#notifications());
771
777
  }
778
+ /*
779
+ * Очистка истории.
780
+ *
781
+ * Карточки уходят каскадом: номер в очереди задаёт `--notification_clear_index`, а шаг —
782
+ * доля токена движения, поэтому весь список читается как рассыпающийся, а не как
783
+ * выключенный одним щелчком. Подписи дней гаснут вместе с ним, пустое состояние
784
+ * и счётчик появляются после последнего кадра.
785
+ *
786
+ * Конец каскада считает срок из тех же токенов движения, а не ждёт `animationend`:
787
+ * у карточки, которую браузер не отрисовывает (скрытая панель, `display: none`), кадр
788
+ * вообще не проигрывается, и событие не придёт — уборка осталась бы висеть
789
+ * незавершённой вместе с погашенными подписями.
790
+ */
772
791
  #clearAll() {
773
- for (const notification of this.#notifications()) {
774
- notification.style.transform = "translateX(100%)";
775
- notification.style.opacity = "0";
776
- setTimeout(() => notification.remove(), 300);
792
+ // Повторное нажатие на ходу нача́ло бы второй каскад и перезапустило кадры
793
+ if (this.#clearing) return;
794
+ const notifications = this.#notifications();
795
+ if (notifications.length === 0) return;
796
+ // Уменьшенное движение: история очищается сразу — каскад только задерживал бы
797
+ if (reducedMotion()) {
798
+ for (const notification of notifications) notification.remove();
799
+ this.#syncState();
800
+ return;
801
+ }
802
+ this.#clearing = true;
803
+ this.#clearButton.disabled = true;
804
+ notifications.forEach((notification, index) => {
805
+ // Следы кадра прихода снимаем: инлайновое имя кадра сильнее класса, а вид движения
806
+ // спорит с ним по весу — без этого запись, только что вставшая в ящик, проиграла бы
807
+ // не кадр уборки, а свой въезд
808
+ notification.style.removeProperty("animation-name");
809
+ notification.removeAttribute("data-motion");
810
+ notification.style.setProperty("--notification_clear_index", String(index));
811
+ notification.classList.add("notification_clearing");
812
+ });
813
+ // Подписи дней гаснут вместе с карточками, а убираются только по концу каскада:
814
+ // иначе строка пропала бы из-под ещё уезжающих карточек
815
+ for (const group of this.#groups()) group.classList.add("notification_group_leaving");
816
+ const step = tokenTime("--motion_fast") / 4;
817
+ const frame = tokenTime("--motion_fast");
818
+ // Запас — ещё один шаг: кадр задерживается ещё и на запуск
819
+ const deadline = frame + step * notifications.length;
820
+ setTimeout(() => this.#finishClearing(notifications), deadline);
821
+ }
822
+ /*
823
+ * Конец каскада: убираем только те карточки, которые и были в очереди (за время уборки
824
+ * могла прийти новая — её очистка не касается), затем согласуем подписи, счётчик и подсказку
825
+ * по настоящему составу ящика.
826
+ */
827
+ #finishClearing(notifications = []) {
828
+ this.#clearing = false;
829
+ for (const notification of notifications) {
830
+ notification.classList.remove("notification_clearing");
831
+ notification.style.removeProperty("--notification_clear_index");
832
+ notification.remove();
777
833
  }
834
+ for (const group of this.#groups()) group.remove();
835
+ this.#syncState();
778
836
  }
779
837
  getBox() {
780
- return this.#notification_box;
838
+ return this.#notificationBox;
781
839
  }
782
840
  }
783
841
 
@@ -822,11 +880,11 @@ class AppLayout {
822
880
  constructor() {
823
881
  require("../../modules/chui_fonts").install();
824
882
  // Глобальные стили
825
- chui_functions.setStyles(__dirname + `/global_style.css`, "chUiJS_Global_App");
826
- chui_functions.setStyles(__dirname + `/main_theme_style.css`, "chUiJS_Main_Theme");
883
+ chuiFunctions.setStyles(__dirname + `/global_style.css`, "chUiJS_Global_App");
884
+ chuiFunctions.setStyles(__dirname + `/main_theme_style.css`, "chUiJS_Main_Theme");
827
885
  // Необработанные ошибки и reject’ы renderer-процесса — один раз на процесс
828
- if (app_error_handlers_installed === false) {
829
- app_error_handlers_installed = true;
886
+ if (appErrorHandlersInstalled === false) {
887
+ appErrorHandlersInstalled = true;
830
888
  window.addEventListener("error", this.#onWindowError);
831
889
  window.addEventListener("unhandledrejection", this.#onUnhandledRejection);
832
890
  }
@@ -877,16 +935,16 @@ class AppLayout {
877
935
  ipcRenderer.on("sendNotificationUpdateClose", () => {
878
936
  if (this.#updateNotification !== undefined) this.#updateNotification.hide();
879
937
  });
880
- let popup = new Popup();
938
+ const popup = new Popup();
881
939
  ipcRenderer.once("checkUpdatesTrue", async (e, check, version) => {
882
940
  if (check) {
883
- let confirm_res = await popup.confirm({
941
+ const confirmRes = await popup.confirm({
884
942
  title: I18n.t("appLayout.update.available", { version: version }),
885
943
  message: I18n.t("appLayout.update.question"),
886
944
  cancelText: I18n.t("appLayout.update.cancel"),
887
945
  okText: I18n.t("appLayout.update.install"),
888
946
  });
889
- e.sender.send("updateInstallConfirm", confirm_res);
947
+ e.sender.send("updateInstallConfirm", confirmRes);
890
948
  }
891
949
  });
892
950
  }
@@ -920,8 +978,8 @@ class AppLayout {
920
978
  this.#layout.get(LAYOUT_ROLE.PAGE_NAME).classList.add("page_name_no_menu");
921
979
  }
922
980
  setScript(pathToJS = String(), id = String()) {
923
- let data = fs.readFileSync(pathToJS, "utf8");
924
- let script = document.createElement("script");
981
+ const data = fs.readFileSync(pathToJS, "utf8");
982
+ const script = document.createElement("script");
925
983
  script.innerHTML = String(data);
926
984
  script.setAttribute("id", id);
927
985
  // Скрипт подключается в сам документ (не узел компонента), поэтому проверка по id и document
@@ -990,21 +1048,21 @@ class AppLayout {
990
1048
  }
991
1049
 
992
1050
  class HeaderTabs {
993
- #id_list = require("randomstring").generate();
994
- #header_tabs_list = undefined;
995
- #header_tabs = document.createElement("header_tabs");
996
- #keydown_event = (event) => this.#onKeydown(event);
1051
+ #idList = require("randomstring").generate();
1052
+ #headerTabsList = undefined;
1053
+ #headerTabs = document.createElement("header_tabs");
1054
+ #keydownEvent = (event) => this.#onKeydown(event);
997
1055
  constructor(options = { width: String(), default: Number(), tabs: [] }) {
998
- this.#header_tabs_list = options.tabs;
999
- this.#header_tabs.id = this.#id_list;
1056
+ this.#headerTabsList = options.tabs;
1057
+ this.#headerTabs.id = this.#idList;
1000
1058
  // Полоса вкладок — не меню: у неё свои роли (`tablist`/`tab`) и свои клавиши (см. #onKeydown).
1001
1059
  // `role="button"` и `tabindex="0"` каждой вкладке уже поставил makeKeyboardButton, поэтому
1002
1060
  // роли и порядок обхода переопределяем здесь: остановка Tab в полосе должна быть ровно одна
1003
- this.#header_tabs.setAttribute("role", "tablist");
1004
- for (let item of this.#header_tabs_list) {
1061
+ this.#headerTabs.setAttribute("role", "tablist");
1062
+ for (const item of this.#headerTabsList) {
1005
1063
  item.setAttribute("role", "tab");
1006
1064
  item.setAttribute("tabindex", "-1");
1007
- this.#header_tabs.appendChild(item);
1065
+ this.#headerTabs.appendChild(item);
1008
1066
  item.addEventListener("click", (event) => {
1009
1067
  if (event.target.tagName === "HEADER_BUTTON_ICON" || event.target.tagName === "HEADER_BUTTON_TITLE") {
1010
1068
  this.#setActive(event.target.parentNode);
@@ -1019,7 +1077,7 @@ class HeaderTabs {
1019
1077
  // кликом он снимал у неё и курсор-указатель, и ховер, которых у соседних кнопок шапки
1020
1078
  // нет. Слушатель ловит клик в фазе перехвата: действие вкладки и `#setActive` висят
1021
1079
  // на самом элементе и иначе сработали бы первыми
1022
- this.#header_tabs.addEventListener(
1080
+ this.#headerTabs.addEventListener(
1023
1081
  "click",
1024
1082
  (event) => {
1025
1083
  const tab = event.target.closest === undefined ? null : event.target.closest("header_button");
@@ -1030,12 +1088,12 @@ class HeaderTabs {
1030
1088
  );
1031
1089
  // Пока ни одна вкладка не выбрана, остановкой обхода становится первая по счёту:
1032
1090
  // иначе в полосу нельзя было бы войти с клавиатуры вообще
1033
- const default_tab = this.#header_tabs_list[options.default === undefined ? 0 : options.default];
1034
- if (default_tab !== undefined) default_tab.setAttribute("tabindex", "0");
1035
- this.#header_tabs.addEventListener("keydown", this.#keydown_event);
1036
- if (options.width !== undefined) this.#header_tabs.style.width = options.width;
1037
- for (let item of this.#header_tabs_list) {
1038
- if (this.#header_tabs_list.indexOf(item) === options.default) {
1091
+ const defaultTab = this.#headerTabsList[options.default === undefined ? 0 : options.default];
1092
+ if (defaultTab !== undefined) defaultTab.setAttribute("tabindex", "0");
1093
+ this.#headerTabs.addEventListener("keydown", this.#keydownEvent);
1094
+ if (options.width !== undefined) this.#headerTabs.style.width = options.width;
1095
+ for (const item of this.#headerTabsList) {
1096
+ if (this.#headerTabsList.indexOf(item) === options.default) {
1039
1097
  setTimeout(() => {
1040
1098
  item.click();
1041
1099
  //this.#setActive(item)
@@ -1044,13 +1102,13 @@ class HeaderTabs {
1044
1102
  }
1045
1103
  }
1046
1104
  set() {
1047
- return this.#header_tabs;
1105
+ return this.#headerTabs;
1048
1106
  }
1049
1107
  #setActive(target) {
1050
1108
  if (target.getAttribute("active") === null) {
1051
1109
  // `children`, а не `childNodes`: в `childNodes` попадают и текстовые узлы, у которых нет classList.
1052
1110
  // `children` — HTMLCollection, поэтому перебираем копию
1053
- [...this.#header_tabs.children].forEach((child) => {
1111
+ [...this.#headerTabs.children].forEach((child) => {
1054
1112
  child.removeAttribute("active");
1055
1113
  child.removeAttribute("aria-current");
1056
1114
  child.removeAttribute("aria-selected");
@@ -1077,7 +1135,7 @@ class HeaderTabs {
1077
1135
  * `makeKeyboardButton` (клик по выбранной вкладке гасит перехватчик выше).
1078
1136
  */
1079
1137
  #onKeydown(event) {
1080
- const tabs = [...this.#header_tabs.children];
1138
+ const tabs = [...this.#headerTabs.children];
1081
1139
  if (tabs.length === 0) return;
1082
1140
  const focused = tabs.indexOf(document.activeElement);
1083
1141
  // Фокус мог быть не в полосе: тогда считаем от вкладки с остановкой обхода
@@ -1100,12 +1158,12 @@ class UserProfile {
1100
1158
  // находится по узлу обёртки — так же, как у MenuBar, и по нему его снимает тот,
1101
1159
  // кто убирает профиль из шапки (`Header.removeToRight`)
1102
1160
  static #instances = new WeakMap();
1103
- #user_main = document.createElement("user_main");
1104
- #user_button = document.createElement("user_button");
1105
- #user_dropdown = document.createElement("user_dropdown");
1106
- #is_open = false;
1161
+ #userMain = document.createElement("user_main");
1162
+ #userButton = document.createElement("user_button");
1163
+ #userDropdown = document.createElement("user_dropdown");
1164
+ #isOpen = false;
1107
1165
  //
1108
- #user_dd_image_main = document.createElement("user_dd_image_main");
1166
+ #userDdImageMain = document.createElement("user_dd_image_main");
1109
1167
  // Аватар — компонент Avatar: инициалы, картинка из адреса/base64/файла
1110
1168
  #avatar = undefined;
1111
1169
  #username = undefined;
@@ -1113,15 +1171,15 @@ class UserProfile {
1113
1171
  // Слушатель window нужен только пока меню открыто: иначе он копился на каждый профиль.
1114
1172
  // Закрывает меню только по клику СНАРУЖИ: раньше сравнивался прямой родитель цели,
1115
1173
  // поэтому клик по аватару или пустому месту внутри меню тоже его закрывал
1116
- #window_click_event = (event) => {
1117
- if (this.#user_main.contains(event.target) || this.#user_dropdown.contains(event.target)) return;
1174
+ #windowClickEvent = (event) => {
1175
+ if (this.#userMain.contains(event.target) || this.#userDropdown.contains(event.target)) return;
1118
1176
  this.#closeMenu();
1119
1177
  };
1120
1178
  // Выбор пункта закрывает меню. Обработчик на списке, а не на каждом пункте: пункты
1121
1179
  // приходят из UserDDItem и могут быть добавлены кем угодно снаружи
1122
- #dropdown_click_event = (event) => {
1180
+ #dropdownClickEvent = (event) => {
1123
1181
  const item = event.target.closest === undefined ? null : event.target.closest("user_item");
1124
- if (item === null || !this.#user_dropdown.contains(item)) return;
1182
+ if (item === null || !this.#userDropdown.contains(item)) return;
1125
1183
  this.#closeMenu();
1126
1184
  };
1127
1185
  //
@@ -1133,15 +1191,15 @@ class UserProfile {
1133
1191
  }
1134
1192
  ) {
1135
1193
  this.#username = options.username;
1136
- this.#user_button.innerText = options.username;
1137
- this.#user_main.appendChild(this.#user_button);
1138
- UserProfile.#instances.set(this.#user_main, this);
1194
+ this.#userButton.innerText = options.username;
1195
+ this.#userMain.appendChild(this.#userButton);
1196
+ UserProfile.#instances.set(this.#userMain, this);
1139
1197
  // Панель — слой в body: внутри шапки (у неё свой backdrop-filter) её blur не работал бы,
1140
1198
  // а выдвижные слои оболочки перекрывали бы её. Идентификатор нужен для aria-controls
1141
- this.#user_dropdown.id = require("randomstring").generate();
1142
- document.body.appendChild(this.#user_dropdown);
1143
- this.#user_button.addEventListener("click", () => {
1144
- if (this.#is_open) this.#closeMenu();
1199
+ this.#userDropdown.id = require("randomstring").generate();
1200
+ document.body.appendChild(this.#userDropdown);
1201
+ this.#userButton.addEventListener("click", () => {
1202
+ if (this.#isOpen) this.#closeMenu();
1145
1203
  else this.#openMenu();
1146
1204
  });
1147
1205
  //
@@ -1153,22 +1211,22 @@ class UserProfile {
1153
1211
  size: "80px",
1154
1212
  decorative: true,
1155
1213
  });
1156
- this.#user_dd_image_main.appendChild(this.#avatar.set());
1157
- this.#user_dropdown.appendChild(this.#user_dd_image_main);
1214
+ this.#userDdImageMain.appendChild(this.#avatar.set());
1215
+ this.#userDropdown.appendChild(this.#userDdImageMain);
1158
1216
  }
1159
1217
  //
1160
- for (let item of options.items) this.#user_dropdown.appendChild(item);
1161
- this.#user_dropdown.addEventListener("click", this.#dropdown_click_event);
1218
+ for (const item of options.items) this.#userDropdown.appendChild(item);
1219
+ this.#userDropdown.addEventListener("click", this.#dropdownClickEvent);
1162
1220
  // Клавиатура и aria меню профиля: стрелки по пунктам, Escape закрывает и возвращает фокус
1163
- this.#menuKeyboard = new MenuKeyboard(this.#user_dropdown, {
1221
+ this.#menuKeyboard = new MenuKeyboard(this.#userDropdown, {
1164
1222
  itemSelector: "user_item",
1165
1223
  onEscape: () => this.#closeMenu(),
1166
- trigger: this.#user_button,
1224
+ trigger: this.#userButton,
1167
1225
  });
1168
- makeKeyboardButton(this.#user_button);
1226
+ makeKeyboardButton(this.#userButton);
1169
1227
  // Закрытое меню объявляется закрытым сразу: иначе у кнопки не было бы aria-expanded
1170
1228
  this.#menuKeyboard.setExpanded(false);
1171
- this.#user_button.addEventListener("keydown", (event) => {
1229
+ this.#userButton.addEventListener("keydown", (event) => {
1172
1230
  if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1173
1231
  event.preventDefault();
1174
1232
  this.#openMenu();
@@ -1177,40 +1235,40 @@ class UserProfile {
1177
1235
  });
1178
1236
  }
1179
1237
  #openMenu() {
1180
- this.#is_open = true;
1238
+ this.#isOpen = true;
1181
1239
  // Панель живёт в body: если её снимал `unmount`, возвращаем на место
1182
- if (document.body.contains(this.#user_dropdown) === false) document.body.appendChild(this.#user_dropdown);
1183
- this.#user_button.classList.add(USER_PROFILE_ACTIVE_CLASS);
1240
+ if (document.body.contains(this.#userDropdown) === false) document.body.appendChild(this.#userDropdown);
1241
+ this.#userButton.classList.add(USER_PROFILE_ACTIVE_CLASS);
1184
1242
  // fadeIn показывает панель (display: flex), и только после этого её можно измерить
1185
- new Animation(this.#user_dropdown).fadeIn();
1243
+ new Animation(this.#userDropdown).fadeIn();
1186
1244
  this.#positionDropdown();
1187
- window.addEventListener("click", this.#window_click_event);
1188
- window.addEventListener("resize", this.#position_dropdown_event);
1245
+ window.addEventListener("click", this.#windowClickEvent);
1246
+ window.addEventListener("resize", this.#positionDropdownEvent);
1189
1247
  this.#menuKeyboard.setExpanded(true);
1190
1248
  }
1191
1249
  #closeMenu() {
1192
- if (!this.#is_open) return;
1193
- this.#is_open = false;
1194
- this.#user_button.classList.remove(USER_PROFILE_ACTIVE_CLASS);
1195
- new Animation(this.#user_dropdown).fadeOut();
1196
- window.removeEventListener("click", this.#window_click_event);
1197
- window.removeEventListener("resize", this.#position_dropdown_event);
1250
+ if (!this.#isOpen) return;
1251
+ this.#isOpen = false;
1252
+ this.#userButton.classList.remove(USER_PROFILE_ACTIVE_CLASS);
1253
+ new Animation(this.#userDropdown).fadeOut();
1254
+ window.removeEventListener("click", this.#windowClickEvent);
1255
+ window.removeEventListener("resize", this.#positionDropdownEvent);
1198
1256
  this.#menuKeyboard.setExpanded(false);
1199
1257
  }
1200
1258
  // position: fixed — координаты считаются от окна, поэтому панель пересчитывается и на resize.
1201
1259
  // Логический `inset-inline-end` держит выравнивание по кнопке и в RTL
1202
1260
  #positionDropdown() {
1203
- const button = this.#user_button.getBoundingClientRect();
1204
- const dropdown = this.#user_dropdown.getBoundingClientRect();
1261
+ const button = this.#userButton.getBoundingClientRect();
1262
+ const dropdown = this.#userDropdown.getBoundingClientRect();
1205
1263
  let end = window.innerWidth - button.right;
1206
1264
  if (end + dropdown.width > window.innerWidth - USER_DROPDOWN_EDGE) {
1207
1265
  end = window.innerWidth - USER_DROPDOWN_EDGE - dropdown.width;
1208
1266
  }
1209
1267
  if (end < USER_DROPDOWN_EDGE) end = USER_DROPDOWN_EDGE;
1210
- this.#user_dropdown.style.insetInlineEnd = `${Math.round(end)}px`;
1211
- this.#user_dropdown.style.top = `${Math.round(button.bottom + USER_DROPDOWN_GAP)}px`;
1268
+ this.#userDropdown.style.insetInlineEnd = `${Math.round(end)}px`;
1269
+ this.#userDropdown.style.top = `${Math.round(button.bottom + USER_DROPDOWN_GAP)}px`;
1212
1270
  }
1213
- #position_dropdown_event = () => this.#positionDropdown();
1271
+ #positionDropdownEvent = () => this.#positionDropdown();
1214
1272
 
1215
1273
  getUsername() {
1216
1274
  return this.#username;
@@ -1228,8 +1286,8 @@ class UserProfile {
1228
1286
  */
1229
1287
  unmount() {
1230
1288
  this.#closeMenu();
1231
- this.#user_dropdown.remove();
1232
- UserProfile.#instances.delete(this.#user_main);
1289
+ this.#userDropdown.remove();
1290
+ UserProfile.#instances.delete(this.#userMain);
1233
1291
  return this;
1234
1292
  }
1235
1293
  /** Полное удаление: панель больше не нужна — снять клавиатуру и убрать панель из body */
@@ -1249,12 +1307,12 @@ class UserProfile {
1249
1307
  }
1250
1308
 
1251
1309
  set() {
1252
- return this.#user_main;
1310
+ return this.#userMain;
1253
1311
  }
1254
1312
  }
1255
1313
 
1256
1314
  class UserDDItem {
1257
- #user_item = document.createElement("user_item");
1315
+ #userItem = document.createElement("user_item");
1258
1316
 
1259
1317
  constructor(
1260
1318
  options = {
@@ -1264,26 +1322,26 @@ class UserDDItem {
1264
1322
  }
1265
1323
  ) {
1266
1324
  if (options.title !== undefined && options.icon !== undefined) {
1267
- this.#user_item.innerText = options.title;
1325
+ this.#userItem.innerText = options.title;
1268
1326
  // Значок ведёт строку — как в пунктах context_menu_item и выпадающих списков
1269
1327
  // меню-бара. Дописанный в конец, он прилипал к подписи, и пункт меню профиля
1270
1328
  // выбивался из семьи меню (в остальных меню значок стоит перед текстом)
1271
- this.#user_item.insertAdjacentHTML("afterbegin", new Icon(options.icon).getHTML());
1329
+ this.#userItem.insertAdjacentHTML("afterbegin", new Icon(options.icon).getHTML());
1272
1330
  } else if (options.title !== undefined) {
1273
- this.#user_item.innerText = options.title;
1331
+ this.#userItem.innerText = options.title;
1274
1332
  } else if (options.icon !== undefined) {
1275
- this.#user_item.innerHTML = new Icon(options.icon).getHTML();
1333
+ this.#userItem.innerHTML = new Icon(options.icon).getHTML();
1276
1334
  }
1277
- this.#user_item.addEventListener("click", options.clickEvent);
1335
+ this.#userItem.addEventListener("click", options.clickEvent);
1278
1336
  }
1279
1337
 
1280
1338
  set() {
1281
- return this.#user_item;
1339
+ return this.#userItem;
1282
1340
  }
1283
1341
  }
1284
1342
 
1285
1343
  class HeaderDialog {
1286
- #header_button = undefined;
1344
+ #headerButton = undefined;
1287
1345
  constructor(
1288
1346
  options = {
1289
1347
  title: String(),
@@ -1308,72 +1366,72 @@ class HeaderDialog {
1308
1366
  // Неполные `dialogOptions` сборку не роняют: `style` и `components` — необязательные
1309
1367
  // части опции, а прямое обращение к ним давало TypeError при создании кнопки
1310
1368
  // («Cannot read properties of undefined») ещё до первого открытия слоя
1311
- const dialog_options = options.dialogOptions === undefined ? {} : options.dialogOptions;
1312
- const dialog_style = dialog_options.style === undefined ? {} : dialog_options.style;
1313
- const dialog_components = dialog_options.components === undefined ? [] : dialog_options.components;
1369
+ const dialogOptions = options.dialogOptions === undefined ? {} : options.dialogOptions;
1370
+ const dialogStyle = dialogOptions.style === undefined ? {} : dialogOptions.style;
1371
+ const dialogComponents = dialogOptions.components === undefined ? [] : dialogOptions.components;
1314
1372
  // Пустой заголовок остаётся пустым: заготовка опции рисовала бы слово «undefined»
1315
- const dialog_title =
1316
- dialog_options.title === undefined || dialog_options.title === null ? "" : dialog_options.title;
1317
- let dialog = new Dialog({
1318
- width: dialog_style.width,
1319
- height: dialog_style.height,
1320
- closeOutSideClick: dialog_options.closeOutSideClick,
1373
+ const dialogTitle =
1374
+ dialogOptions.title === undefined || dialogOptions.title === null ? "" : dialogOptions.title;
1375
+ const dialog = new Dialog({
1376
+ width: dialogStyle.width,
1377
+ height: dialogStyle.height,
1378
+ closeOutSideClick: dialogOptions.closeOutSideClick,
1321
1379
  // Имя модального слоя для скринридера: без него диалог объявлялся бы безымянным
1322
- title: dialog_title,
1380
+ title: dialogTitle,
1323
1381
  });
1324
1382
  // HEADER
1325
- let header_dialog = new ContentBlock({
1383
+ const headerDialog = new ContentBlock({
1326
1384
  direction: "row",
1327
1385
  wrap: "nowrap",
1328
1386
  align: "center",
1329
1387
  justify: "space-between",
1330
1388
  disableMarginChild: true,
1331
1389
  });
1332
- header_dialog.setWidth("-webkit-fill-available");
1390
+ headerDialog.setWidth("-webkit-fill-available");
1333
1391
  // Заголовок — как у заголовков панелей оболочки (размер `--font_default_size`, вес 500
1334
1392
  // даёт `strong`): отдельные 14 pt в диалоге спорили с его содержимым
1335
- let title = new Label({
1336
- markdownText: `**${dialog_title}**`,
1393
+ const title = new Label({
1394
+ markdownText: `**${dialogTitle}**`,
1337
1395
  wordBreak: "normal",
1338
1396
  width: "max-content",
1339
1397
  });
1340
- let close_button = new Button({
1398
+ const closeButton = new Button({
1341
1399
  icon: Icons.NAVIGATION.CLOSE,
1342
1400
  label: I18n.t("common.close"),
1343
1401
  reverse: true,
1344
1402
  clickEvent: () => dialog.close(),
1345
1403
  });
1346
- header_dialog.add(title, close_button);
1347
- dialog.addToHeader(header_dialog);
1404
+ headerDialog.add(title, closeButton);
1405
+ dialog.addToHeader(headerDialog);
1348
1406
  // BODY
1349
- let body_dialog = new ContentBlock({
1350
- direction: dialog_style.direction,
1351
- wrap: dialog_style.wrap,
1352
- align: dialog_style.align,
1353
- justify: dialog_style.justify,
1407
+ const bodyDialog = new ContentBlock({
1408
+ direction: dialogStyle.direction,
1409
+ wrap: dialogStyle.wrap,
1410
+ align: dialogStyle.align,
1411
+ justify: dialogStyle.justify,
1354
1412
  });
1355
- body_dialog.setWidth("-webkit-fill-available");
1356
- body_dialog.add(...dialog_components);
1357
- dialog.addToBody(body_dialog);
1413
+ bodyDialog.setWidth("-webkit-fill-available");
1414
+ bodyDialog.add(...dialogComponents);
1415
+ dialog.addToBody(bodyDialog);
1358
1416
  // FOOTER
1359
1417
 
1360
1418
  //
1361
- this.#header_button = makeHeaderButtonBox(options);
1419
+ this.#headerButton = makeHeaderButtonBox(options);
1362
1420
  // Слой идёт в body при первом открытии, а не при сборке: заранее созданный
1363
1421
  // невидимый диалог оставался бы в документе на всё время жизни кнопки
1364
- this.#header_button.addEventListener("click", () => {
1422
+ this.#headerButton.addEventListener("click", () => {
1365
1423
  if (document.body.contains(dialog.set()) === false) document.body.appendChild(dialog.set());
1366
1424
  dialog.open();
1367
1425
  });
1368
1426
  }
1369
1427
 
1370
1428
  set() {
1371
- return this.#header_button;
1429
+ return this.#headerButton;
1372
1430
  }
1373
1431
  }
1374
1432
 
1375
1433
  class HeaderButton {
1376
- #header_button = undefined;
1434
+ #headerButton = undefined;
1377
1435
 
1378
1436
  constructor(
1379
1437
  options = {
@@ -1384,12 +1442,12 @@ class HeaderButton {
1384
1442
  clickEvent: () => {},
1385
1443
  }
1386
1444
  ) {
1387
- this.#header_button = makeHeaderButtonBox(options);
1388
- this.#header_button.addEventListener("click", options.clickEvent);
1445
+ this.#headerButton = makeHeaderButtonBox(options);
1446
+ this.#headerButton.addEventListener("click", options.clickEvent);
1389
1447
  }
1390
1448
 
1391
1449
  set() {
1392
- return this.#header_button;
1450
+ return this.#headerButton;
1393
1451
  }
1394
1452
  }
1395
1453