chuijs 4.0.2 → 4.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/CHANGELOG.md +597 -0
  2. package/README.md +43 -644
  3. package/framework/appLayout/default/chui_app_layout.js +602 -496
  4. package/framework/appLayout/default/global_style.css +288 -97
  5. package/framework/appLayout/default/main_theme_style.css +188 -101
  6. package/framework/components/chui_accordion/accordion.js +38 -35
  7. package/framework/components/chui_accordion/styles.css +13 -12
  8. package/framework/components/chui_action_sheet/action_sheet.js +93 -64
  9. package/framework/components/chui_action_sheet/styles.css +12 -5
  10. package/framework/components/chui_actions_bar/actions_bar.js +338 -0
  11. package/framework/components/chui_actions_bar/styles.css +139 -0
  12. package/framework/components/chui_affix/affix.js +16 -11
  13. package/framework/components/chui_affix/styles.css +5 -0
  14. package/framework/components/chui_alert/alert.js +34 -19
  15. package/framework/components/chui_alert/styles.css +7 -0
  16. package/framework/components/chui_avatar/avatar.js +36 -35
  17. package/framework/components/chui_avatar/styles.css +1 -1
  18. package/framework/components/chui_avatar_group/avatar_group.js +25 -23
  19. package/framework/components/chui_avatar_group/styles.css +1 -1
  20. package/framework/components/chui_backtop/backtop.js +60 -21
  21. package/framework/components/chui_backtop/styles.css +21 -0
  22. package/framework/components/chui_badge/badge.js +30 -28
  23. package/framework/components/chui_badge/styles.css +6 -6
  24. package/framework/components/chui_banner/banner.js +222 -0
  25. package/framework/components/chui_banner/styles.css +170 -0
  26. package/framework/components/chui_breadcrumb/breadcrumb.js +15 -13
  27. package/framework/components/chui_breadcrumb/styles.css +1 -1
  28. package/framework/components/chui_button/button.js +52 -36
  29. package/framework/components/chui_button/styles.css +7 -7
  30. package/framework/components/chui_calendar/calendar.js +18 -16
  31. package/framework/components/chui_card/card.js +52 -32
  32. package/framework/components/chui_card_scroll/card_scroll.js +169 -0
  33. package/framework/components/chui_card_scroll/styles.css +86 -0
  34. package/framework/components/chui_chat/chat.js +140 -79
  35. package/framework/components/chui_chat/styles.css +18 -7
  36. package/framework/components/chui_clipboard_history/clipboard_history.js +53 -39
  37. package/framework/components/chui_clipboard_history/styles.css +13 -9
  38. package/framework/components/chui_code/code.js +10 -10
  39. package/framework/components/chui_code/styles.css +4 -4
  40. package/framework/components/chui_code_editor/code_editor.js +73 -57
  41. package/framework/components/chui_code_editor/styles.css +6 -5
  42. package/framework/components/chui_command_palette/command_palette.js +87 -64
  43. package/framework/components/chui_command_palette/styles.css +5 -5
  44. package/framework/components/chui_console/console.js +13 -13
  45. package/framework/components/chui_console/styles.css +6 -3
  46. package/framework/components/chui_content_block/content_block.js +35 -21
  47. package/framework/components/chui_content_block/styles.css +2 -2
  48. package/framework/components/chui_context_menu/context_menu.js +171 -58
  49. package/framework/components/chui_context_menu/styles.css +82 -8
  50. package/framework/components/chui_custom_element/custom_element.js +25 -18
  51. package/framework/components/chui_data_grid/data_grid.js +633 -471
  52. package/framework/components/chui_data_grid/styles.css +24 -22
  53. package/framework/components/chui_descriptions/descriptions.js +34 -17
  54. package/framework/components/chui_descriptions/styles.css +1 -1
  55. package/framework/components/chui_details/details.js +42 -36
  56. package/framework/components/chui_details/styles.css +12 -11
  57. package/framework/components/chui_diff_viewer/diff_viewer.js +172 -147
  58. package/framework/components/chui_diff_viewer/styles.css +10 -10
  59. package/framework/components/chui_divider/divider.js +19 -17
  60. package/framework/components/chui_divider/styles.css +2 -2
  61. package/framework/components/chui_drawer/drawer.js +63 -38
  62. package/framework/components/chui_drawer/styles.css +18 -4
  63. package/framework/components/chui_drop_zone/drop_zone.js +37 -27
  64. package/framework/components/chui_drop_zone/styles.css +10 -6
  65. package/framework/components/chui_empty_state/empty_state.js +20 -15
  66. package/framework/components/chui_fieldset/fieldset.js +40 -23
  67. package/framework/components/chui_fieldset/styles.css +6 -6
  68. package/framework/components/chui_file_explorer/file_explorer.js +260 -189
  69. package/framework/components/chui_file_explorer/styles.css +8 -7
  70. package/framework/components/chui_form/form.js +40 -28
  71. package/framework/components/chui_form/styles.css +1 -1
  72. package/framework/components/chui_gantt/gantt.js +102 -78
  73. package/framework/components/chui_gantt/styles.css +5 -5
  74. package/framework/components/chui_graphs/graphs.js +376 -239
  75. package/framework/components/chui_graphs/styles.css +27 -6
  76. package/framework/components/chui_hero/hero.js +98 -37
  77. package/framework/components/chui_hero/styles.css +176 -7
  78. package/framework/components/chui_html_block/html_block.js +8 -8
  79. package/framework/components/chui_html_block/styles.css +3 -3
  80. package/framework/components/chui_hx/hx.js +3 -3
  81. package/framework/components/chui_hx/styles.css +13 -6
  82. package/framework/components/chui_icons/icons.js +2115 -2115
  83. package/framework/components/chui_icons/styles.css +4 -3
  84. package/framework/components/chui_inputs/chui_check_box/check_box.js +31 -19
  85. package/framework/components/chui_inputs/chui_check_box/styles.css +30 -21
  86. package/framework/components/chui_inputs/chui_chips/chips.js +141 -70
  87. package/framework/components/chui_inputs/chui_chips/styles.css +32 -7
  88. package/framework/components/chui_inputs/chui_color_picker/color_picker.js +55 -38
  89. package/framework/components/chui_inputs/chui_color_picker/styles.css +7 -7
  90. package/framework/components/chui_inputs/chui_combo_box/combo_box.js +127 -109
  91. package/framework/components/chui_inputs/chui_combo_box/styles.css +23 -21
  92. package/framework/components/chui_inputs/chui_date/date.js +161 -127
  93. package/framework/components/chui_inputs/chui_date/styles.css +20 -19
  94. package/framework/components/chui_inputs/chui_date_range/date_range.js +61 -50
  95. package/framework/components/chui_inputs/chui_date_range/styles.css +3 -3
  96. package/framework/components/chui_inputs/chui_datetime/datetime.js +47 -39
  97. package/framework/components/chui_inputs/chui_datetime/styles.css +3 -3
  98. package/framework/components/chui_inputs/chui_email/email.js +48 -36
  99. package/framework/components/chui_inputs/chui_email/styles.css +11 -11
  100. package/framework/components/chui_inputs/chui_file/file.js +71 -44
  101. package/framework/components/chui_inputs/chui_file/styles.css +23 -19
  102. package/framework/components/chui_inputs/chui_hotkey/hotkey.js +101 -76
  103. package/framework/components/chui_inputs/chui_hotkey/styles.css +6 -6
  104. package/framework/components/chui_inputs/chui_masked/masked.js +83 -72
  105. package/framework/components/chui_inputs/chui_masked/styles.css +5 -5
  106. package/framework/components/chui_inputs/chui_mention/mention.js +87 -40
  107. package/framework/components/chui_inputs/chui_mention/styles.css +14 -8
  108. package/framework/components/chui_inputs/chui_multi_combo_box/multi_combo_box.js +242 -212
  109. package/framework/components/chui_inputs/chui_multi_combo_box/styles.css +23 -15
  110. package/framework/components/chui_inputs/chui_number/number.js +84 -70
  111. package/framework/components/chui_inputs/chui_number/styles.css +13 -13
  112. package/framework/components/chui_inputs/chui_otp/otp.js +71 -62
  113. package/framework/components/chui_inputs/chui_otp/styles.css +5 -5
  114. package/framework/components/chui_inputs/chui_password/password.js +72 -56
  115. package/framework/components/chui_inputs/chui_password/styles.css +15 -15
  116. package/framework/components/chui_inputs/chui_radio_button/radio_button.js +37 -25
  117. package/framework/components/chui_inputs/chui_radio_button/styles.css +22 -22
  118. package/framework/components/chui_inputs/chui_rating/rating.js +54 -50
  119. package/framework/components/chui_inputs/chui_rating/styles.css +3 -3
  120. package/framework/components/chui_inputs/chui_select_box/select_box.js +132 -108
  121. package/framework/components/chui_inputs/chui_select_box/styles.css +21 -21
  122. package/framework/components/chui_inputs/chui_slider/slider.js +41 -38
  123. package/framework/components/chui_inputs/chui_slider/styles.css +7 -5
  124. package/framework/components/chui_inputs/chui_text/styles.css +14 -14
  125. package/framework/components/chui_inputs/chui_text/text.js +69 -52
  126. package/framework/components/chui_inputs/chui_text_area/styles.css +14 -14
  127. package/framework/components/chui_inputs/chui_text_area/text_area.js +59 -36
  128. package/framework/components/chui_inputs/chui_time/styles.css +18 -20
  129. package/framework/components/chui_inputs/chui_time/time.js +71 -49
  130. package/framework/components/chui_inputs/chui_toggle/styles.css +13 -6
  131. package/framework/components/chui_inputs/chui_toggle/toggle.js +27 -27
  132. package/framework/components/chui_json_editor/json_editor.js +137 -127
  133. package/framework/components/chui_json_editor/styles.css +5 -5
  134. package/framework/components/chui_json_viewer/json_viewer.js +133 -114
  135. package/framework/components/chui_json_viewer/styles.css +9 -8
  136. package/framework/components/chui_kanban/kanban.js +78 -60
  137. package/framework/components/chui_kanban/styles.css +8 -4
  138. package/framework/components/chui_kbd/kbd.js +12 -10
  139. package/framework/components/chui_kbd/styles.css +1 -1
  140. package/framework/components/chui_label/label.js +31 -29
  141. package/framework/components/chui_label/styles.css +3 -3
  142. package/framework/components/chui_link/link.js +36 -29
  143. package/framework/components/chui_link/styles.css +11 -4
  144. package/framework/components/chui_list/list.js +42 -38
  145. package/framework/components/chui_list/styles.css +4 -3
  146. package/framework/components/chui_markdown_editor/markdown_editor.js +125 -109
  147. package/framework/components/chui_markdown_editor/styles.css +5 -5
  148. package/framework/components/chui_media/audio.js +457 -364
  149. package/framework/components/chui_media/audio_styles.css +74 -37
  150. package/framework/components/chui_media/image.js +99 -64
  151. package/framework/components/chui_media/image_compare.js +86 -55
  152. package/framework/components/chui_media/image_compare_styles.css +5 -5
  153. package/framework/components/chui_media/image_styles.css +4 -4
  154. package/framework/components/chui_media/video.js +264 -171
  155. package/framework/components/chui_media/video_styles.css +57 -24
  156. package/framework/components/chui_menu_bar/menu_bar.js +67 -65
  157. package/framework/components/chui_menu_bar/styles.css +14 -12
  158. package/framework/components/chui_message_bar/message_bar.js +551 -0
  159. package/framework/components/chui_message_bar/styles.css +248 -0
  160. package/framework/components/chui_modal/modal.js +81 -56
  161. package/framework/components/chui_modal/styles.css +3 -2
  162. package/framework/components/chui_notification/notification.js +107 -69
  163. package/framework/components/chui_notification/notification_download.js +28 -77
  164. package/framework/components/chui_notification/notification_download_card.js +115 -0
  165. package/framework/components/chui_notification/notification_download_progress.css +5 -4
  166. package/framework/components/chui_notification/notification_download_progress.js +73 -105
  167. package/framework/components/chui_notification/notification_update.js +44 -37
  168. package/framework/components/chui_notification/status_plate.css +3 -3
  169. package/framework/components/chui_notification/status_plate.js +2 -2
  170. package/framework/components/chui_notification/styles.css +6 -1
  171. package/framework/components/chui_notification/styles_download.css +2 -2
  172. package/framework/components/chui_notification/styles_update.css +3 -3
  173. package/framework/components/chui_org_chart/org_chart.js +101 -62
  174. package/framework/components/chui_org_chart/styles.css +2 -2
  175. package/framework/components/chui_pagination/pagination.js +65 -30
  176. package/framework/components/chui_pagination/styles.css +1 -1
  177. package/framework/components/chui_paragraph/paragraph.js +2 -2
  178. package/framework/components/chui_paragraph/styles.css +3 -3
  179. package/framework/components/chui_popover/popover.js +77 -50
  180. package/framework/components/chui_popover/styles.css +1 -1
  181. package/framework/components/chui_popups/popups.js +210 -181
  182. package/framework/components/chui_popups/styles.css +7 -7
  183. package/framework/components/chui_print_button/print_button.js +48 -33
  184. package/framework/components/chui_progress_bar/progress_bar.js +49 -19
  185. package/framework/components/chui_progress_bar/styles.css +6 -6
  186. package/framework/components/chui_progress_ring/progress_ring.js +32 -30
  187. package/framework/components/chui_progress_ring/styles.css +1 -1
  188. package/framework/components/chui_qrcode/qrcode.js +85 -41
  189. package/framework/components/chui_query_builder/query_builder.js +263 -170
  190. package/framework/components/chui_query_builder/styles.css +4 -4
  191. package/framework/components/chui_radio_group/radio_group.js +36 -26
  192. package/framework/components/chui_radio_group/styles.css +2 -2
  193. package/framework/components/chui_result/result.js +41 -31
  194. package/framework/components/chui_result/styles.css +3 -3
  195. package/framework/components/chui_scheduler/scheduler.js +126 -104
  196. package/framework/components/chui_scheduler/styles.css +11 -7
  197. package/framework/components/chui_screen_spinner/screen_spinner.js +167 -0
  198. package/framework/components/chui_screen_spinner/styles.css +49 -0
  199. package/framework/components/chui_segmented_control/segmented_control.js +90 -27
  200. package/framework/components/chui_segmented_control/styles.css +32 -6
  201. package/framework/components/chui_settings/settings.js +160 -87
  202. package/framework/components/chui_settings/styles.css +4 -4
  203. package/framework/components/chui_shortcut_help/shortcut_help.js +82 -61
  204. package/framework/components/chui_sidebar_nav/sidebar_nav.js +132 -53
  205. package/framework/components/chui_sidebar_nav/styles.css +9 -3
  206. package/framework/components/chui_skeleton/skeleton.js +29 -24
  207. package/framework/components/chui_slideshow/slideshow.js +122 -103
  208. package/framework/components/chui_slideshow/styles.css +22 -16
  209. package/framework/components/chui_spinner/spinner.js +17 -17
  210. package/framework/components/chui_spinner/styles.css +0 -1
  211. package/framework/components/chui_split_button/split_button.js +53 -30
  212. package/framework/components/chui_split_button/styles.css +22 -6
  213. package/framework/components/chui_splitter/splitter.js +46 -39
  214. package/framework/components/chui_splitter/styles.css +2 -2
  215. package/framework/components/chui_statistic/statistic.js +111 -84
  216. package/framework/components/chui_status_bar/status_bar.js +11 -9
  217. package/framework/components/chui_status_bar/styles.css +2 -2
  218. package/framework/components/chui_stepper/stepper.js +230 -54
  219. package/framework/components/chui_stepper/styles.css +79 -14
  220. package/framework/components/chui_sticky_bar/sticky_bar.js +53 -17
  221. package/framework/components/chui_sticky_bar/styles.css +11 -7
  222. package/framework/components/chui_tabs/chui_tabs.js +100 -46
  223. package/framework/components/chui_tabs/styles.css +43 -15
  224. package/framework/components/chui_text_editor/styles.css +24 -23
  225. package/framework/components/chui_text_editor/text_editor.js +103 -79
  226. package/framework/components/chui_text_editor/text_editor_panel.js +428 -367
  227. package/framework/components/chui_theme_toggle/theme_toggle.js +46 -25
  228. package/framework/components/chui_timeline/styles.css +3 -1
  229. package/framework/components/chui_timeline/timeline.js +14 -12
  230. package/framework/components/chui_toast_queue/styles.css +11 -11
  231. package/framework/components/chui_toast_queue/toast_queue.js +82 -73
  232. package/framework/components/chui_toolbar/styles.css +2 -2
  233. package/framework/components/chui_toolbar/toolbar.js +20 -13
  234. package/framework/components/chui_tooltip/styles.css +2 -2
  235. package/framework/components/chui_tooltip/tooltip.js +40 -27
  236. package/framework/components/chui_tour/styles.css +12 -7
  237. package/framework/components/chui_tour/tour.js +132 -98
  238. package/framework/components/chui_transfer/styles.css +8 -7
  239. package/framework/components/chui_transfer/transfer.js +143 -123
  240. package/framework/components/chui_tree_table/styles.css +4 -4
  241. package/framework/components/chui_tree_table/tree_table.js +88 -74
  242. package/framework/components/chui_tree_view/styles.css +14 -10
  243. package/framework/components/chui_tree_view/tree_view.js +298 -253
  244. package/framework/components/chui_user_bar/styles.css +145 -0
  245. package/framework/components/chui_user_bar/user_bar.js +402 -0
  246. package/framework/components/chui_user_list_bar/styles.css +173 -0
  247. package/framework/components/chui_user_list_bar/user_list_bar.js +485 -0
  248. package/framework/components/chui_watermark/watermark.js +41 -33
  249. package/framework/components/chui_webview/styles.css +2 -2
  250. package/framework/components/chui_webview/webview.js +64 -52
  251. package/framework/components/chui_write_bar/styles.css +172 -0
  252. package/framework/components/chui_write_bar/write_bar.js +320 -0
  253. package/framework/main_window.js +1234 -0
  254. package/framework/modules/chui_a11y/chui_a11y.js +153 -123
  255. package/framework/modules/chui_animations/animations.js +105 -43
  256. package/framework/modules/chui_animations/motion.js +120 -0
  257. package/framework/modules/chui_animations/styles.css +37 -4
  258. package/framework/modules/chui_bridge/preload.js +36 -25
  259. package/framework/modules/chui_functions.js +69 -63
  260. package/framework/modules/chui_highlight/chui_highlight.js +25 -22
  261. package/framework/modules/chui_highlight/styles.css +13 -10
  262. package/framework/modules/chui_i18n/chui_i18n.js +12 -10
  263. package/framework/modules/chui_i18n/locales/a11y.js +5 -5
  264. package/framework/modules/chui_i18n/locales/app_layout.js +5 -5
  265. package/framework/modules/chui_i18n/locales/calendar.js +3 -3
  266. package/framework/modules/chui_i18n/locales/common.js +3 -3
  267. package/framework/modules/chui_i18n/locales/data.js +36 -26
  268. package/framework/modules/chui_i18n/locales/editors.js +40 -13
  269. package/framework/modules/chui_i18n/locales/feedback.js +65 -3
  270. package/framework/modules/chui_i18n/locales/forms.js +8 -8
  271. package/framework/modules/chui_i18n/locales/graphics.js +8 -8
  272. package/framework/modules/chui_i18n/locales/media.js +4 -4
  273. package/framework/modules/chui_i18n/locales/navigation.js +12 -8
  274. package/framework/modules/chui_i18n/locales/validate.js +3 -3
  275. package/framework/modules/chui_json/chui_json.js +11 -11
  276. package/framework/modules/chui_layout/chui_layout.js +12 -10
  277. package/framework/modules/chui_logger/chui_logger.js +47 -28
  278. package/framework/modules/chui_page/page.js +39 -33
  279. package/framework/modules/chui_palette/palette.js +61 -0
  280. package/framework/modules/chui_qr/chui_qr.js +179 -88
  281. package/framework/modules/chui_scrollbar/chui_scrollbar.js +79 -68
  282. package/framework/modules/chui_scrollbar/styles.css +4 -2
  283. package/framework/modules/chui_services/chui_channels.js +27 -13
  284. package/framework/modules/chui_services/chui_services.js +47 -39
  285. package/framework/modules/chui_services/desktop.js +108 -101
  286. package/framework/modules/chui_sortable/chui_sortable.js +33 -28
  287. package/framework/modules/chui_validate/chui_validate.js +10 -7
  288. package/framework/tray_bar/chui_menu_items.js +136 -46
  289. package/index.d.ts +5046 -0
  290. package/index.js +199 -1254
  291. package/package.json +81 -68
  292. package/framework/components/telegram_bot/chui_telegram_bot.js +0 -184
