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.
@@ -2,10 +2,12 @@ chui_spiner {
2
2
  width: max-content;
3
3
  height: max-content;
4
4
  display: flex;
5
+ /* Цвет дуги: по умолчанию акцент, меняется через `style.color` на самом индикаторе */
6
+ color: var(--blue_prime_background);
5
7
  /* Вращается коробка, и её повёрнутый AABB больше стороны квадрата (90 → до 127 px):
6
8
  без обрезки этот «инк» уходил в прокрутку предков и раздувал scrollWidth карточки.
7
- Краска кольца остаётся внутри окружности, вписанной в коробку, поэтому обрезка
8
- ничего не теряет; 1px запаса сохраняет сглаженные касательные точки окружности. */
9
+ Кольцо остаётся внутри вписанной окружности, поэтому обрезка ничего не теряет;
10
+ 1px запаса сохраняет сглаженную внешнюю кромку кольца. */
9
11
  overflow: clip;
10
12
  overflow-clip-margin: 1px;
11
13
  }
@@ -13,38 +15,35 @@ chui_spiner {
13
15
  spinner {
14
16
  /* Толщину кольца задаёт компонент (в JS — по размеру индикатора) */
15
17
  --spinner_border: 3px;
16
- position: relative;
17
- /* Кольцо — окружность: без border-box верхняя граница прибавлялась к высоте
18
- (например 60x64) и скругление 50% рисовало овал */
19
- box-sizing: border-box;
20
- border-radius: 50%;
21
- animation: spinner 2s linear infinite;
22
- display: flex;
23
- justify-content: center;
24
- align-items: center;
18
+ display: block;
19
+ animation: spinner_rotate 1s linear infinite;
20
+ }
21
+
22
+ spinner svg {
23
+ display: block;
24
+ width: 100%;
25
+ height: 100%;
25
26
  }
26
27
 
27
- /* Дуга — конический градиент, обрезанный маской до полосы кольца: у рамки одного
28
- сегмента (`border-top`) концы дуги сходятся клином, и на кольце 3–5px они читались
29
- рваными. Маска стоит на псевдоэлементе: на самом `spinner` она срезала бы
30
- вложенные кольца индикатора. */
31
- spinner::before {
32
- content: "";
33
- position: absolute;
34
- inset: 0;
35
- border-radius: 50%;
36
- background: conic-gradient(var(--blue_prime_background) 0deg 90deg, transparent 90deg 360deg);
37
- -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--spinner_border)), #000 calc(100% - var(--spinner_border)));
38
- mask: radial-gradient(farthest-side, transparent calc(100% - var(--spinner_border)), #000 calc(100% - var(--spinner_border)));
28
+ /* Дорожка — целое приглушённое кольцо под дугой: дуга читается и на светлом, и на тёмном
29
+ фоне, а в покое кольцо остаётся кругом, без «обрубка» */
30
+ spinner .spinner_track {
31
+ fill: none;
32
+ stroke: var(--text_color);
33
+ stroke-opacity: 0.12;
34
+ stroke-width: var(--spinner_border);
39
35
  }
40
36
 
41
- /* Крутится только внешнее кольцо: у вложенных поворот складывался с родительским
42
- (1×/2×/3×), и дуги расходились по кругу — три рваных штриха вместо индикатора */
43
- spinner spinner {
44
- animation: none;
37
+ /* Дуга — часть кольца со скруглёнными концами: длину полосы задаёт компонент
38
+ (`stroke-dasharray`), толщину — переменная, цвет — `currentColor` */
39
+ spinner .spinner_arc {
40
+ fill: none;
41
+ stroke: currentColor;
42
+ stroke-width: var(--spinner_border);
43
+ stroke-linecap: round;
45
44
  }
46
45
 
47
- @keyframes spinner {
46
+ @keyframes spinner_rotate {
48
47
  from {
49
48
  transform: rotate(0deg);
50
49
  }
@@ -53,3 +52,10 @@ spinner spinner {
53
52
  }
54
53
  }
55
54
 
55
+ /* Кому вращение мешает — оно остаётся, но медленнее: индикатор не «замирает» */
56
+ @media (prefers-reduced-motion: reduce) {
57
+ spinner {
58
+ animation-duration: 2.5s;
59
+ }
60
+ }
61
+
@@ -0,0 +1,230 @@
1
+ const {setStyles, optionOr, scrollParent} = require('../../modules/chui_functions');
2
+
3
+ /**
4
+ * Прилипающая полоса: остаётся у верха области прокрутки, пока её содержимое не кончилось.
5
+ *
6
+ * Полоса задаётся `add()`:
7
+ *
8
+ * ```javascript
9
+ * const bar = new StickyBar({
10
+ * onChange: (stuck) => Log.info('закреплена: ' + stuck)
11
+ * });
12
+ * bar.add(new Button({title: 'Раздел «А'}));
13
+ * content.appendChild(bar.set());
14
+ * ```
15
+ *
16
+ * Закрепление делает CSS (`position: sticky` у корня), поэтому полоса сохраняет место в потоке
17
+ * и не требует подпорки-заглушки. Компонент добавляет к этому состояние: пока полоса в своей
18
+ * позиции, у корня `data-stuck="false"`, после — `data-stuck="true"` и класс `sticky_bar_stuck`,
19
+ * о чём сообщает `onChange(stuck)`. Вид «в покое» и «закреплено» задаёт CSS — обычно материал и
20
+ * тень появляются только у закреплённой полосы, чтобы она не спорила с содержимым.
21
+ *
22
+ * **Отступ.** `offset` — на сколько ниже порога встаёт полоса. Число — пиксели, строка — любое
23
+ * CSS-значение (`'12px'`, `'1rem'`). Порог отсчитывается от **края содержимого** области прокрутки:
24
+ * её собственный `padding` браузер учитывает сам (в Chromium `sticky` стоит по content box),
25
+ * поэтому у страницы приложения (у `center` есть отступ под шапку) полосе хватает `offset: 0` —
26
+ * она встаёт ровно под шапкой. Отдельный отступ нужен, когда у контейнера прокрутки нет
27
+ * внутреннего отступа, а сверху что-то наложено.
28
+ *
29
+ * **Состояние считается замером, а не наблюдателем.** `IntersectionObserver` здесь не годится:
30
+ * липкий узел нельзя наблюдать относительно самого себя, а сторож, движущийся вместе с полосой,
31
+ * пересекает границу ровно в момент закрепления и даёт неустойчивый сигнал на дробных пикселях.
32
+ * Поэтому раз в кадр сравниваются верх полосы и верх области прокрутки; слушатель `scroll` —
33
+ * пассивный, работа сведена к одному замеру на кадр через `requestAnimationFrame`.
34
+ *
35
+ * Доступность: компонент ничего не объявляет и не перехватывает — содержимое остаётся обычным
36
+ * содержимым. `destroy()` снимает слушатели; повторный вызов безвреден.
37
+ */
38
+ class StickyBar {
39
+ static #counter = 0;
40
+
41
+ #root = document.createElement("chui_sticky_bar");
42
+ #content = document.createElement("sticky_bar_content");
43
+ #id = undefined;
44
+ #offset = "0px";
45
+ #target = undefined;
46
+ #stuck = false;
47
+ #on_change = undefined;
48
+ #container = undefined;
49
+ #destroyed = false;
50
+ #frame = 0;
51
+ #pending = 0;
52
+
53
+ constructor(options = {
54
+ id: String(), offset: Number(), target: undefined, onChange: undefined
55
+ }) {
56
+ setStyles(__dirname + "/styles.css", "chUiJS_StickyBar");
57
+ StickyBar.#counter += 1;
58
+ this.#id = optionOr(options.id, `chui_sticky_bar_${StickyBar.#counter}`);
59
+ this.#root.id = this.#id;
60
+ // Начальное состояние видно в разметке: до порога полоса в своей позиции
61
+ this.#root.dataset.stuck = "false";
62
+ this.#root.appendChild(this.#content);
63
+ this.#target = options.target;
64
+ this.#on_change = options.onChange;
65
+ this.setOffset(options.offset === undefined ? 0 : options.offset);
66
+ this.#observe();
67
+ }
68
+
69
+ // GET
70
+ getId() {
71
+ return this.#id;
72
+ }
73
+
74
+ /** Отступ закрепления: как задан (число пикселей или CSS-значение) */
75
+ getOffset() {
76
+ return this.#offset;
77
+ }
78
+
79
+ /** Закреплена ли полоса сейчас */
80
+ isStuck() {
81
+ return this.#stuck;
82
+ }
83
+
84
+ /** Контейнер прокрутки: свой из `target`, иначе ближайший прокручиваемый предок */
85
+ getTarget() {
86
+ const target = this.#target;
87
+ if (target !== undefined && target !== null) {
88
+ if (typeof target === "string") return document.querySelector(target) ?? document.scrollingElement;
89
+ if (target instanceof Element) return target;
90
+ if (typeof target.set === "function") return target.set();
91
+ }
92
+ return scrollParent(this.#root);
93
+ }
94
+
95
+ /** Содержимое полосы (в него пишет `add()`) */
96
+ getContent() {
97
+ return this.#content;
98
+ }
99
+
100
+ // SET
101
+ setId(id = String()) {
102
+ this.#id = id;
103
+ this.#root.id = id;
104
+ return this;
105
+ }
106
+
107
+ /** Отступ закрепления: число пикселей или строка CSS (`"12px"`, `"var(--header_height)"`) */
108
+ setOffset(offset = Number()) {
109
+ this.#offset = typeof offset === "string" ? offset : `${Number(offset) || 0}px`;
110
+ this.#root.style.setProperty("--sticky_offset", this.#offset);
111
+ this.update();
112
+ return this;
113
+ }
114
+
115
+ setOnChange(listener = undefined) {
116
+ this.#on_change = listener;
117
+ return this;
118
+ }
119
+
120
+ /** Содержимое полосы: компоненты фреймворка или готовые узлы (содержимое заменяется) */
121
+ add(...components) {
122
+ this.#content.innerHTML = "";
123
+ for (const component of components) {
124
+ if (component === undefined || component === null) continue;
125
+ this.#content.appendChild(component instanceof Element ? component : component.set());
126
+ }
127
+ this.update();
128
+ return this;
129
+ }
130
+
131
+ clear() {
132
+ this.#content.innerHTML = "";
133
+ return this;
134
+ }
135
+
136
+ /** Пересобрать слушатели и замер — после смены содержимого, контейнера или раскладки */
137
+ update() {
138
+ this.#detach();
139
+ this.#observe();
140
+ return this;
141
+ }
142
+
143
+ /** Снять слушатели; повторный вызов безвреден */
144
+ destroy() {
145
+ this.#destroyed = true;
146
+ this.#detach();
147
+ return this;
148
+ }
149
+
150
+ // RENDER
151
+ set() {
152
+ return this.#root;
153
+ }
154
+
155
+ // === ВНУТРЕННЕЕ ===
156
+ /**
157
+ * Первое подключение — на следующем кадре: узел обычно вставляют в документ сразу после
158
+ * `set()`, и только тогда виден настоящий контейнер прокрутки. Повторные вызовы ничего не
159
+ * делают, пока подключение не снято.
160
+ */
161
+ #observe() {
162
+ if (this.#destroyed === true) return;
163
+ if (this.#container !== undefined || this.#pending !== 0) return;
164
+ this.#pending = requestAnimationFrame(() => {
165
+ this.#pending = 0;
166
+ this.#attach();
167
+ });
168
+ }
169
+
170
+ #attach() {
171
+ if (this.#destroyed === true) return;
172
+ this.#container = this.getTarget();
173
+ if (this.#container instanceof Element) {
174
+ this.#container.addEventListener("scroll", this.#onScroll, { passive: true });
175
+ }
176
+ window.addEventListener("resize", this.#onScroll);
177
+ this.#schedule();
178
+ }
179
+
180
+ #detach() {
181
+ if (this.#container instanceof Element) {
182
+ this.#container.removeEventListener("scroll", this.#onScroll);
183
+ }
184
+ window.removeEventListener("resize", this.#onScroll);
185
+ this.#container = undefined;
186
+ if (this.#pending !== 0) {
187
+ cancelAnimationFrame(this.#pending);
188
+ this.#pending = 0;
189
+ }
190
+ if (this.#frame !== 0) {
191
+ cancelAnimationFrame(this.#frame);
192
+ this.#frame = 0;
193
+ }
194
+ }
195
+
196
+ #onScroll = () => this.#schedule();
197
+
198
+ #schedule() {
199
+ if (this.#frame !== 0 || this.#destroyed === true) return;
200
+ this.#frame = requestAnimationFrame(() => {
201
+ this.#frame = 0;
202
+ this.#measure();
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Закреплена ли полоса: её верх доехал до порога (край содержимого области прокрутки плюс
208
+ * отступ). Запас в 1px гасит дребезг на дробных пикселях (сглаживание при масштабе окна).
209
+ */
210
+ #measure() {
211
+ if (this.#destroyed === true) return;
212
+ const container = this.#container;
213
+ if (!(container instanceof Element) || this.#root.isConnected === false) return;
214
+ // Порог — от края содержимого контейнера: собственный отступ прокрутки браузер
215
+ // учитывает сам, а рамку — нет, поэтому она прибавляется отдельно
216
+ const container_style = window.getComputedStyle(container);
217
+ const padding = Number.parseFloat(container_style.paddingTop) || 0;
218
+ const reference_top = container.getBoundingClientRect().top + container.clientTop + padding;
219
+ const inset = Number.parseFloat(window.getComputedStyle(this.#root).top);
220
+ const offset = Number.isFinite(inset) ? inset : 0;
221
+ const stuck = this.#root.getBoundingClientRect().top <= reference_top + offset + 1;
222
+ if (stuck === this.#stuck) return;
223
+ this.#stuck = stuck;
224
+ this.#root.dataset.stuck = String(stuck);
225
+ this.#root.classList.toggle("sticky_bar_stuck", stuck);
226
+ if (typeof this.#on_change === "function") this.#on_change(stuck);
227
+ }
228
+ }
229
+
230
+ exports.StickyBar = StickyBar;
@@ -0,0 +1,37 @@
1
+ /* Прилипающая полоса: корень сам липнет к верху области прокрутки, поэтому место в потоке
2
+ сохраняется и подпорка-заглушка не нужна. Отступ задаёт компонент переменной `--sticky_offset`
3
+ (число или CSS-значение); отсчитывается он от края содержимого области прокрутки. */
4
+ chui_sticky_bar {
5
+ --sticky_offset: 0px;
6
+ display: block;
7
+ position: sticky;
8
+ inset-block-start: var(--sticky_offset);
9
+ width: -webkit-fill-available;
10
+ /* Ниже шапки (999) и выше содержимого страницы: полоса — слой над текстом, но под оболочкой */
11
+ z-index: 6;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ sticky_bar_content {
16
+ display: flex;
17
+ flex-wrap: wrap;
18
+ align-items: center;
19
+ gap: 6px;
20
+ width: -webkit-fill-available;
21
+ box-sizing: border-box;
22
+ /* Отступы есть всегда, а фон и рамка — только у закреплённой полосы: иначе при закреплении
23
+ содержимое прыгало бы. Прозрачная рамка держит ту же высоту, что и видимая */
24
+ padding: 8px 10px;
25
+ border: 1px solid transparent;
26
+ border-radius: var(--radius_md);
27
+ transition: background-color var(--transition_time) ease, box-shadow var(--transition_time) ease;
28
+ }
29
+
30
+ /* Закреплённая полоса читается отдельным слоем: материал как у шапки, волосяной край и тень.
31
+ В покое она — обычная строка содержимого, ни фона, ни тени */
32
+ chui_sticky_bar[data-stuck="true"] sticky_bar_content {
33
+ border-color: var(--border_color);
34
+ background: var(--header_background);
35
+ backdrop-filter: var(--global_blur);
36
+ box-shadow: var(--box_shadow_main);
37
+ }
@@ -127,6 +127,20 @@ contextBridge.exposeInMainWorld("chui", {
127
127
  shell: {
128
128
  openExternal: (url = String()) => Desktop.shell.openExternal(url)
129
129
  },
130
+ app: {
131
+ path: (name = String()) => Desktop.app.path(name),
132
+ info: () => Desktop.app.info()
133
+ },
134
+ dialog: {
135
+ open: (options = {}) => Desktop.dialog.open(options),
136
+ save: (options = {}) => Desktop.dialog.save(options),
137
+ message: (options = {}) => Desktop.dialog.message(options)
138
+ },
139
+ session: {
140
+ getCacheSize: () => Desktop.session.getCacheSize(),
141
+ clearCache: () => Desktop.session.clearCache(),
142
+ clearStorageData: (options = {}) => Desktop.session.clearStorageData(options)
143
+ },
130
144
  print: {
131
145
  page: (options = {}) => Desktop.print.page(options),
132
146
  html: (html = String(), options = {}) => Desktop.print.html(html, options),
@@ -4,11 +4,14 @@ function sleep(ms) {
4
4
  return new Promise(resolve => setTimeout(resolve, ms));
5
5
  }
6
6
 
7
+ /** Кэш прочитанных таблиц стилей: один файл отдаётся разным `id` без повторного чтения с диска */
8
+ const styles_cache = new Map();
9
+
7
10
  function setStyles(pathToCSS = String(), component = String()) {
8
11
  if (document.getElementById(component) != null) return;
9
- let data = fs.readFileSync(pathToCSS, 'utf8');
12
+ if (styles_cache.has(pathToCSS) === false) styles_cache.set(pathToCSS, fs.readFileSync(pathToCSS, 'utf8'));
10
13
  let style = document.createElement('style');
11
- style.innerHTML = String(data);
14
+ style.innerHTML = styles_cache.get(pathToCSS);
12
15
  style.type = 'text/css';
13
16
  style.setAttribute('id', component);
14
17
  document.head.appendChild(style);
@@ -101,14 +104,27 @@ function shouldOpenDropdownUp(dropdown = new HTMLElement(), anchor = new HTMLEle
101
104
  return dropdownPlacement(dropdown, anchor).up;
102
105
  }
103
106
 
107
+ /**
108
+ * Разбор markdown в HTML — `markdown-it`.
109
+ *
110
+ * Рендер идёт с `html: false`: сырой HTML из разметки не пропускается, а показывается текстом,
111
+ * поэтому `<script>` или обработчик события в `markdownText` не исполняются. Таблицы и
112
+ * ~~зачёркивание~~ включены по умолчанию, авто-ссылок нет — адрес становится ссылкой только
113
+ * явным `[текст](адрес)`. Разборщик создаётся при первом вызове: markdown нужен не каждой
114
+ * странице, а `require` тянет библиотеку целиком.
115
+ */
116
+ let markdown_parser = undefined;
104
117
  function markdownToHtml(text = String()) {
105
- let showdown = require('showdown');
106
- return new showdown.Converter().makeHtml(text);
118
+ if (text === undefined || text === null || String(text) === "") return "";
119
+ if (markdown_parser === undefined) markdown_parser = new (require('markdown-it'))({html: false, linkify: false, breaks: false});
120
+ return markdown_parser.render(String(text)).trim();
107
121
  }
108
122
 
123
+ /** Обратный разбор: HTML → markdown (`turndown`) */
109
124
  function htmlToMarkdown(text = String()) {
110
- let showdown = require('showdown');
111
- return new showdown.Converter().makeMarkdown(text);
125
+ if (text === undefined || text === null || String(text) === "") return "";
126
+ const TurndownService = require('turndown');
127
+ return new TurndownService().turndown(String(text));
112
128
  }
113
129
 
114
130
  function getDate() {
@@ -40,9 +40,31 @@ const SERVICES_CHANNELS = {
40
40
  /** Событие main → рендерер: язык приложения изменился */
41
41
  LOCALE_EVENT: "chui_services_locale_event",
42
42
  /** Событие main → рендерер: сработала глобальная клавиша */
43
- SHORTCUT_EVENT: "chui_shortcut"
43
+ SHORTCUT_EVENT: "chui_shortcut",
44
+ /** Путь приложения по имени из `APP_PATH_NAMES` */
45
+ APP_PATH: "chui_services_app_path",
46
+ /** Сведения о приложении: имя, версия, язык, сборка */
47
+ APP_INFO: "chui_services_app_info",
48
+ /** Системные диалоги: окно берётся у отправителя */
49
+ DIALOG_OPEN: "chui_services_dialog_open",
50
+ DIALOG_SAVE: "chui_services_dialog_save",
51
+ DIALOG_MESSAGE: "chui_services_dialog_message",
52
+ /** Сессия окна-отправителя: кеш и хранилище */
53
+ SESSION_CACHE_SIZE: "chui_services_session_cache_size",
54
+ SESSION_CLEAR_CACHE: "chui_services_session_clear_cache",
55
+ SESSION_CLEAR_STORAGE: "chui_services_session_clear_storage"
44
56
  };
45
57
 
58
+ /**
59
+ * Имена путей `app.getPath()`, которые может запросить страница. Это белый список: всё, что
60
+ * вне него, main отклоняет — странице не должен быть доступен произвольный каталог системы.
61
+ * Имена совпадают с именами Electron, кроме `logs` (в Electron это `logs`).
62
+ */
63
+ const APP_PATH_NAMES = [
64
+ "home", "appData", "userData", "sessionData", "temp", "exe", "module",
65
+ "desktop", "documents", "downloads", "music", "pictures", "videos", "logs", "recent", "crashDumps"
66
+ ];
67
+
46
68
  /**
47
69
  * Метод моста preload'а → канал. `Desktop` вызывает метод по имени: в безопасном режиме
48
70
  * его берут из `window.chui.services`, в обычном — сами зовут `ipcRenderer.invoke(канал)`.
@@ -74,7 +96,15 @@ const SERVICES_CALLS = {
74
96
  themeSet: SERVICES_CHANNELS.THEME_SET,
75
97
  themeToggle: SERVICES_CHANNELS.THEME_TOGGLE,
76
98
  localeGet: SERVICES_CHANNELS.LOCALE_GET,
77
- localeSet: SERVICES_CHANNELS.LOCALE_SET
99
+ localeSet: SERVICES_CHANNELS.LOCALE_SET,
100
+ appPath: SERVICES_CHANNELS.APP_PATH,
101
+ appInfo: SERVICES_CHANNELS.APP_INFO,
102
+ dialogOpen: SERVICES_CHANNELS.DIALOG_OPEN,
103
+ dialogSave: SERVICES_CHANNELS.DIALOG_SAVE,
104
+ dialogMessage: SERVICES_CHANNELS.DIALOG_MESSAGE,
105
+ sessionCacheSize: SERVICES_CHANNELS.SESSION_CACHE_SIZE,
106
+ sessionClearCache: SERVICES_CHANNELS.SESSION_CLEAR_CACHE,
107
+ sessionClearStorage: SERVICES_CHANNELS.SESSION_CLEAR_STORAGE
78
108
  };
79
109
 
80
110
  /** Каналы моста: окно, тема и прикладные сообщения безопасного окна */
@@ -99,4 +129,5 @@ const BRIDGE_ARGS = {
99
129
  exports.SERVICES_CHANNELS = SERVICES_CHANNELS
100
130
  exports.SERVICES_CALLS = SERVICES_CALLS
101
131
  exports.BRIDGE_CHANNELS = BRIDGE_CHANNELS
132
+ exports.APP_PATH_NAMES = APP_PATH_NAMES
102
133
  exports.BRIDGE_ARGS = BRIDGE_ARGS
@@ -125,6 +125,51 @@ class Desktop {
125
125
  onChange: (listener = () => {}) => Desktop.#subscribeLocale(listener)
126
126
  };
127
127
 
128
+ /**
129
+ * Приложение со стороны страницы: пути и сведения. Прямого доступа к модулям Electron
130
+ * у страницы нет — это и есть рекомендация Electron вместо `remote`.
131
+ *
132
+ * ```javascript
133
+ * await Desktop.app.path('userData'); // каталог настроек приложения
134
+ * await Desktop.app.info(); // {name, version, locale, isPackaged}
135
+ * ```
136
+ */
137
+ static app = {
138
+ /** Путь по имени из белого списка (`"userData"`, `"downloads"`, …) */
139
+ path: (name = String()) => Desktop.#call("appPath", String(name)),
140
+ /** @returns {Promise<{name: string, version: string, locale: string, systemLocale: string, isPackaged: boolean}>} */
141
+ info: () => Desktop.#call("appInfo")
142
+ };
143
+
144
+ /**
145
+ * Системные диалоги — в том окне, которое их вызвало: родитель и модальность main
146
+ * берёт у отправителя. Результат открытия — `{canceled, filePaths}`, как у `dialog.*`.
147
+ *
148
+ * ```javascript
149
+ * const {canceled, filePaths} = await Desktop.dialog.open({properties: ["openFile"]});
150
+ * ```
151
+ */
152
+ static dialog = {
153
+ open: (options = Object()) => Desktop.#call("dialogOpen", options),
154
+ save: (options = Object()) => Desktop.#call("dialogSave", options),
155
+ message: (options = Object()) => Desktop.#call("dialogMessage", options)
156
+ };
157
+
158
+ /**
159
+ * Сессия окна-отправителя: кеш и хранилище. Именно своя сессия, а не чужого окна.
160
+ *
161
+ * ```javascript
162
+ * await Desktop.session.clearCache();
163
+ * await Desktop.session.clearStorageData({storages: ["cookies"]});
164
+ * ```
165
+ */
166
+ static session = {
167
+ /** @returns {Promise<number>} размер кеша в байтах */
168
+ getCacheSize: () => Desktop.#call("sessionCacheSize"),
169
+ clearCache: () => Desktop.#call("sessionClearCache"),
170
+ clearStorageData: (options = Object()) => Desktop.#call("sessionClearStorage", options)
171
+ };
172
+
128
173
  // accelerator → набор обработчиков этого окна
129
174
  static #shortcut_handlers = new Map();
130
175
  static #shortcut_event_bound = false;