chuijs 4.0.0 → 4.0.2
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.
- package/CHANGELOG.md +117 -0
- package/README.md +62 -27
- package/framework/appLayout/default/chui_app_layout.js +72 -59
- package/framework/appLayout/default/global_style.css +35 -34
- package/framework/components/chui_data_grid/data_grid.js +7 -3
- package/framework/components/chui_notification/notification_update.js +3 -0
- package/framework/components/chui_notification/status_plate.css +7 -0
- package/framework/components/chui_spinner/spinner.js +38 -21
- package/framework/components/chui_spinner/styles.css +34 -28
- package/framework/components/chui_sticky_bar/sticky_bar.js +230 -0
- package/framework/components/chui_sticky_bar/styles.css +37 -0
- package/framework/modules/chui_bridge/preload.js +35 -2
- package/framework/modules/chui_functions.js +22 -6
- package/framework/modules/chui_page/styles.css +3 -26
- package/framework/modules/chui_services/chui_channels.js +84 -2
- package/framework/modules/chui_services/desktop.js +164 -1
- package/index.js +123 -62
- package/package.json +24 -6
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Формат — [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
|
|
4
|
+
версии — [Semantic Versioning](https://semver.org/lang/ru/).
|
|
5
|
+
|
|
6
|
+
## [4.0.2] - 2026-09-29
|
|
7
|
+
|
|
8
|
+
### Исправлено
|
|
9
|
+
|
|
10
|
+
- **Заголовок страницы вернулся в шапку.** `Route#go()` больше не подмешивает название в
|
|
11
|
+
содержимое: узел `page_title` (появился в 4.0.1) и его стили удалены. Он дублировал заголовок,
|
|
12
|
+
который страница рисует сама, а `Page#render()` отдаёт один и тот же узел — повторный переход
|
|
13
|
+
на ту же страницу (кнопка «Назад» из `Page#setBackButton()`) добавлял второй заголовок.
|
|
14
|
+
Название показывает узел `page_name` в шапке, у страницы с меню-баром он по-прежнему скрыт —
|
|
15
|
+
место занимает полоса меню.
|
|
16
|
+
|
|
17
|
+
### Изменено
|
|
18
|
+
|
|
19
|
+
- **`Desktop.app.path()` отвечает значением, а не промисом.** Из путей страница собирает свои
|
|
20
|
+
(файлы, логи, кеш) на старте, где `await` пришлось бы протаскивать через весь код инициализации.
|
|
21
|
+
Путь приходит по синхронному каналу (`SERVICES_SYNC_CALLS`: `ipcRenderer.sendSync` в странице,
|
|
22
|
+
`ipcMain.on` + `event.returnValue` в main) и кешируется — за одним именем в main ходят один раз.
|
|
23
|
+
Ответ идёт конвертом `{value}`/`{error}`: у `sendSync` нет канала для исключения, поэтому отказ
|
|
24
|
+
(имя вне белого списка) приходит странице обычной ошибкой с текстом main. Прежний
|
|
25
|
+
`await Desktop.app.path(...)` продолжает работать — `await` на строке возвращает ту же строку.
|
|
26
|
+
Асинхронный канал `APP_PATH` оставлен: его ключ есть в карте `window.chui.services`.
|
|
27
|
+
|
|
28
|
+
### Добавлено
|
|
29
|
+
|
|
30
|
+
- **`Desktop.app.quit()` и `Desktop.app.restart()`** — закрытие и перезапуск приложения со страницы,
|
|
31
|
+
в том числе из безопасного окна (`window.chui.desktop.app.quit()`). Идут через `Main.stop()`/
|
|
32
|
+
`Main.restart()`, то есть теми же шагами, что пункт меню «Выход»: снимаются обработчики сервисов,
|
|
33
|
+
глобальные клавиши и подписки, а не просто закрывается окно. Отвечают значением, а не промисом
|
|
34
|
+
(синхронные каналы `appQuitSync`/`appRestartSync`), поэтому `await` не обязателен; выход
|
|
35
|
+
откладывается `setImmediate`, чтобы ответ успел уйти странице. Асинхронные каналы
|
|
36
|
+
`APP_QUIT`/`APP_RESTART` оставлены: их ключи есть в карте `window.chui.services`.
|
|
37
|
+
- Тесты `test/chui_services_sync.test.js` и `test/chui_app_lifecycle.test.js`: сверяют синхронные
|
|
38
|
+
каналы (main ↔ мост preload ↔ страница) и то, что закрытие с перезапуском идут именно через
|
|
39
|
+
`Main.stop()`/`Main.restart()`.
|
|
40
|
+
|
|
41
|
+
### Удалено
|
|
42
|
+
|
|
43
|
+
- **Класс `App` и вспомогательный `Application` удалены.** `App.get()`, `App.getSession()`,
|
|
44
|
+
`App.getWebContents()` и все `App.*Path()` больше не экспортируются. Пути теперь отдаёт
|
|
45
|
+
`Desktop.app.path(name)` — и на странице, и в main-процессе: там канала нет, поэтому путь
|
|
46
|
+
берётся у Electron напрямую с той же проверкой белого списка `APP_PATH_NAMES`. Сырые объекты
|
|
47
|
+
Electron в main отдают `Desktop.app.get()`, `Desktop.app.getSession()` и
|
|
48
|
+
`Desktop.app.getWebContents()`; со страницы они бросают ошибку с подсказкой (`Desktop.session.*`,
|
|
49
|
+
`Desktop.dialog` или свой канал из `security.channels`).
|
|
50
|
+
**Ломающее:** `const { App } = require('chuijs')` перестаёт работать.
|
|
51
|
+
- Тест `test/chui_app_paths.test.js` переписан: сверяет белый список с ожидаемым набором имён
|
|
52
|
+
путей и проверяет оба валидатора — `appPathByName` (`index.js`) и `#appPathInMain` (`desktop.js`).
|
|
53
|
+
|
|
54
|
+
### Пакет
|
|
55
|
+
|
|
56
|
+
- Версия 4.0.2.
|
|
57
|
+
|
|
58
|
+
## [4.0.1] - 2026-09-28
|
|
59
|
+
|
|
60
|
+
### Добавлено
|
|
61
|
+
|
|
62
|
+
- **`StickyBar`** — прилипающая полоса: компонент остаётся у верха области прокрутки, пока его
|
|
63
|
+
содержимое не кончилось. В покое не рисует фона и рамки, у закреплённой появляются материал,
|
|
64
|
+
волосяной край и тень; состояние отдаётся в `data-stuck` и `onChange(stuck)`.
|
|
65
|
+
- Раздел `StickyBar` в витрине: карточка в каталоге группы «Компоновка и навигация» с показом
|
|
66
|
+
закрепления.
|
|
67
|
+
- Страница **«История изменений»** на сайте документации: собирается из `CHANGELOG.md`, чтобы
|
|
68
|
+
журнал не расходился с копией в доке. Дорога к ней — из навигации и с бейджа версии на главной.
|
|
69
|
+
- Тест `test/chui_app_paths.test.js`: белый список путей приложения не разойдётся с `App.*Path()`.
|
|
70
|
+
|
|
71
|
+
### Изменено
|
|
72
|
+
|
|
73
|
+
- **Рендер markdown переведён с `showdown` на `markdown-it`** (HTML), обратный разбор — на
|
|
74
|
+
`turndown`. `showdown` не обновлялся с 2022 года, исправленной версии у него нет, а аудит
|
|
75
|
+
показывал три неустранённые уязвимости (ReDoS и две XSS) именно в пути «markdown → innerHTML».
|
|
76
|
+
Новый разбор идёт с `html: false`: сырой HTML в `markdownText` теперь **показывается текстом,
|
|
77
|
+
а не исполняется** — это и закрывает XSS. Таблицы и `~~зачёркивание~~` работают как раньше,
|
|
78
|
+
авто-ссылок нет (адрес становится ссылкой только явным `[текст](адрес)`). Сайт документации
|
|
79
|
+
собирается тем же `markdown-it`; `npm audit` — 0 уязвимостей.
|
|
80
|
+
- **`@electron/remote` убран полностью** — Electron не рекомендует отдавать странице модули
|
|
81
|
+
main-процесса. Кнопки окна ходят в main по каналам `WINDOW_ACTION` / `WINDOW_STATE` (обработчики
|
|
82
|
+
регистрируются и в обычном режиме, `enableServices`), а пути, диалоги и сессия доступны как
|
|
83
|
+
`Desktop.app.path()/info()`, `Desktop.dialog.*`, `Desktop.session.*` или свой канал из
|
|
84
|
+
`security.channels`; в безопасном окне то же отдаёт мост `window.chui`. `chooseFile` со страницы
|
|
85
|
+
идёт по IPC в своё окно, в main работает напрямую. **Ломающее:** `App.get()`, `App.getSession()`,
|
|
86
|
+
`App.getWebContents()` и `App.*Path()` теперь только для main-процесса — со страницы они бросают
|
|
87
|
+
ошибку с подсказкой. Убраны устаревшие `enableRemoteModule` из `webPreferences`.
|
|
88
|
+
- **`Spinner`** переделан: вместо трёх вложенных колец — одно тонкое кольцо с приглушённой
|
|
89
|
+
дорожкой и гладкой дугой со скруглёнными концами. Цвет дуги задаётся `style.color`,
|
|
90
|
+
при `prefers-reduced-motion` вращение замедляется. Публичный API сохранён.
|
|
91
|
+
- **Кнопки окна** (`AppLayout`): значки свернуть/развернуть/восстановить/закрыть нарисованы
|
|
92
|
+
линиями (SVG) вместо лигатур шрифта иконок — не зависят от Material Symbols.
|
|
93
|
+
- **Шапка**: низ скруглён (логические радиусы), волосяная черта снизу сохранена.
|
|
94
|
+
- **`UpdateNotification`**: под спиннером больше нет заливки плашки (`notification_icon_plain`) —
|
|
95
|
+
подложка читалась вторым фоном вокруг тонкого индикатора.
|
|
96
|
+
- Витрина: липкая навигация по разделам переведена на `StickyBar`.
|
|
97
|
+
- `DataGrid.exportToClipboard()`: резервный буфер обмена берётся через `Desktop`, а не напрямую
|
|
98
|
+
из `electron` — работает и в безопасном окне.
|
|
99
|
+
- `chui_functions.setStyles()` кэширует прочитанные таблицы стилей: один файл отдаётся разным
|
|
100
|
+
`id` без повторного чтения с диска.
|
|
101
|
+
- Документация: сайт пересобран, добавлены страницы `StickyBar` и «История изменений», в README —
|
|
102
|
+
раздел про разметку `markdownText`, её безопасный режим и группы моста `window.chui.desktop`.
|
|
103
|
+
Примеры в разделах о безопасности больше не упоминают `enableRemoteModule` — опции, которой в
|
|
104
|
+
`webPreferences` больше нет.
|
|
105
|
+
|
|
106
|
+
### Пакет
|
|
107
|
+
|
|
108
|
+
- Версия 4.0.1.
|
|
109
|
+
- Удалена неиспользуемая зависимость `dataurl`: в коде не было ни одного `require`, ни реэкспорта.
|
|
110
|
+
- Добавлены `repository`, `bugs`, `engines` и карта экспортов (`exports`) с сохранением глубоких
|
|
111
|
+
импортов вида `require('chuijs/framework/modules/chui_fonts')`.
|
|
112
|
+
- Явно указан `"type": "commonjs"`.
|
|
113
|
+
|
|
114
|
+
## [4.0.0]
|
|
115
|
+
|
|
116
|
+
- Версия 4.0.0: реструктуризация модулей, локализация и RTL, десктоп-сервисы и безопасный режим
|
|
117
|
+
окон, статический сайт документации (`npm run docs:build`).
|
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ render(() => new App()).catch(err => Log.error(err))
|
|
|
60
60
|
#### exampleApp / main.js
|
|
61
61
|
```javascript
|
|
62
62
|
/** main.js */
|
|
63
|
-
const { Main, MenuItem, path,
|
|
63
|
+
const { Main, MenuItem, path, Desktop } = require('chuijs');
|
|
64
64
|
const main = new Main({
|
|
65
65
|
name: "exampleApp",
|
|
66
66
|
// размеры окна задаются вложенным объектом sizes
|
|
@@ -73,9 +73,9 @@ const main = new Main({
|
|
|
73
73
|
render: `${__dirname}/app/app.js`,
|
|
74
74
|
devTools: false,
|
|
75
75
|
resizable: true,
|
|
76
|
-
// папка
|
|
76
|
+
// папка загрузок: если не задана, путь выбирает Electron
|
|
77
77
|
paths: {
|
|
78
|
-
downloadPath: path.join(
|
|
78
|
+
downloadPath: path.join(Desktop.app.path("userData"), "downloads")
|
|
79
79
|
}
|
|
80
80
|
/** icon: `${__dirname}/resources/icons/app/icon.png` */
|
|
81
81
|
});
|
|
@@ -95,8 +95,9 @@ main.start({
|
|
|
95
95
|
> поэтому окно доступно через `main.getWindow()` только после старта.
|
|
96
96
|
#### Download Session в main.js
|
|
97
97
|
```javascript
|
|
98
|
-
const {
|
|
99
|
-
|
|
98
|
+
const { Desktop } = require('chuijs');
|
|
99
|
+
const app = Desktop.app.get(); // сырой `app` — только в main-процессе
|
|
100
|
+
app.on('session-created', (session) => {
|
|
100
101
|
session.on('will-download', (e, item, contents) => {
|
|
101
102
|
// ...
|
|
102
103
|
});
|
|
@@ -124,14 +125,14 @@ async function run() {
|
|
|
124
125
|
Создаёт окно, трей и управляет жизненным циклом приложения.
|
|
125
126
|
|
|
126
127
|
```javascript
|
|
127
|
-
const { Main, MenuItem,
|
|
128
|
+
const { Main, MenuItem, Desktop } = require('chuijs');
|
|
128
129
|
const main = new Main({
|
|
129
130
|
name: 'exampleApp',
|
|
130
131
|
sizes: { width: 1366, height: 768, minWidth: 960, minHeight: 540 },
|
|
131
132
|
render: `${__dirname}/app/app.js`,
|
|
132
133
|
devTools: false,
|
|
133
134
|
resizable: true,
|
|
134
|
-
paths: { downloadPath: path.join(
|
|
135
|
+
paths: { downloadPath: path.join(Desktop.app.path('userData'), 'downloads') }
|
|
135
136
|
});
|
|
136
137
|
main.start({ hideOnClose: false, tray: [new MenuItem().quit('Выход')] });
|
|
137
138
|
```
|
|
@@ -178,29 +179,37 @@ main.getWindows(); // [{name, window}, …]
|
|
|
178
179
|
`stop()` и `restart()` снимают свои обработчики: IPC (`show_system_notification`, `SEND_LOG_TEXT`,
|
|
179
180
|
`updateInstallConfirm`), обработчики десктоп-сервисов вместе с глобальными клавишами,
|
|
180
181
|
подписку апдейтера и глобальные обработчики ошибок процессов —
|
|
181
|
-
повторный `start()` подпишет их заново по одному разу.
|
|
182
|
+
повторный `start()` подпишет их заново по одному разу. Со страницы то же делают
|
|
183
|
+
`Desktop.app.quit()` и `Desktop.app.restart()`: страница — код самого приложения, поэтому своей
|
|
184
|
+
кнопке «Выход» не нужен отдельный канал в каждом проекте. Оба отвечают значением, а не промисом
|
|
185
|
+
(синхронный канал, как у `Desktop.app.path`), поэтому `await` не обязателен.
|
|
182
186
|
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
Пути приложения отдаёт `Desktop.app.path(name)` — и в main-процессе, и на странице, значением
|
|
188
|
+
без промиса (на странице — синхронным каналом, в main — прямо у Electron):
|
|
185
189
|
|
|
186
190
|
```javascript
|
|
187
|
-
const {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
const { Desktop } = require('chuijs');
|
|
192
|
+
Desktop.app.path('userData'); // .../AppData/<name>
|
|
193
|
+
Desktop.app.path('downloads');
|
|
194
|
+
Desktop.app.path('logs');
|
|
191
195
|
```
|
|
192
196
|
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
Имена берутся из белого списка: `home`, `appData`, `userData`, `sessionData`, `temp`, `exe`,
|
|
198
|
+
`module`, `desktop`, `documents`, `downloads`, `music`, `pictures`, `videos`, `logs`, `recent`,
|
|
199
|
+
`crashDumps`; всё, что вне списка, отклоняется. Со страницы то же значение доступно как
|
|
200
|
+
`window.chui.desktop.app.path('userData')`.
|
|
201
|
+
|
|
202
|
+
Сырые объекты Electron (`app`, `session`, `webContents`) странице не отдают — Electron не
|
|
203
|
+
рекомендует `remote`. В main-процессе их отдают `Desktop.app.get()`, `Desktop.app.getSession()`
|
|
204
|
+
и `Desktop.app.getWebContents()`:
|
|
195
205
|
|
|
196
206
|
```javascript
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
App.downloadsPath();
|
|
200
|
-
App.logsPath();
|
|
207
|
+
const app = Desktop.app.get(); // main.js
|
|
208
|
+
app.on('session-created', (session) => { /* … */ });
|
|
201
209
|
```
|
|
202
210
|
|
|
203
|
-
|
|
211
|
+
Со страницы эти методы бросают ошибку с подсказкой: вместо них берут `Desktop.session.*`,
|
|
212
|
+
`Desktop.dialog` или свой канал из `security.channels`.
|
|
204
213
|
|
|
205
214
|
#### `Log`
|
|
206
215
|
Пишет в `userData/logs/app_<date>.log`.
|
|
@@ -277,7 +286,8 @@ await bot.sendMessage({ chat_id: '123', text: 'привет' });
|
|
|
277
286
|
доступны только там), а страница работает с ними через `Desktop`: он сам выбирает транспорт —
|
|
278
287
|
канал `ipcRenderer.invoke` в обычном режиме или методы `window.chui.services` (preload +
|
|
279
288
|
`contextBridge`) в безопасном. Имена каналов лежат в одном модуле `chui_services/chui_channels`,
|
|
280
|
-
поэтому main и рендерер не расходятся.
|
|
289
|
+
поэтому main и рендерер не расходятся. Пути приложения — единственное исключение: они идут
|
|
290
|
+
синхронным каналом (`Desktop.app.path` отвечает значением, а не промисом, см. ниже).
|
|
281
291
|
|
|
282
292
|
```javascript
|
|
283
293
|
const { Desktop } = require('chuijs');
|
|
@@ -299,6 +309,16 @@ offTheme();
|
|
|
299
309
|
await Desktop.print.page(); // системный диалог печати текущего окна
|
|
300
310
|
await Desktop.print.html('<h1>Отчёт</h1>'); // печать HTML из скрытого окна
|
|
301
311
|
await Desktop.print.pdf(html, {path: '/tmp/отчёт.pdf'}); // PDF без диалога (или с ним, без path)
|
|
312
|
+
const userData = Desktop.app.path('userData'); // каталог настроек приложения, сразу
|
|
313
|
+
await Desktop.app.info(); // {name, version, locale, isPackaged}
|
|
314
|
+
const app = Desktop.app.get(); // сырой `app` — только в main-процессе
|
|
315
|
+
Desktop.app.quit(); // выход: тот же, что у пункта меню «Выход»
|
|
316
|
+
Desktop.app.restart(); // перезапуск процесса (состояние не переносится)
|
|
317
|
+
const {canceled, filePaths} = await Desktop.dialog.open({properties: ['openFile']});
|
|
318
|
+
await Desktop.dialog.save({defaultPath: '/tmp/отчёт.pdf'});
|
|
319
|
+
await Desktop.dialog.message({type: 'question', message: 'Продолжить?'});
|
|
320
|
+
await Desktop.session.clearCache(); // кеш своей сессии
|
|
321
|
+
await Desktop.session.clearStorageData({storages: ['cookies']});
|
|
302
322
|
```
|
|
303
323
|
|
|
304
324
|
Глобальная клавиша регистрируется в main один раз на комбинацию: сколько бы окон ни подписалось,
|
|
@@ -389,9 +409,9 @@ const preview = await main.createWindow({
|
|
|
389
409
|
|
|
390
410
|
Что меняется в таком окне:
|
|
391
411
|
|
|
392
|
-
- `nodeIntegration: false`, `contextIsolation: true`, `
|
|
393
|
-
`
|
|
394
|
-
в
|
|
412
|
+
- `nodeIntegration: false`, `contextIsolation: true`, `nodeIntegrationInSubFrames: false`,
|
|
413
|
+
`webviewTag: false` — `require`, `process` и `module` в странице недоступны, модули
|
|
414
|
+
main-процесса (в том числе `@electron/remote`) тоже;
|
|
395
415
|
- preload'ом становится мост фреймворка (`framework/modules/chui_bridge/preload.js`),
|
|
396
416
|
а `render` — **скрипт документа**: мост подключает его как обычный `<script>` после
|
|
397
417
|
`DOMContentLoaded`. Поэтому код вроде `require('chuijs')` в таком окне не работает —
|
|
@@ -409,6 +429,13 @@ window.chui.desktop.clipboard.readText(); // то же, что Desktop из re
|
|
|
409
429
|
window.chui.desktop.recent.list();
|
|
410
430
|
window.chui.desktop.progress.set(0.4);
|
|
411
431
|
const off = window.chui.desktop.shortcuts.on('Mod+Shift+P', () => {});
|
|
432
|
+
window.chui.desktop.app.path('userData'); // путь приложения — сразу, без промиса
|
|
433
|
+
await window.chui.desktop.app.info(); // {name, version, locale, isPackaged}
|
|
434
|
+
window.chui.desktop.app.quit(); // закрыть всё приложение (окно — window.chui.window)
|
|
435
|
+
window.chui.desktop.app.restart(); // перезапустить приложение (тоже без промиса)
|
|
436
|
+
const res = await window.chui.desktop.dialog.open({properties: ['openFile']});
|
|
437
|
+
await window.chui.desktop.dialog.message({type: 'question', message: 'Продолжить?'});
|
|
438
|
+
await window.chui.desktop.session.clearCache();
|
|
412
439
|
window.chui.theme.get(); // 'light' | 'dark'
|
|
413
440
|
window.chui.theme.onChange((theme) => {});
|
|
414
441
|
window.chui.window.minimize(); window.chui.window.toggleMaximize(); window.chui.window.close();
|
|
@@ -435,7 +462,9 @@ const { BrowserWindow, ipcMain, ipcRenderer, shell, path, fs, os, request,
|
|
|
435
462
|
transliterate, systemPreferences, chooseFile, store, downloadSession } = require('chuijs');
|
|
436
463
|
```
|
|
437
464
|
|
|
438
|
-
`chooseFile` —
|
|
465
|
+
`chooseFile` — диалоги `showOpenDialog` / `showSaveDialog` / `showMessageBox`: со страницы они идут
|
|
466
|
+
по IPC в окно, которое их вызвало (`Desktop.dialog`), а в main — напрямую `dialog` Electron.
|
|
467
|
+
`store` — готовый экземпляр `new Store()`.
|
|
439
468
|
|
|
440
469
|
#### Автоматическое обновление приложения
|
|
441
470
|
Только AppImage и NSIS.
|
|
@@ -604,6 +633,12 @@ Desktop.locale.onChange((locale) => I18n.setLocale(locale));
|
|
|
604
633
|
DOM-элемент, поэтому внутри `Page.add(...)`, `Card.add(...)`, `ContentBlock.add(...)` передаётся
|
|
605
634
|
сам объект, а в `Form.components` / `SlideShow.SLIDE` / `FieldSet.components` — тоже объекты.
|
|
606
635
|
|
|
636
|
+
**Разметка `markdownText`.** Компоненты с этой опцией (`Label`, `Tooltip`, `Badge`, `Alert`,
|
|
637
|
+
`Notification`, `ToastQueue`, `Chat`, `Tour`, `MarkdownEditor`) рендерят её `markdown-it` в безопасном
|
|
638
|
+
режиме: сырой HTML из разметки не исполняется, а показывается текстом — так закрыт XSS через
|
|
639
|
+
`markdownText`. Таблицы и `~~зачёркивание~~` поддерживаются, авто-ссылок нет: адрес становится
|
|
640
|
+
ссылкой только явным `[текст](адрес)`. Обратный разбор (`htmlToMarkdown`) делает `turndown`.
|
|
641
|
+
|
|
607
642
|
Описание каждого компонента — отдельная страница статического сайта документации в [`docs/`](docs/):
|
|
608
643
|
[`docs/index.html`](docs/index.html) — полный индекс по группам и алфавиту, а у каждой страницы сверху
|
|
609
644
|
ссылка на группу и общий список. Сайт собирается генератором `node docs/build.js`
|
|
@@ -621,7 +656,7 @@ DOM-элемент, поэтому внутри `Page.add(...)`, `Card.add(...)`
|
|
|
621
656
|
[Константы и статические фабрики](docs/topics/constants.html), [Сценарии целиком](docs/topics/scenarios.html),
|
|
622
657
|
[Витрина компонентов](docs/topics/showcase.html).
|
|
623
658
|
|
|
624
|
-
Модули и десктоп-сервисы (`Main`, `
|
|
659
|
+
Модули и десктоп-сервисы (`Main`, `I18n`, `Theme`, `Desktop`, `Log`) описаны выше, в разделе «Модули».
|
|
625
660
|
|
|
626
661
|
### В разработке
|
|
627
662
|
|
|
@@ -13,6 +13,7 @@ const {DownloadNotification} = require("../../components/chui_notification/notif
|
|
|
13
13
|
const {Log} = require("../../../framework/modules/chui_logger/chui_logger");
|
|
14
14
|
const {MenuKeyboard, makeKeyboardButton} = require('../../modules/chui_a11y/chui_a11y');
|
|
15
15
|
const {LAYOUT_ROLE, LayoutRefs, registerLayout, requireLayoutNode} = require('../../modules/chui_layout/chui_layout');
|
|
16
|
+
const {BRIDGE_CHANNELS} = require('../../modules/chui_services/chui_channels');
|
|
16
17
|
const {OverlayScrollbars} = require('../../modules/chui_scrollbar/chui_scrollbar');
|
|
17
18
|
const {MenuBar} = require('../../components/chui_menu_bar/menu_bar');
|
|
18
19
|
const {Avatar} = require('../../components/chui_avatar/avatar');
|
|
@@ -88,19 +89,12 @@ class Route extends Events {
|
|
|
88
89
|
//
|
|
89
90
|
setPageName(page.getTitle());
|
|
90
91
|
center.innerHTML = '';
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const block = document.createElement("page_title");
|
|
98
|
-
const icon = page.getIcon();
|
|
99
|
-
if (icon !== undefined && icon !== null) block.innerHTML = String(icon);
|
|
100
|
-
block.appendChild(document.createTextNode(String(title)));
|
|
101
|
-
page_node.prepend(block);
|
|
102
|
-
}
|
|
103
|
-
center.appendChild(page_node);
|
|
92
|
+
// Содержимое собирает сама страница (`Page#render`), оболочка в него не подмешивает
|
|
93
|
+
// ничего: узел `page_title` (был здесь) дублировал заголовок, который страница
|
|
94
|
+
// рисует сама, а `Page#render` отдаёт один и тот же узел — повторный переход на тот
|
|
95
|
+
// же экземпляр страницы (кнопка «Назад» из `Page#setBackButton`) добавлял второй
|
|
96
|
+
// заголовок. Место названия — шапка (`setPageName`).
|
|
97
|
+
center.appendChild(page.render());
|
|
104
98
|
const _page = center.querySelector('page');
|
|
105
99
|
new Animation(_page).fadeIn();
|
|
106
100
|
// Стирания `center.removeAttribute("style")` здесь больше нет: инлайновых
|
|
@@ -157,6 +151,22 @@ function makeHeaderButtonBox(options = { title: undefined, icon: undefined, reve
|
|
|
157
151
|
return button;
|
|
158
152
|
}
|
|
159
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Значки кнопок окна нарисованы линиями, а не взяты из шрифта иконок: системные кнопки
|
|
156
|
+
* должны выглядеть одинаково, даже если Material Symbols недоступен (веб-сборка, безопасное
|
|
157
|
+
* окно, где шрифт фреймворка не подключён). «Свернуть»/«развернуть» — зеркальные шевроны,
|
|
158
|
+
* «восстановить» — контурный ромб, «закрыть» — крест. Все четыре значка вписаны в один
|
|
159
|
+
* квадрат 20×20 и центрированы геометрически (чернила симметричны относительно 10,10),
|
|
160
|
+
* поэтому в кнопке они стоят ровно и читаются одной тройкой. Толщина 1.3 и скругления
|
|
161
|
+
* подобраны под вид набора; цвет наследуется от кнопки через `currentColor`.
|
|
162
|
+
*/
|
|
163
|
+
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>',
|
|
167
|
+
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
|
+
};
|
|
169
|
+
|
|
160
170
|
class WindowControls {
|
|
161
171
|
// Управление кнопками
|
|
162
172
|
#box = document.createElement("wc_box")
|
|
@@ -166,70 +176,73 @@ class WindowControls {
|
|
|
166
176
|
// Подписка на нересайзящееся окно: set() вызывается и из конструктора Header, и из
|
|
167
177
|
// addWC(), поэтому в set() слушатель ipcRenderer накапливался.
|
|
168
178
|
#resizable_false_event = () => this.#maximize.remove();
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
#current_window = false;
|
|
179
|
+
// Номер запроса состояния гасит ответы, пришедшие не в том порядке
|
|
180
|
+
#sync_request = 0;
|
|
172
181
|
constructor() {
|
|
173
182
|
// Скрипт окна может быть preload'ом и выполняться до документа: здесь документ уже настоящий,
|
|
174
183
|
// поэтому `dir`/`lang` (и зеркалирование логических свойств CSS) встают гарантированно
|
|
175
184
|
I18n.apply();
|
|
176
|
-
this.#setButton(this.#close, I18n.t("appLayout.window.close"),
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
this.#
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (window_ !== undefined) window_.close();
|
|
187
|
-
})
|
|
188
|
-
this.#minimize.addEventListener("click", () => {
|
|
189
|
-
const window_ = this.#getWindow();
|
|
190
|
-
if (window_ !== undefined) window_.minimize();
|
|
191
|
-
})
|
|
192
|
-
this.#maximize.addEventListener("click", () => {
|
|
193
|
-
const window_ = this.#getWindow();
|
|
194
|
-
if (window_ === undefined) return;
|
|
195
|
-
window_.isMaximized() ? window_.unmaximize() : window_.maximize();
|
|
196
|
-
})
|
|
185
|
+
this.#setButton(this.#close, I18n.t("appLayout.window.close"), "close");
|
|
186
|
+
// «Свернуть» — шеврон вниз: плоская широкая галочка читается лучше горизонтальной
|
|
187
|
+
// черты и не сливается с рамкой кнопки
|
|
188
|
+
this.#setButton(this.#minimize, I18n.t("appLayout.window.minimize"), "minimize");
|
|
189
|
+
// «Развернуть» — шеврон вверх, зеркало «свернуть»: пара читается как одна ось
|
|
190
|
+
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"))
|
|
193
|
+
// Разворот и восстановление — одно действие: решение принимает окно, а не кнопка
|
|
194
|
+
this.#maximize.addEventListener("click", () => this.#windowAction("toggle-maximize"))
|
|
197
195
|
// Разворот приходит не только от нашей кнопки (двойной клик по полосе, меню окна),
|
|
198
196
|
// поэтому состояние читаем по факту изменения размера окна
|
|
199
197
|
window.addEventListener("resize", this.#sync_maximized_event)
|
|
200
198
|
this.#syncMaximized()
|
|
201
199
|
ipcRenderer.on("chui_resizable_false", this.#resizable_false_event)
|
|
202
200
|
}
|
|
203
|
-
/** Кнопка окна —
|
|
204
|
-
*
|
|
205
|
-
* иконок шапки, поэтому опция размера здесь не передаётся */
|
|
201
|
+
/** Кнопка окна — значок с именем для скринридера, фокусом и активацией с клавиатуры.
|
|
202
|
+
* Значок рисуется линиями (`WINDOW_ICONS`), размер задаёт CSS (см. global_style.css) */
|
|
206
203
|
#setButton(button, label, icon) {
|
|
207
|
-
button.innerHTML =
|
|
204
|
+
button.innerHTML = WINDOW_ICONS[icon];
|
|
208
205
|
button.setAttribute("title", label);
|
|
209
206
|
button.setAttribute("aria-label", label);
|
|
210
207
|
makeKeyboardButton(button);
|
|
211
208
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Действия кнопок окна идут по IPC в main (`WINDOW_ACTION`/`WINDOW_STATE`). Прежде здесь
|
|
211
|
+
* стоял `@electron/remote`, но он устарел и тянул модули main-процесса прямо в страницу.
|
|
212
|
+
* Обработчики берут окно отправителя, поэтому канал не может задеть чужое окно; ошибка
|
|
213
|
+
* глушится — без `Main` (обработчики не зарегистрированы) нажатие не должно ломать страницу.
|
|
214
|
+
*/
|
|
215
|
+
#windowAction(action = String()) {
|
|
216
|
+
try {
|
|
217
|
+
return Promise.resolve(ipcRenderer.invoke(BRIDGE_CHANNELS.WINDOW_ACTION, action)).catch(() => false);
|
|
218
|
+
} catch (_error) {
|
|
219
|
+
return Promise.resolve(false);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Состояние окна: `{isMaximized, isMinimized, …}`; без окна — пустой объект */
|
|
224
|
+
async #windowState() {
|
|
225
|
+
try {
|
|
226
|
+
return (await ipcRenderer.invoke(BRIDGE_CHANNELS.WINDOW_STATE)) ?? {};
|
|
227
|
+
} catch (_error) {
|
|
228
|
+
return {};
|
|
219
229
|
}
|
|
220
|
-
return this.#current_window === null ? undefined : this.#current_window;
|
|
221
230
|
}
|
|
222
|
-
// Развёрнутое окно — другой
|
|
223
|
-
//
|
|
231
|
+
// Развёрнутое окно — другой значок и другая подпись: кнопка возвращает окно в оконный режим.
|
|
232
|
+
// «Восстановить» — контурный ромб; размер значкам задаёт CSS.
|
|
233
|
+
// Состояние приходит асинхронно, поэтому ответ устаревшего запроса отбрасывается по номеру:
|
|
234
|
+
// иначе быстрая пара «развернуть/восстановить» могла бы применить ответ в обратном порядке
|
|
224
235
|
#syncMaximized() {
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
236
|
+
const request = ++this.#sync_request;
|
|
237
|
+
this.#windowState().then((state) => {
|
|
238
|
+
if (request !== this.#sync_request) return;
|
|
239
|
+
const maximized = state.isMaximized === true;
|
|
240
|
+
const label = maximized ? I18n.t("appLayout.window.restore") : I18n.t("appLayout.window.maximize");
|
|
241
|
+
this.#maximize.classList.toggle("wc_maximized", maximized);
|
|
242
|
+
this.#maximize.innerHTML = WINDOW_ICONS[maximized ? "restore" : "maximize"];
|
|
243
|
+
this.#maximize.setAttribute("title", label);
|
|
244
|
+
this.#maximize.setAttribute("aria-label", label);
|
|
245
|
+
});
|
|
233
246
|
}
|
|
234
247
|
#sync_maximized_event = () => this.#syncMaximized();
|
|
235
248
|
set(pos_bool = Boolean()) {
|
|
@@ -454,15 +454,17 @@ header {
|
|
|
454
454
|
display: flex;
|
|
455
455
|
z-index: 999;
|
|
456
456
|
flex-direction: column;
|
|
457
|
-
/* Шапка — плоская полоса во всю
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
457
|
+
/* Шапка — плоская полоса во всю ширину: содержимое уходит под неё (материал
|
|
458
|
+
полупрозрачный, с блюром), низ скруглён и подчёркнут волосяной чертой. Радиус задан
|
|
459
|
+
логическими свойствами, поэтому в RTL скругление зеркалится; верхние углы прямые —
|
|
460
|
+
их срезал бы композитор. */
|
|
461
461
|
align-items: start;
|
|
462
462
|
background-color: var(--header_background);
|
|
463
463
|
backdrop-filter: var(--global_blur);
|
|
464
464
|
justify-content: space-between;
|
|
465
465
|
border-bottom: 1px solid var(--border_color);
|
|
466
|
+
border-end-start-radius: var(--radius_md);
|
|
467
|
+
border-end-end-radius: var(--radius_md);
|
|
466
468
|
}
|
|
467
469
|
|
|
468
470
|
/* Прокрутка содержимого: материал плотнее и появляется тень — полоса читается
|
|
@@ -882,11 +884,14 @@ app_menu_empty {
|
|
|
882
884
|
ниже содержимого, и длинное название выдавило бы кнопки за край окна. Полный текст
|
|
883
885
|
лежит в подсказке (её ставит setPageName в chui_app_layout.js). */
|
|
884
886
|
page_name {
|
|
885
|
-
/* Название
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
887
|
+
/* Название показывает шапка, а не содержимое: оболочка в страницу его не подмешивает
|
|
888
|
+
(`Route#go`), поэтому приложение, рисующее свой заголовок, ничего не дублирует.
|
|
889
|
+
Узел — якорь полосы меню (`applyPageMenuBar` ставит её следом), и он же держит текст
|
|
890
|
+
для меню маршрутов и подсказки (`setPageName`).
|
|
891
|
+
У страницы с меню-баром название скрыто — место занимает полоса (правило
|
|
892
|
+
`.header_with_menu_bar page_name` ниже) */
|
|
893
|
+
display: flex;
|
|
894
|
+
align-items: center;
|
|
890
895
|
outline: none;
|
|
891
896
|
box-sizing: border-box;
|
|
892
897
|
min-width: 0;
|
|
@@ -1253,25 +1258,21 @@ wc_close, wc_maximize, wc_minimize {
|
|
|
1253
1258
|
transition: background-color var(--transition_time) ease, color var(--transition_time) ease;
|
|
1254
1259
|
}
|
|
1255
1260
|
|
|
1256
|
-
/*
|
|
1257
|
-
кнопки
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1261
|
+
/* Значки кнопок окна нарисованы линиями (SVG), а не взяты из шрифта иконок: три системные
|
|
1262
|
+
кнопки должны выглядеть одинаково и тогда, когда Material Symbols недоступен (веб-сборка,
|
|
1263
|
+
безопасное окно без шрифта фреймворка). Все четыре значка вписаны в один квадрат 20×20 и
|
|
1264
|
+
центрированы геометрически, поэтому при общем размере 16 пикселей стоят ровно и читаются
|
|
1265
|
+
одной тройкой. Цвет приходит от кнопки через `currentColor`, поэтому состояния ниже красят
|
|
1266
|
+
сам `svg`. */
|
|
1267
|
+
wc_close svg,
|
|
1268
|
+
wc_maximize svg,
|
|
1269
|
+
wc_minimize svg {
|
|
1270
|
+
display: block;
|
|
1271
|
+
width: 16px;
|
|
1272
|
+
height: 16px;
|
|
1265
1273
|
color: var(--text_color_disabled);
|
|
1266
1274
|
}
|
|
1267
1275
|
|
|
1268
|
-
/* «Восстановить» — два квадрата вместо одного: знак плотнее и при общем кегле выходил
|
|
1269
|
-
крупнее одиночного квадрата (чернила 15×15 против 11×11). Меньший кегль выравнивает
|
|
1270
|
-
боксы: 9.5×9.5 у «восстановить» против 9.6×9.6 у «развернуть» */
|
|
1271
|
-
wc_maximize.wc_maximized chui_icon {
|
|
1272
|
-
font-size: 10px;
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
1276
|
wc_maximize:hover, wc_minimize:hover {
|
|
1276
1277
|
background: var(--element_background_hover);
|
|
1277
1278
|
}
|
|
@@ -1280,13 +1281,13 @@ wc_maximize:active, wc_minimize:active {
|
|
|
1280
1281
|
background: var(--element_background_prime); color: var(--text_color_hover);
|
|
1281
1282
|
}
|
|
1282
1283
|
|
|
1283
|
-
wc_maximize:hover
|
|
1284
|
-
wc_maximize:active
|
|
1285
|
-
wc_minimize:hover
|
|
1286
|
-
wc_minimize:active
|
|
1287
|
-
wc_close:focus-visible
|
|
1288
|
-
wc_maximize:focus-visible
|
|
1289
|
-
wc_minimize:focus-visible
|
|
1284
|
+
wc_maximize:hover svg,
|
|
1285
|
+
wc_maximize:active svg,
|
|
1286
|
+
wc_minimize:hover svg,
|
|
1287
|
+
wc_minimize:active svg,
|
|
1288
|
+
wc_close:focus-visible svg,
|
|
1289
|
+
wc_maximize:focus-visible svg,
|
|
1290
|
+
wc_minimize:focus-visible svg {
|
|
1290
1291
|
color: var(--text_color);
|
|
1291
1292
|
}
|
|
1292
1293
|
|
|
@@ -1297,8 +1298,8 @@ wc_close:active {
|
|
|
1297
1298
|
background: var(--red_prime_background);
|
|
1298
1299
|
}
|
|
1299
1300
|
|
|
1300
|
-
wc_close:hover
|
|
1301
|
-
wc_close:active
|
|
1301
|
+
wc_close:hover svg,
|
|
1302
|
+
wc_close:active svg {
|
|
1302
1303
|
color: var(--text_color);
|
|
1303
1304
|
}
|
|
1304
1305
|
|