@@ -1,42 +1,43 @@
1
- const {Animation} = require('../../modules/chui_animations/animations');
2
- const {Icon, Icons} = require('../../components/chui_icons/icons');
3
- const {Button} = require("../../components/chui_button/button");
1
+ const { Animation } = require("../../modules/chui_animations/animations");
2
+ const { reducedMotion, tokenTime } = require("../../modules/chui_animations/motion");
3
+ const { Icon, Icons } = require("../../components/chui_icons/icons");
4
+ const { Button } = require("../../components/chui_button/button");
4
5
  // НАСТРОЙКИ
5
- const {Dialog} = require("../../components/chui_modal/modal");
6
- const {Label} = require("../../components/chui_label/label");
7
- const {ContentBlock} = require("../../components/chui_content_block/content_block");
8
- const {Popup} = require("../../components/chui_popups/popups");
9
- const {UpdateNotification} = require("../../components/chui_notification/notification_update");
10
- const {ipcRenderer} = require("electron");
11
- const chui_functions = require('../../modules/chui_functions');
12
- const {DownloadNotification} = require("../../components/chui_notification/notification_download");
13
- const {Log} = require("../../../framework/modules/chui_logger/chui_logger");
14
- const {MenuKeyboard, makeKeyboardButton} = require('../../modules/chui_a11y/chui_a11y');
15
- const {LAYOUT_ROLE, LayoutRefs, registerLayout, requireLayoutNode} = require('../../modules/chui_layout/chui_layout');
16
- const {BRIDGE_CHANNELS} = require('../../modules/chui_services/chui_channels');
17
- const {OverlayScrollbars} = require('../../modules/chui_scrollbar/chui_scrollbar');
18
- const {MenuBar} = require('../../components/chui_menu_bar/menu_bar');
19
- const {Avatar} = require('../../components/chui_avatar/avatar');
20
- const {EmptyState} = require('../../components/chui_empty_state/empty_state');
21
- const {I18n} = require('../../modules/chui_i18n/chui_i18n');
6
+ const { Dialog } = require("../../components/chui_modal/modal");
7
+ const { Label } = require("../../components/chui_label/label");
8
+ const { ContentBlock } = require("../../components/chui_content_block/content_block");
9
+ const { Popup } = require("../../components/chui_popups/popups");
10
+ const { UpdateNotification } = require("../../components/chui_notification/notification_update");
11
+ const { ipcRenderer } = require("electron");
12
+ const chuiFunctions = require("../../modules/chui_functions");
13
+ const { DownloadNotification } = require("../../components/chui_notification/notification_download");
14
+ const { Log } = require("../../../framework/modules/chui_logger/chui_logger");
15
+ const { MenuKeyboard, makeKeyboardButton } = require("../../modules/chui_a11y/chui_a11y");
16
+ const { LAYOUT_ROLE, LayoutRefs, registerLayout, requireLayoutNode } = require("../../modules/chui_layout/chui_layout");
17
+ const { BRIDGE_CHANNELS } = require("../../modules/chui_services/chui_channels");
18
+ const { OverlayScrollbars } = require("../../modules/chui_scrollbar/chui_scrollbar");
19
+ const { MenuBar } = require("../../components/chui_menu_bar/menu_bar");
20
+ const { Avatar } = require("../../components/chui_avatar/avatar");
21
+ const { EmptyState } = require("../../components/chui_empty_state/empty_state");
22
+ 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,
35
36
  title: page.getTitle(),
