chuijs 4.0.0 → 4.0.1
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 +65 -0
- package/README.md +29 -6
- package/framework/appLayout/default/chui_app_layout.js +66 -46
- package/framework/appLayout/default/global_style.css +27 -29
- 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 +14 -0
- package/framework/modules/chui_functions.js +22 -6
- package/framework/modules/chui_services/chui_channels.js +33 -2
- package/framework/modules/chui_services/desktop.js +45 -0
- package/index.js +67 -28
- package/package.json +24 -6
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
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.1] - 2026-09-28
|
|
7
|
+
|
|
8
|
+
### Добавлено
|
|
9
|
+
|
|
10
|
+
- **`StickyBar`** — прилипающая полоса: компонент остаётся у верха области прокрутки, пока его
|
|
11
|
+
содержимое не кончилось. В покое не рисует фона и рамки, у закреплённой появляются материал,
|
|
12
|
+
волосяной край и тень; состояние отдаётся в `data-stuck` и `onChange(stuck)`.
|
|
13
|
+
- Раздел `StickyBar` в витрине: карточка в каталоге группы «Компоновка и навигация» с показом
|
|
14
|
+
закрепления.
|
|
15
|
+
- Страница **«История изменений»** на сайте документации: собирается из `CHANGELOG.md`, чтобы
|
|
16
|
+
журнал не расходился с копией в доке. Дорога к ней — из навигации и с бейджа версии на главной.
|
|
17
|
+
- Тест `test/chui_app_paths.test.js`: белый список путей приложения не разойдётся с `App.*Path()`.
|
|
18
|
+
|
|
19
|
+
### Изменено
|
|
20
|
+
|
|
21
|
+
- **Рендер markdown переведён с `showdown` на `markdown-it`** (HTML), обратный разбор — на
|
|
22
|
+
`turndown`. `showdown` не обновлялся с 2022 года, исправленной версии у него нет, а аудит
|
|
23
|
+
показывал три неустранённые уязвимости (ReDoS и две XSS) именно в пути «markdown → innerHTML».
|
|
24
|
+
Новый разбор идёт с `html: false`: сырой HTML в `markdownText` теперь **показывается текстом,
|
|
25
|
+
а не исполняется** — это и закрывает XSS. Таблицы и `~~зачёркивание~~` работают как раньше,
|
|
26
|
+
авто-ссылок нет (адрес становится ссылкой только явным `[текст](адрес)`). Сайт документации
|
|
27
|
+
собирается тем же `markdown-it`; `npm audit` — 0 уязвимостей.
|
|
28
|
+
- **`@electron/remote` убран полностью** — Electron не рекомендует отдавать странице модули
|
|
29
|
+
main-процесса. Кнопки окна ходят в main по каналам `WINDOW_ACTION` / `WINDOW_STATE` (обработчики
|
|
30
|
+
регистрируются и в обычном режиме, `enableServices`), а пути, диалоги и сессия доступны как
|
|
31
|
+
`Desktop.app.path()/info()`, `Desktop.dialog.*`, `Desktop.session.*` или свой канал из
|
|
32
|
+
`security.channels`; в безопасном окне то же отдаёт мост `window.chui`. `chooseFile` со страницы
|
|
33
|
+
идёт по IPC в своё окно, в main работает напрямую. **Ломающее:** `App.get()`, `App.getSession()`,
|
|
34
|
+
`App.getWebContents()` и `App.*Path()` теперь только для main-процесса — со страницы они бросают
|
|
35
|
+
ошибку с подсказкой. Убраны устаревшие `enableRemoteModule` из `webPreferences`.
|
|
36
|
+
- **`Spinner`** переделан: вместо трёх вложенных колец — одно тонкое кольцо с приглушённой
|
|
37
|
+
дорожкой и гладкой дугой со скруглёнными концами. Цвет дуги задаётся `style.color`,
|
|
38
|
+
при `prefers-reduced-motion` вращение замедляется. Публичный API сохранён.
|
|
39
|
+
- **Кнопки окна** (`AppLayout`): значки свернуть/развернуть/восстановить/закрыть нарисованы
|
|
40
|
+
линиями (SVG) вместо лигатур шрифта иконок — не зависят от Material Symbols.
|
|
41
|
+
- **Шапка**: низ скруглён (логические радиусы), волосяная черта снизу сохранена.
|
|
42
|
+
- **`UpdateNotification`**: под спиннером больше нет заливки плашки (`notification_icon_plain`) —
|
|
43
|
+
подложка читалась вторым фоном вокруг тонкого индикатора.
|
|
44
|
+
- Витрина: липкая навигация по разделам переведена на `StickyBar`.
|
|
45
|
+
- `DataGrid.exportToClipboard()`: резервный буфер обмена берётся через `Desktop`, а не напрямую
|
|
46
|
+
из `electron` — работает и в безопасном окне.
|
|
47
|
+
- `chui_functions.setStyles()` кэширует прочитанные таблицы стилей: один файл отдаётся разным
|
|
48
|
+
`id` без повторного чтения с диска.
|
|
49
|
+
- Документация: сайт пересобран, добавлены страницы `StickyBar` и «История изменений», в README —
|
|
50
|
+
раздел про разметку `markdownText`, её безопасный режим и группы моста `window.chui.desktop`.
|
|
51
|
+
Примеры в разделах о безопасности больше не упоминают `enableRemoteModule` — опции, которой в
|
|
52
|
+
`webPreferences` больше нет.
|
|
53
|
+
|
|
54
|
+
### Пакет
|
|
55
|
+
|
|
56
|
+
- Версия 4.0.1.
|
|
57
|
+
- Удалена неиспользуемая зависимость `dataurl`: в коде не было ни одного `require`, ни реэкспорта.
|
|
58
|
+
- Добавлены `repository`, `bugs`, `engines` и карта экспортов (`exports`) с сохранением глубоких
|
|
59
|
+
импортов вида `require('chuijs/framework/modules/chui_fonts')`.
|
|
60
|
+
- Явно указан `"type": "commonjs"`.
|
|
61
|
+
|
|
62
|
+
## [4.0.0]
|
|
63
|
+
|
|
64
|
+
- Версия 4.0.0: реструктуризация модулей, локализация и RTL, десктоп-сервисы и безопасный режим
|
|
65
|
+
окон, статический сайт документации (`npm run docs:build`).
|
package/README.md
CHANGED
|
@@ -181,7 +181,9 @@ main.getWindows(); // [{name, window}, …]
|
|
|
181
181
|
повторный `start()` подпишет их заново по одному разу.
|
|
182
182
|
|
|
183
183
|
#### `App`
|
|
184
|
-
Обёртка над `app` Electron.
|
|
184
|
+
Обёртка над `app` Electron. **Только для main-процесса:** странице модули main не отдают
|
|
185
|
+
(Electron не рекомендует `remote`), и её `App.get*` бросает ошибку с подсказкой. Со страницы
|
|
186
|
+
берут `Desktop.app`, `Desktop.session`, `Desktop.dialog` или свой канал из `security.channels`.
|
|
185
187
|
|
|
186
188
|
```javascript
|
|
187
189
|
const { App } = require('chuijs');
|
|
@@ -191,7 +193,8 @@ App.getWebContents(); // webContents
|
|
|
191
193
|
```
|
|
192
194
|
|
|
193
195
|
#### `App.*Path()`
|
|
194
|
-
Статические системные пути (все возвращают строку).
|
|
196
|
+
Статические системные пути (все возвращают строку). **Только для main-процесса;** со страницы —
|
|
197
|
+
`await Desktop.app.path('userData')` (там путь приходит по IPC и потому асинхронно).
|
|
195
198
|
|
|
196
199
|
```javascript
|
|
197
200
|
const { App } = require('chuijs');
|
|
@@ -299,6 +302,13 @@ offTheme();
|
|
|
299
302
|
await Desktop.print.page(); // системный диалог печати текущего окна
|
|
300
303
|
await Desktop.print.html('<h1>Отчёт</h1>'); // печать HTML из скрытого окна
|
|
301
304
|
await Desktop.print.pdf(html, {path: '/tmp/отчёт.pdf'}); // PDF без диалога (или с ним, без path)
|
|
305
|
+
await Desktop.app.path('userData'); // каталог настроек приложения
|
|
306
|
+
await Desktop.app.info(); // {name, version, locale, isPackaged}
|
|
307
|
+
const {canceled, filePaths} = await Desktop.dialog.open({properties: ['openFile']});
|
|
308
|
+
await Desktop.dialog.save({defaultPath: '/tmp/отчёт.pdf'});
|
|
309
|
+
await Desktop.dialog.message({type: 'question', message: 'Продолжить?'});
|
|
310
|
+
await Desktop.session.clearCache(); // кеш своей сессии
|
|
311
|
+
await Desktop.session.clearStorageData({storages: ['cookies']});
|
|
302
312
|
```
|
|
303
313
|
|
|
304
314
|
Глобальная клавиша регистрируется в main один раз на комбинацию: сколько бы окон ни подписалось,
|
|
@@ -389,9 +399,9 @@ const preview = await main.createWindow({
|
|
|
389
399
|
|
|
390
400
|
Что меняется в таком окне:
|
|
391
401
|
|
|
392
|
-
- `nodeIntegration: false`, `contextIsolation: true`, `
|
|
393
|
-
`
|
|
394
|
-
в
|
|
402
|
+
- `nodeIntegration: false`, `contextIsolation: true`, `nodeIntegrationInSubFrames: false`,
|
|
403
|
+
`webviewTag: false` — `require`, `process` и `module` в странице недоступны, модули
|
|
404
|
+
main-процесса (в том числе `@electron/remote`) тоже;
|
|
395
405
|
- preload'ом становится мост фреймворка (`framework/modules/chui_bridge/preload.js`),
|
|
396
406
|
а `render` — **скрипт документа**: мост подключает его как обычный `<script>` после
|
|
397
407
|
`DOMContentLoaded`. Поэтому код вроде `require('chuijs')` в таком окне не работает —
|
|
@@ -409,6 +419,11 @@ window.chui.desktop.clipboard.readText(); // то же, что Desktop из re
|
|
|
409
419
|
window.chui.desktop.recent.list();
|
|
410
420
|
window.chui.desktop.progress.set(0.4);
|
|
411
421
|
const off = window.chui.desktop.shortcuts.on('Mod+Shift+P', () => {});
|
|
422
|
+
await window.chui.desktop.app.path('userData'); // путь приложения — по IPC, потому асинхронно
|
|
423
|
+
await window.chui.desktop.app.info(); // {name, version, locale, isPackaged}
|
|
424
|
+
const res = await window.chui.desktop.dialog.open({properties: ['openFile']});
|
|
425
|
+
await window.chui.desktop.dialog.message({type: 'question', message: 'Продолжить?'});
|
|
426
|
+
await window.chui.desktop.session.clearCache();
|
|
412
427
|
window.chui.theme.get(); // 'light' | 'dark'
|
|
413
428
|
window.chui.theme.onChange((theme) => {});
|
|
414
429
|
window.chui.window.minimize(); window.chui.window.toggleMaximize(); window.chui.window.close();
|
|
@@ -435,7 +450,9 @@ const { BrowserWindow, ipcMain, ipcRenderer, shell, path, fs, os, request,
|
|
|
435
450
|
transliterate, systemPreferences, chooseFile, store, downloadSession } = require('chuijs');
|
|
436
451
|
```
|
|
437
452
|
|
|
438
|
-
`chooseFile` —
|
|
453
|
+
`chooseFile` — диалоги `showOpenDialog` / `showSaveDialog` / `showMessageBox`: со страницы они идут
|
|
454
|
+
по IPC в окно, которое их вызвало (`Desktop.dialog`), а в main — напрямую `dialog` Electron.
|
|
455
|
+
`store` — готовый экземпляр `new Store()`.
|
|
439
456
|
|
|
440
457
|
#### Автоматическое обновление приложения
|
|
441
458
|
Только AppImage и NSIS.
|
|
@@ -604,6 +621,12 @@ Desktop.locale.onChange((locale) => I18n.setLocale(locale));
|
|
|
604
621
|
DOM-элемент, поэтому внутри `Page.add(...)`, `Card.add(...)`, `ContentBlock.add(...)` передаётся
|
|
605
622
|
сам объект, а в `Form.components` / `SlideShow.SLIDE` / `FieldSet.components` — тоже объекты.
|
|
606
623
|
|
|
624
|
+
**Разметка `markdownText`.** Компоненты с этой опцией (`Label`, `Tooltip`, `Badge`, `Alert`,
|
|
625
|
+
`Notification`, `ToastQueue`, `Chat`, `Tour`, `MarkdownEditor`) рендерят её `markdown-it` в безопасном
|
|
626
|
+
режиме: сырой HTML из разметки не исполняется, а показывается текстом — так закрыт XSS через
|
|
627
|
+
`markdownText`. Таблицы и `~~зачёркивание~~` поддерживаются, авто-ссылок нет: адрес становится
|
|
628
|
+
ссылкой только явным `[текст](адрес)`. Обратный разбор (`htmlToMarkdown`) делает `turndown`.
|
|
629
|
+
|
|
607
630
|
Описание каждого компонента — отдельная страница статического сайта документации в [`docs/`](docs/):
|
|
608
631
|
[`docs/index.html`](docs/index.html) — полный индекс по группам и алфавиту, а у каждой страницы сверху
|
|
609
632
|
ссылка на группу и общий список. Сайт собирается генератором `node docs/build.js`
|
|
@@ -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');
|
|
@@ -157,6 +158,22 @@ function makeHeaderButtonBox(options = { title: undefined, icon: undefined, reve
|
|
|
157
158
|
return button;
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Значки кнопок окна нарисованы линиями, а не взяты из шрифта иконок: системные кнопки
|
|
163
|
+
* должны выглядеть одинаково, даже если Material Symbols недоступен (веб-сборка, безопасное
|
|
164
|
+
* окно, где шрифт фреймворка не подключён). «Свернуть»/«развернуть» — зеркальные шевроны,
|
|
165
|
+
* «восстановить» — контурный ромб, «закрыть» — крест. Все четыре значка вписаны в один
|
|
166
|
+
* квадрат 20×20 и центрированы геометрически (чернила симметричны относительно 10,10),
|
|
167
|
+
* поэтому в кнопке они стоят ровно и читаются одной тройкой. Толщина 1.3 и скругления
|
|
168
|
+
* подобраны под вид набора; цвет наследуется от кнопки через `currentColor`.
|
|
169
|
+
*/
|
|
170
|
+
const WINDOW_ICONS = {
|
|
171
|
+
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>',
|
|
172
|
+
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>',
|
|
173
|
+
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>',
|
|
174
|
+
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>',
|
|
175
|
+
};
|
|
176
|
+
|
|
160
177
|
class WindowControls {
|
|
161
178
|
// Управление кнопками
|
|
162
179
|
#box = document.createElement("wc_box")
|
|
@@ -166,70 +183,73 @@ class WindowControls {
|
|
|
166
183
|
// Подписка на нересайзящееся окно: set() вызывается и из конструктора Header, и из
|
|
167
184
|
// addWC(), поэтому в set() слушатель ipcRenderer накапливался.
|
|
168
185
|
#resizable_false_event = () => this.#maximize.remove();
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
#current_window = false;
|
|
186
|
+
// Номер запроса состояния гасит ответы, пришедшие не в том порядке
|
|
187
|
+
#sync_request = 0;
|
|
172
188
|
constructor() {
|
|
173
189
|
// Скрипт окна может быть preload'ом и выполняться до документа: здесь документ уже настоящий,
|
|
174
190
|
// поэтому `dir`/`lang` (и зеркалирование логических свойств CSS) встают гарантированно
|
|
175
191
|
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
|
-
})
|
|
192
|
+
this.#setButton(this.#close, I18n.t("appLayout.window.close"), "close");
|
|
193
|
+
// «Свернуть» — шеврон вниз: плоская широкая галочка читается лучше горизонтальной
|
|
194
|
+
// черты и не сливается с рамкой кнопки
|
|
195
|
+
this.#setButton(this.#minimize, I18n.t("appLayout.window.minimize"), "minimize");
|
|
196
|
+
// «Развернуть» — шеврон вверх, зеркало «свернуть»: пара читается как одна ось
|
|
197
|
+
this.#setButton(this.#maximize, I18n.t("appLayout.window.maximize"), "maximize");
|
|
198
|
+
this.#close.addEventListener("click", () => this.#windowAction("close"))
|
|
199
|
+
this.#minimize.addEventListener("click", () => this.#windowAction("minimize"))
|
|
200
|
+
// Разворот и восстановление — одно действие: решение принимает окно, а не кнопка
|
|
201
|
+
this.#maximize.addEventListener("click", () => this.#windowAction("toggle-maximize"))
|
|
197
202
|
// Разворот приходит не только от нашей кнопки (двойной клик по полосе, меню окна),
|
|
198
203
|
// поэтому состояние читаем по факту изменения размера окна
|
|
199
204
|
window.addEventListener("resize", this.#sync_maximized_event)
|
|
200
205
|
this.#syncMaximized()
|
|
201
206
|
ipcRenderer.on("chui_resizable_false", this.#resizable_false_event)
|
|
202
207
|
}
|
|
203
|
-
/** Кнопка окна —
|
|
204
|
-
*
|
|
205
|
-
* иконок шапки, поэтому опция размера здесь не передаётся */
|
|
208
|
+
/** Кнопка окна — значок с именем для скринридера, фокусом и активацией с клавиатуры.
|
|
209
|
+
* Значок рисуется линиями (`WINDOW_ICONS`), размер задаёт CSS (см. global_style.css) */
|
|
206
210
|
#setButton(button, label, icon) {
|
|
207
|
-
button.innerHTML =
|
|
211
|
+
button.innerHTML = WINDOW_ICONS[icon];
|
|
208
212
|
button.setAttribute("title", label);
|
|
209
213
|
button.setAttribute("aria-label", label);
|
|
210
214
|
makeKeyboardButton(button);
|
|
211
215
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Действия кнопок окна идут по IPC в main (`WINDOW_ACTION`/`WINDOW_STATE`). Прежде здесь
|
|
218
|
+
* стоял `@electron/remote`, но он устарел и тянул модули main-процесса прямо в страницу.
|
|
219
|
+
* Обработчики берут окно отправителя, поэтому канал не может задеть чужое окно; ошибка
|
|
220
|
+
* глушится — без `Main` (обработчики не зарегистрированы) нажатие не должно ломать страницу.
|
|
221
|
+
*/
|
|
222
|
+
#windowAction(action = String()) {
|
|
223
|
+
try {
|
|
224
|
+
return Promise.resolve(ipcRenderer.invoke(BRIDGE_CHANNELS.WINDOW_ACTION, action)).catch(() => false);
|
|
225
|
+
} catch (_error) {
|
|
226
|
+
return Promise.resolve(false);
|
|
219
227
|
}
|
|
220
|
-
return this.#current_window === null ? undefined : this.#current_window;
|
|
221
228
|
}
|
|
222
|
-
|
|
223
|
-
|
|
229
|
+
|
|
230
|
+
/** Состояние окна: `{isMaximized, isMinimized, …}`; без окна — пустой объект */
|
|
231
|
+
async #windowState() {
|
|
232
|
+
try {
|
|
233
|
+
return (await ipcRenderer.invoke(BRIDGE_CHANNELS.WINDOW_STATE)) ?? {};
|
|
234
|
+
} catch (_error) {
|
|
235
|
+
return {};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Развёрнутое окно — другой значок и другая подпись: кнопка возвращает окно в оконный режим.
|
|
239
|
+
// «Восстановить» — контурный ромб; размер значкам задаёт CSS.
|
|
240
|
+
// Состояние приходит асинхронно, поэтому ответ устаревшего запроса отбрасывается по номеру:
|
|
241
|
+
// иначе быстрая пара «развернуть/восстановить» могла бы применить ответ в обратном порядке
|
|
224
242
|
#syncMaximized() {
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
243
|
+
const request = ++this.#sync_request;
|
|
244
|
+
this.#windowState().then((state) => {
|
|
245
|
+
if (request !== this.#sync_request) return;
|
|
246
|
+
const maximized = state.isMaximized === true;
|
|
247
|
+
const label = maximized ? I18n.t("appLayout.window.restore") : I18n.t("appLayout.window.maximize");
|
|
248
|
+
this.#maximize.classList.toggle("wc_maximized", maximized);
|
|
249
|
+
this.#maximize.innerHTML = WINDOW_ICONS[maximized ? "restore" : "maximize"];
|
|
250
|
+
this.#maximize.setAttribute("title", label);
|
|
251
|
+
this.#maximize.setAttribute("aria-label", label);
|
|
252
|
+
});
|
|
233
253
|
}
|
|
234
254
|
#sync_maximized_event = () => this.#syncMaximized();
|
|
235
255
|
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
|
/* Прокрутка содержимого: материал плотнее и появляется тень — полоса читается
|
|
@@ -1253,25 +1255,21 @@ wc_close, wc_maximize, wc_minimize {
|
|
|
1253
1255
|
transition: background-color var(--transition_time) ease, color var(--transition_time) ease;
|
|
1254
1256
|
}
|
|
1255
1257
|
|
|
1256
|
-
/*
|
|
1257
|
-
кнопки
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1258
|
+
/* Значки кнопок окна нарисованы линиями (SVG), а не взяты из шрифта иконок: три системные
|
|
1259
|
+
кнопки должны выглядеть одинаково и тогда, когда Material Symbols недоступен (веб-сборка,
|
|
1260
|
+
безопасное окно без шрифта фреймворка). Все четыре значка вписаны в один квадрат 20×20 и
|
|
1261
|
+
центрированы геометрически, поэтому при общем размере 16 пикселей стоят ровно и читаются
|
|
1262
|
+
одной тройкой. Цвет приходит от кнопки через `currentColor`, поэтому состояния ниже красят
|
|
1263
|
+
сам `svg`. */
|
|
1264
|
+
wc_close svg,
|
|
1265
|
+
wc_maximize svg,
|
|
1266
|
+
wc_minimize svg {
|
|
1267
|
+
display: block;
|
|
1268
|
+
width: 16px;
|
|
1269
|
+
height: 16px;
|
|
1265
1270
|
color: var(--text_color_disabled);
|
|
1266
1271
|
}
|
|
1267
1272
|
|
|
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
1273
|
wc_maximize:hover, wc_minimize:hover {
|
|
1276
1274
|
background: var(--element_background_hover);
|
|
1277
1275
|
}
|
|
@@ -1280,13 +1278,13 @@ wc_maximize:active, wc_minimize:active {
|
|
|
1280
1278
|
background: var(--element_background_prime); color: var(--text_color_hover);
|
|
1281
1279
|
}
|
|
1282
1280
|
|
|
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
|
|
1281
|
+
wc_maximize:hover svg,
|
|
1282
|
+
wc_maximize:active svg,
|
|
1283
|
+
wc_minimize:hover svg,
|
|
1284
|
+
wc_minimize:active svg,
|
|
1285
|
+
wc_close:focus-visible svg,
|
|
1286
|
+
wc_maximize:focus-visible svg,
|
|
1287
|
+
wc_minimize:focus-visible svg {
|
|
1290
1288
|
color: var(--text_color);
|
|
1291
1289
|
}
|
|
1292
1290
|
|
|
@@ -1297,8 +1295,8 @@ wc_close:active {
|
|
|
1297
1295
|
background: var(--red_prime_background);
|
|
1298
1296
|
}
|
|
1299
1297
|
|
|
1300
|
-
wc_close:hover
|
|
1301
|
-
wc_close:active
|
|
1298
|
+
wc_close:hover svg,
|
|
1299
|
+
wc_close:active svg {
|
|
1302
1300
|
color: var(--text_color);
|
|
1303
1301
|
}
|
|
1304
1302
|
|
|
@@ -5,6 +5,7 @@ const {Dialog} = require('../chui_modal/modal');
|
|
|
5
5
|
const {ContentBlock} = require('../chui_content_block/content_block');
|
|
6
6
|
const {Button} = require('../chui_button/button');
|
|
7
7
|
const {I18n} = require('../../modules/chui_i18n/chui_i18n');
|
|
8
|
+
const {Desktop} = require('../../modules/chui_services/desktop');
|
|
8
9
|
const {CheckBox} = require('../chui_inputs/chui_check_box/check_box');
|
|
9
10
|
const {Select} = require('../chui_inputs/chui_select_box/select_box');
|
|
10
11
|
|
|
@@ -1939,7 +1940,10 @@ class DataGrid {
|
|
|
1939
1940
|
return lines.join("\r\n");
|
|
1940
1941
|
}
|
|
1941
1942
|
|
|
1942
|
-
/**
|
|
1943
|
+
/**
|
|
1944
|
+
* Текст в буфер обмена: сначала буфер окна, иначе буфер приложения через `Desktop`
|
|
1945
|
+
* (он работает и в безопасном окне, где прямого доступа к Electron нет), иначе возврат
|
|
1946
|
+
*/
|
|
1943
1947
|
async exportToClipboard(options = {}) {
|
|
1944
1948
|
const text = this.exportCSV(options);
|
|
1945
1949
|
try {
|
|
@@ -1948,10 +1952,10 @@ class DataGrid {
|
|
|
1948
1952
|
return text;
|
|
1949
1953
|
}
|
|
1950
1954
|
} catch {
|
|
1951
|
-
// Буфера в окне нет: пробуем буфер
|
|
1955
|
+
// Буфера в окне нет: пробуем буфер приложения ниже
|
|
1952
1956
|
}
|
|
1953
1957
|
try {
|
|
1954
|
-
|
|
1958
|
+
await Desktop.clipboard.writeText(text);
|
|
1955
1959
|
} catch {
|
|
1956
1960
|
// Ни одного буфера нет — текст остаётся у вызывающего кода
|
|
1957
1961
|
}
|
|
@@ -44,6 +44,9 @@ class UpdateNotification {
|
|
|
44
44
|
// а текст не отъезжает вбок под вращающимся кольцом
|
|
45
45
|
this.#update_notification_icon = createStatusPlate(undefined, Icons.NOTIFICATION.SYSTEM_UPDATE);
|
|
46
46
|
if (options.spinner) {
|
|
47
|
+
// Под кольцом заливки нет: плашка держит только место значка, иначе серая подложка
|
|
48
|
+
// читалась вторым фоном вокруг тонкого индикатора
|
|
49
|
+
this.#update_notification_icon.classList.add("notification_icon_plain");
|
|
47
50
|
this.#update_notification_icon.innerHTML = "";
|
|
48
51
|
this.#update_notification_icon.appendChild(new Spinner(Spinner.SIZE.V_SMALL, "0").set());
|
|
49
52
|
}
|
|
@@ -35,3 +35,10 @@ notification_icon.notification_icon_error {
|
|
|
35
35
|
background: var(--badge_error_back);
|
|
36
36
|
color: var(--badge_error_text);
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
/* Плашка без заливки — только место значка. Так стоит вращающийся индикатор карточки
|
|
40
|
+
обновления: кольцо тонкое, и серая подложка читалась вторым фоном вокруг него.
|
|
41
|
+
Размер и выравнивание те же, поэтому текст заголовка не съезжает. */
|
|
42
|
+
notification_icon.notification_icon_plain {
|
|
43
|
+
background: transparent;
|
|
44
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
const {Animation} = require('../../modules/chui_animations/animations');
|
|
2
2
|
const {I18n} = require('../../modules/chui_i18n/chui_i18n');
|
|
3
3
|
|
|
4
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
5
|
+
/** Доля кольца, которую занимает дуга: четверть — самый читаемый «бегунок» */
|
|
6
|
+
const ARC_SHARE = 0.25;
|
|
7
|
+
|
|
4
8
|
/**
|
|
5
|
-
* Индикатор
|
|
9
|
+
* Индикатор загрузки — одно тонкое кольцо: приглушённая дорожка и гладкая дуга со
|
|
10
|
+
* скруглёнными концами, равномерно вращающаяся. Раньше это были три вложенных кольца
|
|
11
|
+
* с коническими градиентами: на малых размерах они сливались и читались шумно.
|
|
6
12
|
*
|
|
7
13
|
* Доступность: блок — live-регион `role="status"` с именем из опции `label` (значения у индикатора
|
|
8
14
|
* нет, поэтому сообщать можно только о факте загрузки; пустая строка имя убирает).
|
|
@@ -11,8 +17,6 @@ const {I18n} = require('../../modules/chui_i18n/chui_i18n');
|
|
|
11
17
|
class Spinner {
|
|
12
18
|
#spinner_main = document.createElement('chui_spiner')
|
|
13
19
|
#spinner = document.createElement('spinner')
|
|
14
|
-
#spinner_2 = document.createElement('spinner')
|
|
15
|
-
#spinner_3 = document.createElement('spinner')
|
|
16
20
|
#label = I18n.t("feedback.loading");
|
|
17
21
|
#removed = false;
|
|
18
22
|
constructor(size = Spinner.SIZE.DEFAULT, margin = String(), label = I18n.t("feedback.loading")) {
|
|
@@ -22,25 +26,38 @@ class Spinner {
|
|
|
22
26
|
this.#spinner_main.setAttribute("aria-live", "polite");
|
|
23
27
|
this.setLabel(label);
|
|
24
28
|
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
this.#spinner.style.
|
|
28
|
-
this.#spinner.style.
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
const diameter = size.SIZE;
|
|
30
|
+
const border = size.BORDER;
|
|
31
|
+
this.#spinner.style.width = `${diameter}px`;
|
|
32
|
+
this.#spinner.style.height = `${diameter}px`;
|
|
33
|
+
// Толщину кольца читает CSS: `stroke-width` рисует и дорожку, и дугу
|
|
34
|
+
this.#spinner.style.setProperty("--spinner_border", `${border}px`);
|
|
35
|
+
// Геометрия идёт по радиусу до середины штриха, минус полпикселя запаса: сглаженная
|
|
36
|
+
// внешняя кромка кольца не должна срезаться рамкой `svg`
|
|
37
|
+
const radius = diameter / 2 - border / 2 - 0.5;
|
|
38
|
+
const circumference = 2 * Math.PI * radius;
|
|
39
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
40
|
+
svg.setAttribute("viewBox", `0 0 ${diameter} ${diameter}`);
|
|
41
|
+
svg.setAttribute("aria-hidden", "true");
|
|
42
|
+
svg.setAttribute("focusable", "false");
|
|
43
|
+
// Дорожка — целое приглушённое кольцо: дуга читается на любом фоне
|
|
44
|
+
svg.appendChild(this.#circle(radius, diameter, "spinner_track"));
|
|
45
|
+
// Дуга — четверть кольца; скруглённые концы и цвет задаёт CSS
|
|
46
|
+
const arc = this.#circle(radius, diameter, "spinner_arc");
|
|
47
|
+
arc.setAttribute("stroke-dasharray", `${circumference * ARC_SHARE} ${circumference}`);
|
|
48
|
+
svg.appendChild(arc);
|
|
49
|
+
this.#spinner.appendChild(svg);
|
|
42
50
|
this.#spinner_main.appendChild(this.#spinner);
|
|
43
51
|
}
|
|
52
|
+
/** Окружность по центру квадрата: общая разметка дорожки и дуги */
|
|
53
|
+
#circle(radius, diameter, className) {
|
|
54
|
+
const circle = document.createElementNS(SVG_NS, "circle");
|
|
55
|
+
circle.setAttribute("class", className);
|
|
56
|
+
circle.setAttribute("cx", `${diameter / 2}`);
|
|
57
|
+
circle.setAttribute("cy", `${diameter / 2}`);
|
|
58
|
+
circle.setAttribute("r", `${radius}`);
|
|
59
|
+
return circle;
|
|
60
|
+
}
|
|
44
61
|
/** Имя для скринридера; пустая строка убирает объявление */
|
|
45
62
|
getLabel() {
|
|
46
63
|
return this.#label;
|