36
- page: page
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,24 +86,24 @@ 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
- center.innerHTML = '';
92
+ center.innerHTML = "";
92
93
  // Содержимое собирает сама страница (`Page#render`), оболочка в него не подмешивает
93
94
  // ничего: узел `page_title` (был здесь) дублировал заголовок, который страница
94
95
  // рисует сама, а `Page#render` отдаёт один и тот же узел — повторный переход на тот
95
96
  // же экземпляр страницы (кнопка «Назад» из `Page#setBackButton`) добавлял второй
96
97
  // заголовок. Место названия — шапка (`setPageName`).
97
98
  center.appendChild(page.render());
98
- const _page = center.querySelector('page');
99
+ const _page = center.querySelector("page");
99
100
  new Animation(_page).fadeIn();
100
101
  // Стирания `center.removeAttribute("style")` здесь больше нет: инлайновых
101
102
  // стилей на `center` не ставит ни одна часть оболочки, а стирать чужое
102
103
  // (если появится) — это ловушка, на которой уже терялись `display: flex`
103
104
  applyPageMenuBar(requireLayoutNode(LAYOUT_ROLE.HEADER), page.getMenuBar());
104
105
 
105
- this.route(page)
106
+ this.route(page);
106
107
  }
107
108
  }
108
109
 
@@ -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
 
@@ -161,23 +162,26 @@ function makeHeaderButtonBox(options = { title: undefined, icon: undefined, reve
161
162
  * подобраны под вид набора; цвет наследуется от кнопки через `currentColor`.
162
163
  */
163
164
  const WINDOW_ICONS = {
164
- minimize: '<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M4.5 7.5 L10 12.5 L15.5 7.5" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>',
165
- maximize: '<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M4.5 12.5 L10 7.5 L15.5 12.5" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>',
166
- restore: '<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M10 5 L15 10 L10 15 L5 10 Z" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>',
165
+ minimize:
166
+ '<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M4.5 7.5 L10 12.5 L15.5 7.5" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>',
167
+ maximize:
168
+ '<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M4.5 12.5 L10 7.5 L15.5 12.5" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>',
169
+ restore:
170
+ '<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M10 5 L15 10 L10 15 L5 10 Z" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>',
167
171
  close: '<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M5 5 L15 15 M15 5 L5 15" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>',
168
172
  };
169
173
 
170
174
  class WindowControls {
171
175
  // Управление кнопками
172
- #box = document.createElement("wc_box")
173
- #close = document.createElement("wc_close")
174
- #maximize = document.createElement("wc_maximize")
175
- #minimize = document.createElement("wc_minimize")
176
+ #box = document.createElement("wc_box");
177
+ #close = document.createElement("wc_close");
178
+ #maximize = document.createElement("wc_maximize");
179
+ #minimize = document.createElement("wc_minimize");
176
180
  // Подписка на нересайзящееся окно: set() вызывается и из конструктора Header, и из
177
181
  // addWC(), поэтому в set() слушатель ipcRenderer накапливался.
178
- #resizable_false_event = () => this.#maximize.remove();
182
+ #resizableFalseEvent = () => this.#maximize.remove();
179
183
  // Номер запроса состояния гасит ответы, пришедшие не в том порядке
180
- #sync_request = 0;
184
+ #syncRequest = 0;
181
185
  constructor() {
182
186
  // Скрипт окна может быть preload'ом и выполняться до документа: здесь документ уже настоящий,
183
187
  // поэтому `dir`/`lang` (и зеркалирование логических свойств CSS) встают гарантированно
@@ -188,15 +192,15 @@ class WindowControls {
188
192
  this.#setButton(this.#minimize, I18n.t("appLayout.window.minimize"), "minimize");
189
193
  // «Развернуть» — шеврон вверх, зеркало «свернуть»: пара читается как одна ось
190
194
  this.#setButton(this.#maximize, I18n.t("appLayout.window.maximize"), "maximize");
191
- this.#close.addEventListener("click", () => this.#windowAction("close"))
192
- this.#minimize.addEventListener("click", () => this.#windowAction("minimize"))
195
+ this.#close.addEventListener("click", () => this.#windowAction("close"));
196
+ this.#minimize.addEventListener("click", () => this.#windowAction("minimize"));
193
197
  // Разворот и восстановление — одно действие: решение принимает окно, а не кнопка
194
- this.#maximize.addEventListener("click", () => this.#windowAction("toggle-maximize"))
198
+ this.#maximize.addEventListener("click", () => this.#windowAction("toggle-maximize"));
195
199
  // Разворот приходит не только от нашей кнопки (двойной клик по полосе, меню окна),
196
200
  // поэтому состояние читаем по факту изменения размера окна
197
- window.addEventListener("resize", this.#sync_maximized_event)
198
- this.#syncMaximized()
199
- ipcRenderer.on("chui_resizable_false", this.#resizable_false_event)
201
+ window.addEventListener("resize", this.#syncMaximizedEvent);
202
+ this.#syncMaximized();
203
+ ipcRenderer.on("chui_resizable_false", this.#resizableFalseEvent);
200
204
  }
201
205
  /** Кнопка окна — значок с именем для скринридера, фокусом и активацией с клавиатуры.
202
206
  * Значок рисуется линиями (`WINDOW_ICONS`), размер задаёт CSS (см. global_style.css) */
@@ -233,9 +237,9 @@ class WindowControls {
233
237
  // Состояние приходит асинхронно, поэтому ответ устаревшего запроса отбрасывается по номеру:
234
238
  // иначе быстрая пара «развернуть/восстановить» могла бы применить ответ в обратном порядке
235
239
  #syncMaximized() {
236
- const request = ++this.#sync_request;
240
+ const request = ++this.#syncRequest;
237
241
  this.#windowState().then((state) => {
238
- if (request !== this.#sync_request) return;
242
+ if (request !== this.#syncRequest) return;
239
243
  const maximized = state.isMaximized === true;
240
244
  const label = maximized ? I18n.t("appLayout.window.restore") : I18n.t("appLayout.window.maximize");
241
245
  this.#maximize.classList.toggle("wc_maximized", maximized);
@@ -244,39 +248,39 @@ class WindowControls {
244
248
  this.#maximize.setAttribute("aria-label", label);
245
249
  });
246
250
  }
247
- #sync_maximized_event = () => this.#syncMaximized();
248
- set(pos_bool = Boolean()) {
249
- if (pos_bool) {
250
- this.#box.appendChild(this.#close)
251
- this.#box.appendChild(this.#maximize)
252
- this.#box.appendChild(this.#minimize)
251
+ #syncMaximizedEvent = () => this.#syncMaximized();
252
+ set(isEnabled = Boolean()) {
253
+ if (isEnabled) {
254
+ this.#box.appendChild(this.#close);
255
+ this.#box.appendChild(this.#maximize);
256
+ this.#box.appendChild(this.#minimize);
253
257
  } else {
254
- this.#box.appendChild(this.#minimize)
255
- this.#box.appendChild(this.#maximize)
256
- this.#box.appendChild(this.#close)
258
+ this.#box.appendChild(this.#minimize);
259
+ this.#box.appendChild(this.#maximize);
260
+ this.#box.appendChild(this.#close);
257
261
  }
258
262
  return this.#box;
259
263
  }
260
264
  }
261
265
 
262
266
  class Header {
263
- #wc_box = new WindowControls();
267
+ #wcBox = new WindowControls();
264
268
  #header = document.createElement("header");
265
- #header_main = document.createElement("header_main");
266
- #header_left_box = document.createElement("header_left_box");
267
- #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");
268
272
  // Высота полосы измеряется, а не предполагается — см. #observeHeight
269
- #height_observer = undefined;
270
- #height_frame = 0;
271
- #published_height = 0;
273
+ #heightObserver = undefined;
274
+ #heightFrame = 0;
275
+ #publishedHeight = 0;
272
276
  constructor(layout) {
273
277
  layout.set(LAYOUT_ROLE.HEADER, this.#header);
274
278
  this.#header.id = "header";
275
- this.#header_main.id = "header_main";
276
- this.#header_main.appendChild(this.#header_left_box);
277
- this.#header_main.appendChild(this.#header_right_box);
278
- this.#header_main.appendChild(this.#wc_box.set(false));
279
- 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);
280
284
  this.#observeHeight();
281
285
  }
282
286
  /*
@@ -289,8 +293,8 @@ class Header {
289
293
  * Теперь высота измеряется и публикуется, поэтому токен верен при любой полосе.
290
294
  */
291
295
  #observeHeight() {
292
- this.#height_observer = new ResizeObserver(() => this.#publishHeight());
293
- this.#height_observer.observe(this.#header);
296
+ this.#heightObserver = new ResizeObserver(() => this.#publishHeight());
297
+ this.#heightObserver.observe(this.#header);
294
298
  this.#publishHeight();
295
299
  }
296
300
  /*
@@ -302,40 +306,41 @@ class Header {
302
306
  * записи без проверки крутят пересчёт на ровном месте.
303
307
  */
304
308
  #publishHeight() {
305
- if (this.#height_frame !== 0) return;
306
- this.#height_frame = requestAnimationFrame(() => {
307
- this.#height_frame = 0;
309
+ if (this.#heightFrame !== 0) return;
310
+ this.#heightFrame = requestAnimationFrame(() => {
311
+ this.#heightFrame = 0;
308
312
  // offsetHeight включает черту снизу: она принадлежит полосе, от неё и считается отступ
309
313
  const height = this.#header.offsetHeight;
310
- if (height === 0 || height === this.#published_height) return;
311
- this.#published_height = height;
314
+ if (height === 0 || height === this.#publishedHeight) return;
315
+ this.#publishedHeight = height;
312
316
  document.documentElement.style.setProperty("--header_height", `${height}px`);
313
317
  });
314
318
  }
315
319
  addWC(boolean = Boolean()) {
316
- this.#header_main.insertBefore(this.#wc_box.set(boolean), this.#header_main.firstChild);
320
+ this.#headerMain.insertBefore(this.#wcBox.set(boolean), this.#headerMain.firstChild);
317
321
  }
318
322
  addToLeftBeforeTitle(...components) {
319
- for (let component of components) this.#header_left_box.insertBefore(component, this.#header_left_box.firstChild);
323
+ for (const component of components) this.#headerLeftBox.insertBefore(component, this.#headerLeftBox.firstChild);
320
324
  }
321
325
  /** Прежнее имя с опечаткой (`Tittle`): оставлено синонимом, чтобы не ломать чужие вызовы */
322
326
  addToLeftBeforeTittle(...components) {
323
327
  this.addToLeftBeforeTitle(...components);
324
328
  }
325
329
  addToLeft(...components) {
326
- for (let component of components) {
327
- this.#header_left_box.appendChild(component)
330
+ for (const component of components) {
331
+ this.#headerLeftBox.appendChild(component);
328
332
  }
329
333
  }
330
334
  addToRight(...components) {
331
- for (let component of components) 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);
332
337
  }
333
338
  removeToRight(...components) {
334
- for (let component of components) {
339
+ for (const component of components) {
335
340
  // У меню профиля панель лежит в body, а не в шапке (см. UserProfile),
336
341
  // поэтому снятие кнопки обязано убирать и панель: иначе в body копились бы невидимые меню
337
342
  UserProfile.destroy(component);
338
- this.#header_right_box.removeChild(component);
343
+ this.#headerRightBox.removeChild(component);
339
344
  }
340
345
  }
341
346
  set() {
@@ -344,7 +349,7 @@ class Header {
344
349
  }
345
350
 
346
351
  class Center {
347
- #center = document.createElement('center');
352
+ #center = document.createElement("center");
348
353
  constructor(layout) {
349
354
  this.#center.id = "center";
350
355
  layout.set(LAYOUT_ROLE.CENTER, this.#center);
@@ -356,30 +361,32 @@ class Center {
356
361
 
357
362
  class AppMenu {
358
363
  #routeList = [];
359
- #appMenu = document.createElement('app_menu');
364
+ #appMenu = document.createElement("app_menu");
360
365
  #appMenuWidth = 400;
361
366
  #appMenuOffset = 25;
362
- get #appMenuSlide() { return this.#appMenuWidth + this.#appMenuOffset; }
363
- #routeViews = document.createElement('route_views');
364
- #appMenuButton = document.createElement('app_menu_button');
367
+ get #appMenuSlide() {
368
+ return this.#appMenuWidth + this.#appMenuOffset;
369
+ }
370
+ #routeViews = document.createElement("route_views");
371
+ #appMenuButton = document.createElement("app_menu_button");
365
372
  // Шапка панели: плитка со значком и заголовок — панель читается выдвижным ящиком
366
- #app_menu_header = document.createElement("app_menu_header");
367
- #app_menu_header_icon = document.createElement("app_menu_header_icon");
368
- #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");
369
376
  // Блок поиска: своя поверхность со значком, а не одинокий ввод на фоне панели
370
- #app_menu_search_main = document.createElement("app_menu_search_main");
371
- #app_menu_search_box = document.createElement("app_menu_search_box");
372
- #app_menu_search_icon = document.createElement("app_menu_search_icon");
373
- #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");
374
381
  // Подсказка, когда фильтр не нашёл ни одного пункта
375
- #app_menu_empty = document.createElement("app_menu_empty");
382
+ #appMenuEmpty = document.createElement("app_menu_empty");
376
383
  // ===
377
- #auto_close = false;
378
- #search_enabled = false;
384
+ #autoClose = false;
385
+ #searchEnabled = false;
379
386
  #routeKeyboard = undefined;
380
387
  #header = undefined;
381
388
  #center = undefined;
382
- #page_name = document.createElement('page_name');
389
+ #pageName = document.createElement("page_name");
383
390
  constructor(header, center, layout) {
384
391
  this.#header = header;
385
392
  this.#center = center;
@@ -389,9 +396,9 @@ class AppMenu {
389
396
  this.#appMenu.style.width = `${this.#appMenuWidth}px`;
390
397
  this.#appMenu.style.left = `calc(-${this.#appMenuSlide}px)`;
391
398
  //
392
- this.#appMenuButton.id = "appMenuButton"
399
+ this.#appMenuButton.id = "appMenuButton";
393
400
  // Список пунктов — тот узел, которым управляет кнопка: `aria-controls` ссылается на него
394
- this.#routeViews.id = "chui_app_menu"
401
+ this.#routeViews.id = "chui_app_menu";
395
402
  // Значок — украшение: без подписи имя кнопки складывалось из лигатуры («menu»)
396
403
  this.#appMenuButton.setAttribute("aria-label", I18n.t("appLayout.menu"));
397
404
  this.#appMenuButton.setAttribute("title", I18n.t("appLayout.menu"));
@@ -399,37 +406,37 @@ class AppMenu {
399
406
  this.#appMenuButton.addEventListener("click", () => {
400
407
  if (this.#appMenuButton.classList.contains("app_menu_button_active")) this.#close();
401
408
  else this.#open();
402
- })
403
- header.set().addEventListener('click', (e) => {
409
+ });
410
+ header.set().addEventListener("click", (e) => {
404
411
  if (e.target !== this.#appMenuButton) this.#close();
405
- })
406
- center.addEventListener('click', (e) => {
412
+ });
413
+ center.addEventListener("click", (e) => {
407
414
  if (!this.#appMenu.contains(e.target)) {
408
415
  if (this.#appMenu.style.transform === `translateX(${this.#appMenuSlide}px)`) this.#close();
409
416
  }
410
- })
411
- this.#page_name.id = "page_name"
412
- layout.set(LAYOUT_ROLE.PAGE_NAME, this.#page_name);
413
- header.addToLeft(this.#appMenuButton, this.#page_name);
414
- this.#app_menu_header_icon.appendChild(new Icon(Icons.NAVIGATION.MENU_OPEN, "16px").set());
417
+ });
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());
415
422
  // Плитка и значок в ней — украшение: имя панели уже есть в заголовке
416
- this.#app_menu_header_icon.setAttribute("aria-hidden", "true");
417
- 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");
418
425
  // Список пунктов назван головкой панели: скринридер читает «Меню», а не безымянный список
419
- this.#app_menu_header_title.id = "chui_app_menu_title";
420
- this.#routeViews.setAttribute("aria-labelledby", this.#app_menu_header_title.id);
421
- this.#app_menu_header.appendChild(this.#app_menu_header_icon)
422
- this.#app_menu_header.appendChild(this.#app_menu_header_title)
423
- 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");
424
431
  // Подсказка лежит внутри списка, но пунктом не является: клавиатура меню её не видит (itemSelector "route")
425
- this.#routeViews.appendChild(this.#app_menu_empty)
426
- this.#appMenu.appendChild(this.#app_menu_header)
427
- this.#appMenu.appendChild(this.#routeViews)
432
+ this.#routeViews.appendChild(this.#appMenuEmpty);
433
+ this.#appMenu.appendChild(this.#appMenuHeader);
434
+ this.#appMenu.appendChild(this.#routeViews);
428
435
  // Клавиатура и aria меню маршрутов: стрелки по пунктам, Escape закрывает и возвращает фокус на кнопку
429
436
  this.#routeKeyboard = new MenuKeyboard(this.#routeViews, {
430
437
  itemSelector: "route",
431
438
  onEscape: () => this.#close(),
432
- trigger: this.#appMenuButton
439
+ trigger: this.#appMenuButton,
433
440
  });
434
441
  makeKeyboardButton(this.#appMenuButton);
435
442
  // Закрытая панель объявляется закрытой сразу: иначе у кнопки не было бы aria-expanded,
@@ -464,20 +471,20 @@ class AppMenu {
464
471
  this.#routeKeyboard.setExpanded(false);
465
472
  }
466
473
  enableSearchInput() {
467
- if (this.#search_enabled) return;
468
- this.#search_enabled = true;
469
- this.#app_menu_search_input.classList.add("app_menu_search_input")
470
- this.#app_menu_search_input.placeholder = I18n.t("appLayout.search.placeholder")
471
- 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"));
472
479
  // Подсветку поля даёт :focus-within на поверхности поиска — инлайновых стилей больше нет
473
- this.#app_menu_search_input.addEventListener("input", (event) => {
474
- this.#filterRoutes(event.target.value)
475
- })
476
- this.#app_menu_search_icon.innerHTML = new Icon(Icons.ACTIONS.SEARCH, "15px").getHTML();
477
- this.#app_menu_search_box.appendChild(this.#app_menu_search_icon)
478
- this.#app_menu_search_box.appendChild(this.#app_menu_search_input)
479
- this.#app_menu_search_main.appendChild(this.#app_menu_search_box)
480
- this.#appMenu.insertBefore(this.#app_menu_search_main, this.#routeViews)
480
+ this.#appMenuSearchInput.addEventListener("input", (event) => {
481
+ this.#filterRoutes(event.target.value);
482
+ });
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);
481
488
  }
482
489
  // Пункты списка: подсказка о пустом результате лежит там же, поэтому фильтруем по тегу
483
490
  #routes() {
@@ -493,15 +500,15 @@ class AppMenu {
493
500
  item.style.display = matches ? "" : "none";
494
501
  if (matches) visible++;
495
502
  }
496
- this.#app_menu_empty.classList.toggle("app_menu_empty_visible", visible === 0);
503
+ this.#appMenuEmpty.classList.toggle("app_menu_empty_visible", visible === 0);
497
504
  }
498
505
  go(page) {
499
- let center = this.#center;
506
+ const center = this.#center;
500
507
  //
501
508
  setPageName(page.getTitle());
502
- center.innerHTML = '';
509
+ center.innerHTML = "";
503
510
  center.appendChild(page.render());
504
- const _page = center.querySelector('page');
511
+ const _page = center.querySelector("page");
505
512
  new Animation(_page).fadeIn();
506
513
  // Стирания `center.removeAttribute("style")` здесь больше нет: инлайновых
507
514
  // стилей на `center` не ставит ни одна часть оболочки, а стирать чужое
@@ -510,33 +517,33 @@ class AppMenu {
510
517
  }
511
518
  setRouteTest(page) {
512
519
  this.#routeList.push(page);
513
- let test = this.#routeList.filter(route => route.getTitle().includes(page.getTitle()));
520
+ const test = this.#routeList.filter((route) => route.getTitle().includes(page.getTitle()));
514
521
  if (test.length === 1) {
515
- let button_route = document.createElement('route');
516
- let icon_menu = document.createElement('route_icon');
517
- icon_menu.innerHTML = page.getIcon();
518
- let title_menu = document.createElement('route_title');
519
- 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();
520
527
  if (page.getMain()) {
521
528
  this.go(page);
522
- button_route.classList.add("route_active");
529
+ buttonRoute.classList.add("route_active");
523
530
  }
524
- button_route.addEventListener('click', () => {
525
- const eventAwesome = new CustomEvent("route_event_"+page.constructor.name, {
531
+ buttonRoute.addEventListener("click", () => {
532
+ const eventAwesome = new CustomEvent("route_event_" + page.constructor.name, {
526
533
  bubbles: true,
527
534
  detail: {
528
535
  class: page.constructor.name,
529
536
  title: page.getTitle(),
530
- page: page
537
+ page: page,
531
538
  },
532
539
  });
533
- document.dispatchEvent(eventAwesome)
540
+ document.dispatchEvent(eventAwesome);
534
541
 
535
- if (!button_route.classList.contains('route_active')) {
536
- for (const act of this.#routes()) act.classList.remove('route_active');
542
+ if (!buttonRoute.classList.contains("route_active")) {
543
+ for (const act of this.#routes()) act.classList.remove("route_active");
537
544
  this.go(page);
538
- button_route.classList.add("route_active");
539
- if (this.#auto_close) {
545
+ buttonRoute.classList.add("route_active");
546
+ if (this.#autoClose) {
540
547
  // Панель закрывается после перехода: состояние и значок меняет тот же код,
541
548
  // что и по Escape — иначе у кнопки осталось бы открытое aria-expanded
542
549
  if (this.#appMenu.style.transform === `translateX(${this.#appMenuSlide}px)`) this.#close();
@@ -544,19 +551,19 @@ class AppMenu {
544
551
  }
545
552
  });
546
553
  if (page.getIcon() !== undefined) {
547
- button_route.appendChild(icon_menu)
554
+ buttonRoute.appendChild(iconMenu);
548
555
  }
549
- button_route.appendChild(title_menu)
550
- this.#routeViews.appendChild(button_route)
556
+ buttonRoute.appendChild(titleMenu);
557
+ this.#routeViews.appendChild(buttonRoute);
551
558
  this.#routeKeyboard.refresh();
552
559
  // Новый пункт мог не пройти уже введённый фильтр — список и подсказка пересобираются
553
- this.#filterRoutes(this.#app_menu_search_input.value);
560
+ this.#filterRoutes(this.#appMenuSearchInput.value);
554
561
  } else {
555
- Log.error(I18n.t("appLayout.search.alreadyAdded", {title: page.getTitle()}))
562
+ Log.error(I18n.t("appLayout.search.alreadyAdded", { title: page.getTitle() }));
556
563
  }
557
564
  }
558
565
  setAutoClose() {
559
- this.#auto_close = true;
566
+ this.#autoClose = true;
560
567
  }
561
568
  getMenu() {
562
569
  return this.#appMenu;
@@ -564,125 +571,128 @@ class AppMenu {
564
571
  }
565
572
 
566
573
  class NotificationBox {
567
- #notification_box = document.createElement('notification_box');
568
- #notification_box_main = document.createElement("notification_box_main");
574
+ #notificationBox = document.createElement("notification_box");
575
+ #notificationBoxMain = document.createElement("notification_box_main");
569
576
  // Шапка ящика: заголовок со счётчиком истории и действие «Очистить»
570
- #notification_box_header = document.createElement("notification_box_header");
571
- #notification_box_header_text = document.createElement("notification_box_header_text");
572
- #notification_box_title = document.createElement("notification_box_title");
573
- #notification_box_count = document.createElement("notification_box_count");
574
- #notification_box_empty = document.createElement("notification_box_empty");
575
- #empty_state = undefined;
576
- #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");
577
586
  // Состав ящика меняет Notification — за списком следит observer
578
587
  #observer = undefined;
579
- #notification_box_width = 400;
580
- #notification_box_width_test = 425;
581
- #notification_button = document.createElement('notification_button');
588
+ #notificationBoxWidth = 400;
589
+ #notificationBoxWidthTest = 425;
590
+ #notificationButton = document.createElement("notification_button");
582
591
  constructor(header, center, layout) {
583
- layout.set(LAYOUT_ROLE.NOTIFICATION_BOX, this.#notification_box);
584
- layout.set(LAYOUT_ROLE.NOTIFICATION_BOX_MAIN, this.#notification_box_main);
585
- 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";
586
595
  // Шапка: заголовок, счётчик записей и очистка списка
587
- this.#notification_box_title.innerText = I18n.t("appLayout.notifications.title");
588
- this.#notification_box_count.setAttribute("aria-hidden", "true");
589
- this.#notification_box_header_text.appendChild(this.#notification_box_title)
590
- 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);
591
600
  // Очистка — обычная кнопка-действие, а не Button: в шапке нужна тихая ссылка-действие
592
- this.#clear_button.className = "notification_box_clear";
593
- this.#clear_button.type = "button";
594
- this.#clear_button.setAttribute("aria-label", I18n.t("appLayout.notifications.clearAll"));
595
- this.#clear_button.setAttribute("title", I18n.t("appLayout.notifications.clear"));
596
- this.#clear_button.appendChild(new Icon(Icons.ACTIONS.DELETE, "14px").set());
597
- const clear_title = document.createElement("span");
598
- clear_title.innerText = I18n.t("appLayout.notifications.clear");
599
- this.#clear_button.appendChild(clear_title)
600
- this.#clear_button.addEventListener("click", () => this.#clearAll());
601
- this.#notification_box_header.appendChild(this.#notification_box_header_text)
602
- 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);
603
612
  // Пустое состояние — общий компонент, а не своя разметка из значка и подписи: это был
604
613
  // третий вариант одного и того же блока (здесь, в поиске меню, в таблицах). Заодно он
605
614
  // объявляется скринридеру (role="status"), а прежняя заглушка молчала
606
- this.#empty_state = new EmptyState({
615
+ this.#emptyState = new EmptyState({
607
616
  icon: Icons.SOCIAL.NOTIFICATIONS_OFF,
608
617
  title: I18n.t("appLayout.notifications.empty"),
609
- compact: true
618
+ compact: true,
610
619
  });
611
- this.#notification_box_empty.appendChild(this.#empty_state.set())
612
- this.#notification_box_main.appendChild(this.#notification_box_empty)
613
- this.#notification_box.appendChild(this.#notification_box_header)
614
- 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);
615
624
  // Карточки кладёт снаружи Notification, поэтому счётчик, группы и пустое состояние
616
625
  // следим по списку. Разделители дней ящик вставляет сам: на них реакция не нужна,
617
626
  // иначе пересборка дёргала бы сама себя без конца
618
627
  this.#observer = new MutationObserver((records) => this.#onBoxMutated(records));
619
- this.#observer.observe(this.#notification_box_main, { childList: true });
628
+ this.#observer.observe(this.#notificationBoxMain, { childList: true });
620
629
  this.#syncState();
621
- this.#notification_box.style.width = `${this.#notification_box_width}px`;
622
- this.#notification_box.style.right = `calc(-${this.#notification_box_width_test}px)`;
623
- this.#notification_button.addEventListener("click", () => {
624
- 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();
625
634
  else this.#openPanel();
626
- })
635
+ });
627
636
  // Значок — украшение: без подписи имя кнопки складывалось из лигатуры («notifications»)
628
- const notification_glyph = new Icon(Icons.SOCIAL.NOTIFICATIONS, "var(--header_icon_size)").set();
629
- notification_glyph.setAttribute("aria-hidden", "true");
630
- 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);
631
640
  // Имя кнопки — в том числе «непрочитанных: N» — держит #syncUnread: одно место
632
641
  // Клавиатура и aria панели уведомлений
633
- makeKeyboardButton(this.#notification_button);
634
- this.#notification_button.setAttribute("aria-haspopup", "true");
635
- this.#notification_button.setAttribute("aria-expanded", "false");
636
- this.#notification_button.setAttribute("aria-controls", "chui_notification_box");
637
- this.#notification_box.setAttribute("role", "region");
638
- this.#notification_box.setAttribute("aria-label", I18n.t("appLayout.notifications.title"));
639
- 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) => {
640
649
  if (event.key !== "Escape") return;
641
650
  event.preventDefault();
642
651
  this.#closePanel();
643
- this.#notification_button.focus();
652
+ this.#notificationButton.focus();
644
653
  });
645
654
  // Панель открывается с кнопки, и фокус остаётся на ней же — Escape обязан закрывать
646
655
  // ящик и оттуда, иначе выйти с клавиатуры можно было только Tab'ом внутрь панели
647
- this.#notification_button.addEventListener("keydown", (event) => {
656
+ this.#notificationButton.addEventListener("keydown", (event) => {
648
657
  if (event.key !== "Escape") return;
649
658
  event.preventDefault();
650
659
  this.#closePanel();
651
660
  });
652
- header.set().addEventListener('click', (e) => {
653
- if (e.target !== this.#notification_button) this.#closePanel();
654
- })
655
- center.addEventListener('click', (e) => {
656
- if (!this.#notification_box.contains(e.target)) {
657
- if (this.#notification_box.style.transform === `translateX(-${this.#notification_box_width_test}px)`) this.#closePanel();
661
+ header.set().addEventListener("click", (e) => {
662
+ if (e.target !== this.#notificationButton) this.#closePanel();
663
+ });
664
+ center.addEventListener("click", (e) => {
665
+ if (!this.#notificationBox.contains(e.target)) {
666
+ if (this.#notificationBox.style.transform === `translateX(-${this.#notificationBoxWidthTest}px)`)
667
+ this.#closePanel();
658
668
  }
659
- })
669
+ });
660
670
  center.onscroll = () => {
661
671
  // Прокрутка меняет материал шапки: плотнее фон и тень. Класс, а не инлайновые стили:
662
672
  // вид полосы целиком описан в global_style.css
663
673
  header.set().classList.toggle("header_scrolled", center.scrollTop > 15);
664
674
  };
665
- header.addToRight(this.#notification_button)
675
+ header.addToRight(this.#notificationButton);
666
676
  }
667
677
  #openPanel() {
668
- this.#notification_box.style.transform = `translateX(-${this.#notification_box_width_test}px)`;
669
- this.#notification_button.classList.add("notification_button_active");
670
- 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");
671
681
  // Открытый ящик — значит прочитанный: отметки снимаются все сразу, а не по одной
672
682
  this.#markRead();
673
683
  }
674
684
  #closePanel() {
675
- this.#notification_box.style.transform = `translateX(${this.#notification_box_width}px)`;
676
- this.#notification_button.classList.remove("notification_button_active");
677
- 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");
678
688
  }
679
689
  // Записи истории: карточки в ящик кладёт Notification, поэтому состав читаем из DOM
680
690
  #notifications() {
681
- return [...this.#notification_box_main.children].filter((child) => child.tagName === "NOTIFICATION");
691
+ return [...this.#notificationBoxMain.children].filter((child) => child.tagName === "NOTIFICATION");
682
692
  }
683
693
  // Разделители дней — тоже прямые дети списка, но записями не являются
684
694
  #groups() {
685
- return [...this.#notification_box_main.children].filter((child) => child.tagName === "NOTIFICATION_GROUP");
695
+ return [...this.#notificationBoxMain.children].filter((child) => child.tagName === "NOTIFICATION_GROUP");
686
696
  }
687
697
  /*
688
698
  * Реакция на изменение состава ящика.
@@ -693,17 +703,19 @@ class NotificationBox {
693
703
  */
694
704
  #onBoxMutated(records = []) {
695
705
  const changed = records.some((record) =>
696
- [...record.addedNodes, ...record.removedNodes].some((node) => node.tagName === "NOTIFICATION"));
706
+ [...record.addedNodes, ...record.removedNodes].some((node) => node.tagName === "NOTIFICATION")
707
+ );
697
708
  if (changed) this.#syncState();
698
709
  }
699
710
  // Счётчик, пустое состояние, группы дней и доступность кнопки согласованы с составом ящика
700
711
  #syncState() {
701
712
  const notifications = this.#notifications();
702
713
  const count = notifications.length;
703
- this.#notification_box_count.innerText = String(count);
704
- this.#notification_box_count.style.display = count === 0 ? "none" : "flex";
705
- this.#clear_button.disabled = count === 0;
706
- 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);
707
719
  this.#group(notifications);
708
720
  this.#syncUnread(notifications);
709
721
  }
@@ -716,12 +728,15 @@ class NotificationBox {
716
728
  * по тегу (счётчик, очистка), работает без изменений.
717
729
  */
718
730
  #group(notifications = []) {
731
+ // Идёт каскад уборки: подписи уже гаснут своим кадром, а пересборка заменила бы их
732
+ // новыми и вернула бы рывок, который каскад и убирает
733
+ if (this.#clearing) return;
719
734
  for (const separator of this.#groups()) separator.remove();
720
735
  let day = undefined;
721
736
  for (const notification of notifications) {
722
737
  if (notification.dataset.day === day) continue;
723
738
  day = notification.dataset.day;
724
- this.#notification_box_main.insertBefore(this.#makeGroup(day), notification);
739
+ this.#notificationBoxMain.insertBefore(this.#makeGroup(day), notification);
725
740
  }
726
741
  }
727
742
  /** Подпись дня: сегодня и вчера — словами, раньше — датой */
@@ -729,8 +744,9 @@ class NotificationBox {
729
744
  const yesterday = new Date();
730
745
  yesterday.setDate(yesterday.getDate() - 1);
731
746
  const group = document.createElement("notification_group");
732
- if (day === chui_functions.getDayKey()) group.innerText = I18n.t("appLayout.notifications.today");
733
- else if (day === chui_functions.getDayKey(yesterday)) group.innerText = I18n.t("appLayout.notifications.yesterday");
747
+ if (day === chuiFunctions.getDayKey()) group.innerText = I18n.t("appLayout.notifications.today");
748
+ else if (day === chuiFunctions.getDayKey(yesterday))
749
+ group.innerText = I18n.t("appLayout.notifications.yesterday");
734
750
  else {
735
751
  // Ключ хранится как ГГГГ-ММ-ДД, а в подписи порядок привычный: ДД.ММ.ГГГГ
736
752
  const [year, month, date] = String(day).split("-");
@@ -747,26 +763,79 @@ class NotificationBox {
747
763
  */
748
764
  #syncUnread(notifications = []) {
749
765
  const unread = notifications.filter((notification) => notification.dataset.unread === "true").length;
750
- this.#notification_button.classList.toggle("notification_button_unread", unread > 0);
751
- const label = unread === 0
752
- ? I18n.t("appLayout.notifications.title")
753
- : I18n.t("appLayout.notifications.unreadCount", {count: unread});
754
- this.#notification_button.setAttribute("aria-label", label);
755
- this.#notification_button.setAttribute("title", label);
766
+ this.#notificationButton.classList.toggle("notification_button_unread", unread > 0);
767
+ const label =
768
+ unread === 0
769
+ ? I18n.t("appLayout.notifications.title")
770
+ : I18n.t("appLayout.notifications.unreadCount", { count: unread });
771
+ this.#notificationButton.setAttribute("aria-label", label);
772
+ this.#notificationButton.setAttribute("title", label);
756
773
  }
757
774
  #markRead() {
758
775
  for (const notification of this.#notifications()) notification.dataset.unread = "false";
759
776
  this.#syncUnread(this.#notifications());
760
777
  }
778
+ /*
779
+ * Очистка истории.
780
+ *
781
+ * Карточки уходят каскадом: номер в очереди задаёт `--notification_clear_index`, а шаг —
782
+ * доля токена движения, поэтому весь список читается как рассыпающийся, а не как
783
+ * выключенный одним щелчком. Подписи дней гаснут вместе с ним, пустое состояние
784
+ * и счётчик появляются после последнего кадра.
785
+ *
786
+ * Конец каскада считает срок из тех же токенов движения, а не ждёт `animationend`:
787
+ * у карточки, которую браузер не отрисовывает (скрытая панель, `display: none`), кадр
788
+ * вообще не проигрывается, и событие не придёт — уборка осталась бы висеть
789
+ * незавершённой вместе с погашенными подписями.
790
+ */
761
791
  #clearAll() {
762
- for (const notification of this.#notifications()) {
763
- notification.style.transform = "translateX(100%)"
764
- notification.style.opacity = "0"
765
- 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;
766
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();
833
+ }
834
+ for (const group of this.#groups()) group.remove();
835
+ this.#syncState();
767
836
  }
768
837
  getBox() {
769
- return this.#notification_box;
838
+ return this.#notificationBox;
770
839
  }
771
840
  }
772
841
 
@@ -776,7 +845,7 @@ class AppLayout {
776
845
  #center = new Center(this.#layout).set();
777
846
  #appMenu = new AppMenu(this.#header, this.#center, this.#layout);
778
847
  #notificationBox = new NotificationBox(this.#header, this.#center, this.#layout);
779
- #notificationPanel = document.createElement('notification_panel');
848
+ #notificationPanel = document.createElement("notification_panel");
780
849
  // Текущие уведомления: ipc-обработчики регистрируются один раз и работают с последним
781
850
  #downloadNotification = undefined;
782
851
  #updateNotificationLoad = undefined;
@@ -785,18 +854,22 @@ class AppLayout {
785
854
  // Слушатели живут весь процесс — обработчик ошибок обязан быть на месте всегда.
786
855
  #onWindowError = (event) => {
787
856
  const place = event.filename === undefined ? "" : ` (${event.filename}:${event.lineno})`;
788
- this.#logError(I18n.t("appLayout.error.uncaught"), event.error === undefined ? event.message : event.error, place)
789
- }
857
+ this.#logError(
858
+ I18n.t("appLayout.error.uncaught"),
859
+ event.error === undefined ? event.message : event.error,
860
+ place
861
+ );
862
+ };
790
863
  #onUnhandledRejection = (event) => {
791
- this.#logError(I18n.t("appLayout.error.unhandledRejection"), event.reason)
792
- }
864
+ this.#logError(I18n.t("appLayout.error.unhandledRejection"), event.reason);
865
+ };
793
866
  #logError(prefix, value, place = "") {
794
867
  const text = `${prefix}: ${AppLayout.#formatError(value)}${place}`;
795
868
  try {
796
- Log.error(text)
869
+ Log.error(text);
797
870
  } catch (e) {
798
871
  // Сам обработчик ошибок падать не должен (например, ipcRenderer уже недоступен при закрытии окна)
799
- console.error(text, e)
872
+ console.error(text, e);
800
873
  }
801
874
  }
802
875
  static #formatError(value) {
@@ -805,24 +878,24 @@ class AppLayout {
805
878
  return String(value);
806
879
  }
807
880
  constructor() {
808
- require('../../modules/chui_fonts').install();
881
+ require("../../modules/chui_fonts").install();
809
882
  // Глобальные стили
810
- chui_functions.setStyles(__dirname + `/global_style.css`, 'chUiJS_Global_App')
811
- 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");
812
885
  // Необработанные ошибки и reject’ы renderer-процесса — один раз на процесс
813
- if (app_error_handlers_installed === false) {
814
- app_error_handlers_installed = true;
886
+ if (appErrorHandlersInstalled === false) {
887
+ appErrorHandlersInstalled = true;
815
888
  window.addEventListener("error", this.#onWindowError);
816
889
  window.addEventListener("unhandledrejection", this.#onUnhandledRejection);
817
890
  }
818
891
  //if (process.platform !== "darwin") document.body.style.border = "1px solid var(--border_color)";
819
892
  // ===
820
893
 
821
- document.body.appendChild(this.#header.set())
822
- document.body.appendChild(this.#center)
823
- document.body.appendChild(this.#appMenu.getMenu())
824
- document.body.appendChild(this.#notificationBox.getBox())
825
- document.body.appendChild(this.#notificationPanel)
894
+ document.body.appendChild(this.#header.set());
895
+ document.body.appendChild(this.#center);
896
+ document.body.appendChild(this.#appMenu.getMenu());
897
+ document.body.appendChild(this.#notificationBox.getBox());
898
+ document.body.appendChild(this.#notificationPanel);
826
899
  // Части layout'а зарегистрированы: уведомления и Route ищут свои узлы по ролям, а не по id
827
900
  this.#layout.set(LAYOUT_ROLE.NOTIFICATION_PANEL, this.#notificationPanel);
828
901
  registerLayout(this.#layout);
@@ -837,41 +910,43 @@ class AppLayout {
837
910
  ipcRenderer.on("sendNotificationDownload", (e, text, body) => {
838
911
  this.#downloadNotification = new DownloadNotification({ title: text, text: body });
839
912
  this.#downloadNotification.show();
840
- })
913
+ });
841
914
  ipcRenderer.on("sendNotificationDownloadUpdate", (e, title, text) => {
842
915
  if (this.#downloadNotification !== undefined) this.#downloadNotification.update(title, text);
843
- })
916
+ });
844
917
  ipcRenderer.on("sendNotificationDownloadComplete", () => {
845
918
  if (this.#downloadNotification !== undefined) this.#downloadNotification.done();
846
- })
919
+ });
847
920
  ipcRenderer.on("sendNotificationDownloadError", () => {
848
921
  if (this.#downloadNotification !== undefined) this.#downloadNotification.error();
849
- })
922
+ });
850
923
  //
851
924
  ipcRenderer.on("sendNotificationUpdateLoad", (e, text, body) => {
852
925
  this.#updateNotificationLoad = new UpdateNotification({ title: text, text: body, spinner: true });
853
926
  this.#updateNotificationLoad.show(true);
854
- })
927
+ });
855
928
  ipcRenderer.on("sendNotificationUpdateLoadClose", () => {
856
929
  if (this.#updateNotificationLoad !== undefined) this.#updateNotificationLoad.hide();
857
- })
930
+ });
858
931
  ipcRenderer.on("sendNotificationUpdate", (e, text, body) => {
859
932
  this.#updateNotification = new UpdateNotification({ title: text, text: body, spinner: false });
860
933
  this.#updateNotification.show(true);
861
- })
934
+ });
862
935
  ipcRenderer.on("sendNotificationUpdateClose", () => {
863
936
  if (this.#updateNotification !== undefined) this.#updateNotification.hide();
864
- })
865
- let popup = new Popup();
937
+ });
938
+ const popup = new Popup();
866
939
  ipcRenderer.once("checkUpdatesTrue", async (e, check, version) => {
867
940
  if (check) {
868
- let confirm_res = await popup.confirm({
869
- title: I18n.t("appLayout.update.available", {version: version}), message: I18n.t("appLayout.update.question"),
870
- cancelText: I18n.t("appLayout.update.cancel"), okText: I18n.t("appLayout.update.install")
941
+ const confirmRes = await popup.confirm({
942
+ title: I18n.t("appLayout.update.available", { version: version }),
943
+ message: I18n.t("appLayout.update.question"),
944
+ cancelText: I18n.t("appLayout.update.cancel"),
945
+ okText: I18n.t("appLayout.update.install"),
871
946
  });
872
- e.sender.send("updateInstallConfirm", confirm_res)
947
+ e.sender.send("updateInstallConfirm", confirmRes);
873
948
  }
874
- })
949
+ });
875
950
  }
876
951
  setWindowControlsLeft() {
877
952
  this.#header.addWC(true);
@@ -898,83 +973,103 @@ class AppLayout {
898
973
  this.#header.addToLeftBeforeTitle(...headerLeft);
899
974
  }
900
975
  disableAppMenu() {
901
- this.#layout.get(LAYOUT_ROLE.APP_MENU_BUTTON).remove()
976
+ this.#layout.get(LAYOUT_ROLE.APP_MENU_BUTTON).remove();
902
977
  // Отступ имени страницы считает CSS: `.page_name_no_menu` в global_style.css
903
- this.#layout.get(LAYOUT_ROLE.PAGE_NAME).classList.add("page_name_no_menu")
978
+ this.#layout.get(LAYOUT_ROLE.PAGE_NAME).classList.add("page_name_no_menu");
904
979
  }
905
980
  setScript(pathToJS = String(), id = String()) {
906
- let data = fs.readFileSync(pathToJS, 'utf8');
907
- let script = document.createElement('script');
981
+ const data = fs.readFileSync(pathToJS, "utf8");
982
+ const script = document.createElement("script");
908
983
  script.innerHTML = String(data);
909
- script.setAttribute('id', id);
984
+ script.setAttribute("id", id);
910
985
  // Скрипт подключается в сам документ (не узел компонента), поэтому проверка по id и document
911
986
  if (document.querySelector(`script[id="${id}"]`) === null) document.body.appendChild(script);
912
987
  }
913
- static TABS(options = {
914
- width: String(),
915
- default: Number(),
916
- tabs: []
917
- }) { return new HeaderTabs(options).set(); }
918
- static BUTTON(options = {
919
- title: String(),
920
- icon: undefined,
921
- reverse: Boolean(),
922
- clickEvent: () => {}
923
- }) { return new HeaderButton(options).set(); }
924
- static USER_PROFILE(options = {
925
- username: String(),
926
- image: { noImage: Boolean(), imageLink: String(), imageBase64: String(), path: String() },
927
- items: []
928
- }) { return new UserProfile(options).set(); }
929
- static USER_PROFILE_ITEM(options = {
930
- title: String(),
931
- icon: undefined,
932
- clickEvent: () => {}
933
- }) { return new UserDDItem(options).set(); }
934
- static DIALOG(options = {
935
- title: String(),
936
- icon: undefined,
937
- reverse: Boolean(),
938
- label: String(),
939
- dialogOptions: {
988
+ static TABS(
989
+ options = {
990
+ width: String(),
991
+ default: Number(),
992
+ tabs: [],
993
+ }
994
+ ) {
995
+ return new HeaderTabs(options).set();
996
+ }
997
+ static BUTTON(
998
+ options = {
999
+ title: String(),
1000
+ icon: undefined,
1001
+ reverse: Boolean(),
1002
+ clickEvent: () => {},
1003
+ }
1004
+ ) {
1005
+ return new HeaderButton(options).set();
1006
+ }
1007
+ static USER_PROFILE(
1008
+ options = {
1009
+ username: String(),
1010
+ image: { noImage: Boolean(), imageLink: String(), imageBase64: String(), path: String() },
1011
+ items: [],
1012
+ }
1013
+ ) {
1014
+ return new UserProfile(options).set();
1015
+ }
1016
+ static USER_PROFILE_ITEM(
1017
+ options = {
1018
+ title: String(),
1019
+ icon: undefined,
1020
+ clickEvent: () => {},
1021
+ }
1022
+ ) {
1023
+ return new UserDDItem(options).set();
1024
+ }
1025
+ static DIALOG(
1026
+ options = {
940
1027
  title: String(),
941
- closeOutSideClick: Boolean(),
942
- style: {
943
- width: String(),
944
- height: String(),
945
- direction: String(),
946
- wrap: String(),
947
- align: String(),
948
- justify: String(),
1028
+ icon: undefined,
1029
+ reverse: Boolean(),
1030
+ label: String(),
1031
+ dialogOptions: {
1032
+ title: String(),
1033
+ closeOutSideClick: Boolean(),
1034
+ style: {
1035
+ width: String(),
1036
+ height: String(),
1037
+ direction: String(),
1038
+ wrap: String(),
1039
+ align: String(),
1040
+ justify: String(),
1041
+ },
1042
+ components: [],
949
1043
  },
950
- components: []
951
1044
  }
952
- }) { return new HeaderDialog(options).set(); }
1045
+ ) {
1046
+ return new HeaderDialog(options).set();
1047
+ }
953
1048
  }
954
1049
 
955
1050
  class HeaderTabs {
956
- #id_list = require("randomstring").generate();
957
- #header_tabs_list = undefined;
958
- #header_tabs = document.createElement('header_tabs');
959
- #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);
960
1055
  constructor(options = { width: String(), default: Number(), tabs: [] }) {
961
- this.#header_tabs_list = options.tabs;
962
- this.#header_tabs.id = this.#id_list;
1056
+ this.#headerTabsList = options.tabs;
1057
+ this.#headerTabs.id = this.#idList;
963
1058
  // Полоса вкладок — не меню: у неё свои роли (`tablist`/`tab`) и свои клавиши (см. #onKeydown).
964
1059
  // `role="button"` и `tabindex="0"` каждой вкладке уже поставил makeKeyboardButton, поэтому
965
1060
  // роли и порядок обхода переопределяем здесь: остановка Tab в полосе должна быть ровно одна
966
- this.#header_tabs.setAttribute("role", "tablist");
967
- for (let item of this.#header_tabs_list) {
1061
+ this.#headerTabs.setAttribute("role", "tablist");
1062
+ for (const item of this.#headerTabsList) {
968
1063
  item.setAttribute("role", "tab");
969
1064
  item.setAttribute("tabindex", "-1");
970
- this.#header_tabs.appendChild(item);
971
- item.addEventListener('click', (event) => {
1065
+ this.#headerTabs.appendChild(item);
1066
+ item.addEventListener("click", (event) => {
972
1067
  if (event.target.tagName === "HEADER_BUTTON_ICON" || event.target.tagName === "HEADER_BUTTON_TITLE") {
973
- this.#setActive(event.target.parentNode)
1068
+ this.#setActive(event.target.parentNode);
974
1069
  } else {
975
- this.#setActive(event.target)
1070
+ this.#setActive(event.target);
976
1071
  }
977
- })
1072
+ });
978
1073
  }
979
1074
  // Клик по выбранной вкладке не переигрывает её действие — как у пункта меню текущего
980
1075
  // раздела (`route_active`, который тоже не переходит повторно). Раньше клик до вкладки
@@ -982,40 +1077,44 @@ class HeaderTabs {
982
1077
  // кликом он снимал у неё и курсор-указатель, и ховер, которых у соседних кнопок шапки
983
1078
  // нет. Слушатель ловит клик в фазе перехвата: действие вкладки и `#setActive` висят
984
1079
  // на самом элементе и иначе сработали бы первыми
985
- this.#header_tabs.addEventListener("click", (event) => {
986
- const tab = event.target.closest === undefined ? null : event.target.closest("header_button");
987
- if (tab === null || tab.getAttribute("active") === null) return;
988
- event.stopImmediatePropagation();
989
- }, true);
1080
+ this.#headerTabs.addEventListener(
1081
+ "click",
1082
+ (event) => {
1083
+ const tab = event.target.closest === undefined ? null : event.target.closest("header_button");
1084
+ if (tab === null || tab.getAttribute("active") === null) return;
1085
+ event.stopImmediatePropagation();
1086
+ },
1087
+ true
1088
+ );
990
1089
  // Пока ни одна вкладка не выбрана, остановкой обхода становится первая по счёту:
991
1090
  // иначе в полосу нельзя было бы войти с клавиатуры вообще
992
- const default_tab = this.#header_tabs_list[options.default === undefined ? 0 : options.default];
993
- if (default_tab !== undefined) default_tab.setAttribute("tabindex", "0");
994
- this.#header_tabs.addEventListener("keydown", this.#keydown_event);
995
- if (options.width !== undefined) this.#header_tabs.style.width = options.width;
996
- for (let item of this.#header_tabs_list) {
997
- 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) {
998
1097
  setTimeout(() => {
999
- item.click()
1098
+ item.click();
1000
1099
  //this.#setActive(item)
1001
- }, 250)
1100
+ }, 250);
1002
1101
  }
1003
1102
  }
1004
1103
  }
1005
1104
  set() {
1006
- return this.#header_tabs;
1105
+ return this.#headerTabs;
1007
1106
  }
1008
1107
  #setActive(target) {
1009
1108
  if (target.getAttribute("active") === null) {
1010
1109
  // `children`, а не `childNodes`: в `childNodes` попадают и текстовые узлы, у которых нет classList.
1011
1110
  // `children` — HTMLCollection, поэтому перебираем копию
1012
- [...this.#header_tabs.children].forEach(child => {
1111
+ [...this.#headerTabs.children].forEach((child) => {
1013
1112
  child.removeAttribute("active");
1014
1113
  child.removeAttribute("aria-current");
1015
1114
  child.removeAttribute("aria-selected");
1016
1115
  child.setAttribute("tabindex", "-1");
1017
- child.classList.remove("header_tab_active")
1018
- })
1116
+ child.classList.remove("header_tab_active");
1117
+ });
1019
1118
  target.setAttribute("active", true);
1020
1119
  // Вкладка без `active` была только подсвечена классом: скринридер о выборе не знал (как у пункта меню маршрутов)
1021
1120
  target.setAttribute("aria-current", "page");
@@ -1023,7 +1122,7 @@ class HeaderTabs {
1023
1122
  target.setAttribute("aria-selected", "true");
1024
1123
  // Выбранная вкладка — единственная остановка Tab в полосе вкладок
1025
1124
  target.setAttribute("tabindex", "0");
1026
- target.classList.add("header_tab_active")
1125
+ target.classList.add("header_tab_active");
1027
1126
  }
1028
1127
  }
1029
1128
  /*
@@ -1036,11 +1135,11 @@ class HeaderTabs {
1036
1135
  * `makeKeyboardButton` (клик по выбранной вкладке гасит перехватчик выше).
1037
1136
  */
1038
1137
  #onKeydown(event) {
1039
- const tabs = [...this.#header_tabs.children];
1138
+ const tabs = [...this.#headerTabs.children];
1040
1139
  if (tabs.length === 0) return;
1041
1140
  const focused = tabs.indexOf(document.activeElement);
1042
1141
  // Фокус мог быть не в полосе: тогда считаем от вкладки с остановкой обхода
1043
- const from = focused === -1 ? tabs.findIndex(tab => tab.getAttribute("tabindex") === "0") : focused;
1142
+ const from = focused === -1 ? tabs.findIndex((tab) => tab.getAttribute("tabindex") === "0") : focused;
1044
1143
  const start = from === -1 ? 0 : from;
1045
1144
  const last = tabs.length - 1;
1046
1145
  let next;
@@ -1059,12 +1158,12 @@ class UserProfile {
1059
1158
  // находится по узлу обёртки — так же, как у MenuBar, и по нему его снимает тот,
1060
1159
  // кто убирает профиль из шапки (`Header.removeToRight`)
1061
1160
  static #instances = new WeakMap();
1062
- #user_main = document.createElement("user_main");
1063
- #user_button = document.createElement("user_button");
1064
- #user_dropdown = document.createElement("user_dropdown");
1065
- #is_open = false;
1161
+ #userMain = document.createElement("user_main");
1162
+ #userButton = document.createElement("user_button");
1163
+ #userDropdown = document.createElement("user_dropdown");
1164
+ #isOpen = false;
1066
1165
  //
1067
- #user_dd_image_main = document.createElement("user_dd_image_main");
1166
+ #userDdImageMain = document.createElement("user_dd_image_main");
1068
1167
  // Аватар — компонент Avatar: инициалы, картинка из адреса/base64/файла
1069
1168
  #avatar = undefined;
1070
1169
  #username = undefined;
@@ -1072,35 +1171,37 @@ class UserProfile {
1072
1171
  // Слушатель window нужен только пока меню открыто: иначе он копился на каждый профиль.
1073
1172
  // Закрывает меню только по клику СНАРУЖИ: раньше сравнивался прямой родитель цели,
1074
1173
  // поэтому клик по аватару или пустому месту внутри меню тоже его закрывал
1075
- #window_click_event = (event) => {
1076
- 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;
1077
1176
  this.#closeMenu();
1078
- }
1177
+ };
1079
1178
  // Выбор пункта закрывает меню. Обработчик на списке, а не на каждом пункте: пункты
1080
1179
  // приходят из UserDDItem и могут быть добавлены кем угодно снаружи
1081
- #dropdown_click_event = (event) => {
1180
+ #dropdownClickEvent = (event) => {
1082
1181
  const item = event.target.closest === undefined ? null : event.target.closest("user_item");
1083
- if (item === null || !this.#user_dropdown.contains(item)) return;
1182
+ if (item === null || !this.#userDropdown.contains(item)) return;
1084
1183
  this.#closeMenu();
1085
- }
1184
+ };
1086
1185
  //
1087
- constructor(options = {
1088
- username: String(),
1089
- image: { noImage: Boolean(), imageLink: String(), imageBase64: String(), path: String() },
1090
- items: []
1091
- }) {
1186
+ constructor(
1187
+ options = {
1188
+ username: String(),
1189
+ image: { noImage: Boolean(), imageLink: String(), imageBase64: String(), path: String() },
1190
+ items: [],
1191
+ }
1192
+ ) {
1092
1193
  this.#username = options.username;
1093
- this.#user_button.innerText = options.username;
1094
- this.#user_main.appendChild(this.#user_button)
1095
- 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);
1096
1197
  // Панель — слой в body: внутри шапки (у неё свой backdrop-filter) её blur не работал бы,
1097
1198
  // а выдвижные слои оболочки перекрывали бы её. Идентификатор нужен для aria-controls
1098
- this.#user_dropdown.id = require("randomstring").generate();
1099
- document.body.appendChild(this.#user_dropdown);
1100
- this.#user_button.addEventListener("click", () => {
1101
- 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();
1102
1203
  else this.#openMenu();
1103
- })
1204
+ });
1104
1205
  //
1105
1206
  if (options.image !== undefined) {
1106
1207
  // Имя пользователя уже есть в кнопке и в пунктах меню, поэтому аватар — украшение
@@ -1108,24 +1209,24 @@ class UserProfile {
1108
1209
  name: options.username,
1109
1210
  image: options.image,
1110
1211
  size: "80px",
1111
- decorative: true
1212
+ decorative: true,
1112
1213
  });
1113
- this.#user_dd_image_main.appendChild(this.#avatar.set());
1114
- this.#user_dropdown.appendChild(this.#user_dd_image_main);
1214
+ this.#userDdImageMain.appendChild(this.#avatar.set());
1215
+ this.#userDropdown.appendChild(this.#userDdImageMain);
1115
1216
  }
1116
1217
  //
1117
- for (let item of options.items) this.#user_dropdown.appendChild(item);
1118
- 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);
1119
1220
  // Клавиатура и aria меню профиля: стрелки по пунктам, Escape закрывает и возвращает фокус
1120
- this.#menuKeyboard = new MenuKeyboard(this.#user_dropdown, {
1221
+ this.#menuKeyboard = new MenuKeyboard(this.#userDropdown, {
1121
1222
  itemSelector: "user_item",
1122
1223
  onEscape: () => this.#closeMenu(),
1123
- trigger: this.#user_button
1224
+ trigger: this.#userButton,
1124
1225
  });
1125
- makeKeyboardButton(this.#user_button);
1226
+ makeKeyboardButton(this.#userButton);
1126
1227
  // Закрытое меню объявляется закрытым сразу: иначе у кнопки не было бы aria-expanded
1127
1228
  this.#menuKeyboard.setExpanded(false);
1128
- this.#user_button.addEventListener("keydown", (event) => {
1229
+ this.#userButton.addEventListener("keydown", (event) => {
1129
1230
  if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1130
1231
  event.preventDefault();
1131
1232
  this.#openMenu();
@@ -1134,40 +1235,40 @@ class UserProfile {
1134
1235
  });
1135
1236
  }
1136
1237
  #openMenu() {
1137
- this.#is_open = true;
1238
+ this.#isOpen = true;
1138
1239
  // Панель живёт в body: если её снимал `unmount`, возвращаем на место
1139
- if (document.body.contains(this.#user_dropdown) === false) document.body.appendChild(this.#user_dropdown);
1140
- 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);
1141
1242
  // fadeIn показывает панель (display: flex), и только после этого её можно измерить
1142
- new Animation(this.#user_dropdown).fadeIn();
1243
+ new Animation(this.#userDropdown).fadeIn();
1143
1244
  this.#positionDropdown();
1144
- window.addEventListener("click", this.#window_click_event);
1145
- window.addEventListener("resize", this.#position_dropdown_event);
1245
+ window.addEventListener("click", this.#windowClickEvent);
1246
+ window.addEventListener("resize", this.#positionDropdownEvent);
1146
1247
  this.#menuKeyboard.setExpanded(true);
1147
1248
  }
1148
1249
  #closeMenu() {
1149
- if (!this.#is_open) return;
1150
- this.#is_open = false;
1151
- this.#user_button.classList.remove(USER_PROFILE_ACTIVE_CLASS);
1152
- new Animation(this.#user_dropdown).fadeOut();
1153
- window.removeEventListener("click", this.#window_click_event);
1154
- 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);
1155
1256
  this.#menuKeyboard.setExpanded(false);
1156
1257
  }
1157
1258
  // position: fixed — координаты считаются от окна, поэтому панель пересчитывается и на resize.
1158
1259
  // Логический `inset-inline-end` держит выравнивание по кнопке и в RTL
1159
1260
  #positionDropdown() {
1160
- const button = this.#user_button.getBoundingClientRect();
1161
- const dropdown = this.#user_dropdown.getBoundingClientRect();
1261
+ const button = this.#userButton.getBoundingClientRect();
1262
+ const dropdown = this.#userDropdown.getBoundingClientRect();
1162
1263
  let end = window.innerWidth - button.right;
1163
1264
  if (end + dropdown.width > window.innerWidth - USER_DROPDOWN_EDGE) {
1164
1265
  end = window.innerWidth - USER_DROPDOWN_EDGE - dropdown.width;
1165
1266
  }
1166
1267
  if (end < USER_DROPDOWN_EDGE) end = USER_DROPDOWN_EDGE;
1167
- this.#user_dropdown.style.insetInlineEnd = `${Math.round(end)}px`;
1168
- 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`;
1169
1270
  }
1170
- #position_dropdown_event = () => this.#positionDropdown();
1271
+ #positionDropdownEvent = () => this.#positionDropdown();
1171
1272
 
1172
1273
  getUsername() {
1173
1274
  return this.#username;
@@ -1185,8 +1286,8 @@ class UserProfile {
1185
1286
  */
1186
1287
  unmount() {
1187
1288
  this.#closeMenu();
1188
- this.#user_dropdown.remove();
1189
- UserProfile.#instances.delete(this.#user_main);
1289
+ this.#userDropdown.remove();
1290
+ UserProfile.#instances.delete(this.#userMain);
1190
1291
  return this;
1191
1292
  }
1192
1293
  /** Полное удаление: панель больше не нужна — снять клавиатуру и убрать панель из body */
@@ -1206,144 +1307,149 @@ class UserProfile {
1206
1307
  }
1207
1308
 
1208
1309
  set() {
1209
- return this.#user_main;
1310
+ return this.#userMain;
1210
1311
  }
1211
1312
  }
1212
1313
 
1213
1314
  class UserDDItem {
1214
- #user_item = document.createElement("user_item");
1315
+ #userItem = document.createElement("user_item");
1215
1316
 
1216
- constructor(options = {
1217
- title: String(),
1218
- icon: undefined,
1219
- clickEvent: () => {}
1220
- }) {
1317
+ constructor(
1318
+ options = {
1319
+ title: String(),
1320
+ icon: undefined,
1321
+ clickEvent: () => {},
1322
+ }
1323
+ ) {
1221
1324
  if (options.title !== undefined && options.icon !== undefined) {
1222
- this.#user_item.innerText = options.title;
1325
+ this.#userItem.innerText = options.title;
1223
1326
  // Значок ведёт строку — как в пунктах context_menu_item и выпадающих списков
1224
1327
  // меню-бара. Дописанный в конец, он прилипал к подписи, и пункт меню профиля
1225
1328
  // выбивался из семьи меню (в остальных меню значок стоит перед текстом)
1226
- this.#user_item.insertAdjacentHTML("afterbegin", new Icon(options.icon).getHTML());
1329
+ this.#userItem.insertAdjacentHTML("afterbegin", new Icon(options.icon).getHTML());
1227
1330
  } else if (options.title !== undefined) {
1228
- this.#user_item.innerText = options.title;
1331
+ this.#userItem.innerText = options.title;
1229
1332
  } else if (options.icon !== undefined) {
1230
- this.#user_item.innerHTML = new Icon(options.icon).getHTML();
1333
+ this.#userItem.innerHTML = new Icon(options.icon).getHTML();
1231
1334
  }
1232
- this.#user_item.addEventListener("click", options.clickEvent)
1335
+ this.#userItem.addEventListener("click", options.clickEvent);
1233
1336
  }
1234
1337
 
1235
1338
  set() {
1236
- return this.#user_item;
1339
+ return this.#userItem;
1237
1340
  }
1238
1341
  }
1239
1342
 
1240
1343
  class HeaderDialog {
1241
- #header_button = undefined;
1242
- constructor(options = {
1243
- title: String(),
1244
- icon: undefined,
1245
- reverse: Boolean(),
1246
- label: String(),
1247
- dialogOptions: {
1344
+ #headerButton = undefined;
1345
+ constructor(
1346
+ options = {
1248
1347
  title: String(),
1249
- closeOutSideClick: Boolean(),
1250
- style: {
1251
- width: String(),
1252
- height: String(),
1253
- direction: String(),
1254
- wrap: String(),
1255
- align: String(),
1256
- justify: String(),
1348
+ icon: undefined,
1349
+ reverse: Boolean(),
1350
+ label: String(),
1351
+ dialogOptions: {
1352
+ title: String(),
1353
+ closeOutSideClick: Boolean(),
1354
+ style: {
1355
+ width: String(),
1356
+ height: String(),
1357
+ direction: String(),
1358
+ wrap: String(),
1359
+ align: String(),
1360
+ justify: String(),
1361
+ },
1362
+ components: [],
1257
1363
  },
1258
- components: []
1259
1364
  }
1260
- }) {
1365
+ ) {
1261
1366
  // Неполные `dialogOptions` сборку не роняют: `style` и `components` — необязательные
1262
1367
  // части опции, а прямое обращение к ним давало TypeError при создании кнопки
1263
1368
  // («Cannot read properties of undefined») ещё до первого открытия слоя
1264
- const dialog_options = options.dialogOptions === undefined ? {} : options.dialogOptions;
1265
- const dialog_style = dialog_options.style === undefined ? {} : dialog_options.style;
1266
- 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;
1267
1372
  // Пустой заголовок остаётся пустым: заготовка опции рисовала бы слово «undefined»
1268
- const dialog_title = dialog_options.title === undefined || dialog_options.title === null
1269
- ? "" : dialog_options.title;
1270
- let dialog = new Dialog({
1271
- width: dialog_style.width,
1272
- height: dialog_style.height,
1273
- 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,
1274
1379
  // Имя модального слоя для скринридера: без него диалог объявлялся бы безымянным
1275
- title: dialog_title
1276
- })
1380
+ title: dialogTitle,
1381
+ });
1277
1382
  // HEADER
1278
- let header_dialog = new ContentBlock({
1383
+ const headerDialog = new ContentBlock({
1279
1384
  direction: "row",
1280
1385
  wrap: "nowrap",
1281
1386
  align: "center",
1282
1387
  justify: "space-between",
1283
- disableMarginChild: true
1388
+ disableMarginChild: true,
1284
1389
  });
1285
- header_dialog.setWidth("-webkit-fill-available");
1390
+ headerDialog.setWidth("-webkit-fill-available");
1286
1391
  // Заголовок — как у заголовков панелей оболочки (размер `--font_default_size`, вес 500
1287
1392
  // даёт `strong`): отдельные 14 pt в диалоге спорили с его содержимым
1288
- let title = new Label({
1289
- markdownText: `**${dialog_title}**`,
1393
+ const title = new Label({
1394
+ markdownText: `**${dialogTitle}**`,
1290
1395
  wordBreak: "normal",
1291
- width: "max-content"
1292
- })
1293
- let close_button = new Button({
1396
+ width: "max-content",
1397
+ });
1398
+ const closeButton = new Button({
1294
1399
  icon: Icons.NAVIGATION.CLOSE,
1295
1400
  label: I18n.t("common.close"),
1296
1401
  reverse: true,
1297
- clickEvent: () => dialog.close()
1402
+ clickEvent: () => dialog.close(),
1298
1403
  });
1299
- header_dialog.add(title, close_button);
1300
- dialog.addToHeader(header_dialog);
1404
+ headerDialog.add(title, closeButton);
1405
+ dialog.addToHeader(headerDialog);
1301
1406
  // BODY
1302
- let body_dialog = new ContentBlock({
1303
- direction: dialog_style.direction,
1304
- wrap: dialog_style.wrap,
1305
- align: dialog_style.align,
1306
- 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,
1307
1412
  });
1308
- body_dialog.setWidth("-webkit-fill-available");
1309
- body_dialog.add(...dialog_components)
1310
- dialog.addToBody(body_dialog);
1413
+ bodyDialog.setWidth("-webkit-fill-available");
1414
+ bodyDialog.add(...dialogComponents);
1415
+ dialog.addToBody(bodyDialog);
1311
1416
  // FOOTER
1312
1417
 
1313
1418
  //
1314
- this.#header_button = makeHeaderButtonBox(options);
1419
+ this.#headerButton = makeHeaderButtonBox(options);
1315
1420
  // Слой идёт в body при первом открытии, а не при сборке: заранее созданный
1316
1421
  // невидимый диалог оставался бы в документе на всё время жизни кнопки
1317
- this.#header_button.addEventListener("click", () => {
1422
+ this.#headerButton.addEventListener("click", () => {
1318
1423
  if (document.body.contains(dialog.set()) === false) document.body.appendChild(dialog.set());
1319
1424
  dialog.open();
1320
1425
  });
1321
1426
  }
1322
1427
 
1323
1428
  set() {
1324
- return this.#header_button;
1429
+ return this.#headerButton;
1325
1430
  }
1326
1431
  }
1327
1432
 
1328
1433
  class HeaderButton {
1329
- #header_button = undefined;
1434
+ #headerButton = undefined;
1330
1435
 
1331
- constructor(options = {
1332
- title: String(),
1333
- icon: undefined,
1334
- reverse: Boolean(),
1335
- label: String(),
1336
- clickEvent: () => {
1436
+ constructor(
1437
+ options = {
1438
+ title: String(),
1439
+ icon: undefined,
1440
+ reverse: Boolean(),
1441
+ label: String(),
1442
+ clickEvent: () => {},
1337
1443
  }
1338
- }) {
1339
- this.#header_button = makeHeaderButtonBox(options);
1340
- this.#header_button.addEventListener("click", options.clickEvent);
1444
+ ) {
1445
+ this.#headerButton = makeHeaderButtonBox(options);
1446
+ this.#headerButton.addEventListener("click", options.clickEvent);
1341
1447
  }
1342
1448
 
1343
1449
  set() {
1344
- return this.#header_button;
1450
+ return this.#headerButton;
1345
1451
  }
1346
1452
  }
1347
1453
 
1348
- exports.AppLayout = AppLayout
1349
- exports.Route = Route
1454
+ exports.AppLayout = AppLayout;
1455
+ exports.Route = Route;