Repository navigation
Expand file tree
/
Copy pathconstants.js
More file actions
316 lines (296 loc) · 23.6 KB
/
Copy pathconstants.js
File metadata and controls
316 lines (296 loc) · 23.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
// Constants для Timer Widget
// Централизованное хранилище всех констант и magic numbers
const CONFIG = {
// ============================================
// TIMER INTERVALS
// ============================================
TIMER_TICK_MARGIN_MS: 25, // Тик — через столько после границы секунды: таймер Windows грубее 15,6 мс
// ============================================
// UI DELAYS & DEBOUNCE
// ============================================
RESIZE_DEBOUNCE: 300, // Debounce для resize events
FLASH_INTERVAL: 250, // Интервал мигания при завершении таймера
// ============================================
// WIDGET DIMENSIONS
// ============================================
// Timer Widget. Окно КВАДРАТНОЕ: WIDGET_BASE_SIZE в рендерере считает и
// ширину, и высоту от одного числа, поэтому высота 280 жила ровно до
// первого масштабирования, а потом навсегда становилась 250 — и стартовый
// вид больше не воспроизводился за всю сессию.
WIDGET_DEFAULT_WIDTH: 250,
WIDGET_DEFAULT_HEIGHT: 250,
// Пол окна — это его размер при МИНИМАЛЬНОМ масштабе, а не отдельное число:
// 30 % (WindowGeometry.MIN_SCALE_PCT) от 250 — это 75. Пока здесь стояли
// литералы 120/140, обещанные 30 % были недостижимы: замер 01.09.2026 —
// ползунок панели на 30 %, окно 120 px, то есть 48 %. Равенство закреплено
// tests/scale-range.test.js: разъедься оно снова — упрётся уже тест, а не
// пользователь.
//
// Оба минимума РАВНЫ, и это несущее: высоту виджет выводит из ширины
// (windowHeightFor → width), а минимум применяется по КАЖДОЙ оси
// независимо (fitScaledBounds/sideSize). Разные минимумы поэтому означали
// не «пол повыше», а «ниже 56 % окно перестаёт быть квадратом» — замерено
// 120×140 при 30 %, 125×140 при 50 %, 138×140 при 55 %. Высота 140 была
// полом полосы LED; стиля LED больше нет (слит с «Цифрами» 13.08.2026),
// вместе с ним ушли и ledStripHeight, и WIDGET_LED_MIN_HEIGHT.
WIDGET_MIN_WIDTH: 75,
WIDGET_MIN_HEIGHT: 75,
// Clock Widget. CLOCK_WIDGET_MIN_SIZE объявлялся как 100, а окно всё это
// время создавалось со 120 — реестр никто не читал, поэтому расхождение
// никого не роняло и никем не замечалось. Теперь это, как и у виджета,
// размер при MIN_SCALE_PCT: 30 % от 220 — это 66. Со 120 у ползунка часов
// пять первых делений (30…50 %) давали ОДИН И ТОТ ЖЕ размер.
CLOCK_WIDGET_DEFAULT_SIZE: 220,
CLOCK_WIDGET_MIN_SIZE: 66,
// Control Window. CONTROL_WINDOW_HEIGHT объявлялась как 500, а окно
// создавалось в диапазоне 660…740; CONTROL_WINDOW_MIN_HEIGHT — как 300 при
// настоящем минимуме 660, вдвое больше. Ни ту, ни другую никто не читал.
// Здесь стоят НАСТОЯЩИЕ значения.
CONTROL_WINDOW_WIDTH: 400,
CONTROL_WINDOW_HEIGHT: 740,
CONTROL_WINDOW_MIN_WIDTH: 380,
// Пол ширины СВЁРНУТОЙ полосы. Полоса — то же окно в другом состоянии, и
// размер ей задаёт содержимое: с 20.08.2026 в ней, кроме времени и
// управления отсчётом, живут четыре ячейки вида и замок. На 400px этот
// набор не помещается. Число не выдумано: замер в e2e/mini-bar.spec.js
// печатает ширину содержимого и запас. Худший случай полосы — время вида
// «−9:59:59» и самое длинное слово действия «Продолжить» — занимает 422px
// плюс 40px зазоров, то есть 490px окна впритык. Меньше ставить нельзя: во
// flex нехватка ширины видна не вылезшим элементом, а тихо сжатой кнопкой,
// и на 470 замер поймал именно это — круглые кнопки стали 29.1px вместо 32.
//
// Это ПОЛ, а не размер: окно, которое пользователь растянул шире, при
// сворачивании свою ширину сохраняет, а разворот возвращает прежние
// границы целиком (главный процесс держит их в controlBoundsBeforeCollapse).
CONTROL_BAR_MIN_WIDTH: 510,
// Высота СВЁРНУТОЙ полосы. Её шлёт полоса (mini-bar.js) в payload
// control-collapse, и её же главный процесс берёт запасной, если payload
// высоты не принёс. Литерал в двух файлах разошёлся бы молча: полоса
// просила бы одно, main без высоты ставил бы другое. CSS повторяет число
// в `body.collapsed .mini-bar` — это меряет e2e/mini-bar.spec.js.
CONTROL_BAR_HEIGHT: 52,
// Потолок высоты СВЁРНУТОЙ полосы. Его знают две стороны: главный процесс
// обрезает им высоту из payload control-collapse, а полоса (mini-bar.js)
// по нему отличает «окно уже снова панель» от «ещё полоса», когда ждёт
// разворота для отложенной подгонки. Литерал в двух файлах разошёлся бы
// молча: полоса ждала бы resize, которого main не даст.
CONTROL_BAR_MAX_HEIGHT: 120,
// Потолок ширины окна управления. Значение нужно ДВУМ сторонам: главный
// процесс ставит его в maxWidth, а панель обязана учитывать его, когда
// считает ширину своей левой колонки при открытии ящика настроек. Пока
// константа была литералом только в главном процессе, панель у предела
// ширины просила «текущая + 336», запрос обрезался до потолка, а колонка
// оставалась во всю ширину — ящик ложился ПОВЕРХ содержимого панели.
//
// Потолок ДВУХУРОВНЕВЫЙ, и это следствие того, что у панели нет ни одного
// контрола, которому нужна ширина 1200: колонка контента — 720px, дальше
// растёт только пустое поле. Прежний единственный потолок 1200 позволял
// растянуть окно так, что панель тонула в поле шириной 640px — состояние,
// ради оправдания которого в control.css завели вторую оболочку (карточку
// с рамкой и тенью), и окно стало читаться как окно внутри окна.
//
// Переключает уровень канал `control-drawer`, а НЕ ширина в запросе
// resize-control-window: вывод потолка из запрошенной ширины означал бы,
// что любой запрос его и поднимает, то есть потолка нет.
CONTROL_WINDOW_MAX_WIDTH: 760,
// Тот же потолок с открытым ящиком: 720 колонки + поля + 336 ящика.
CONTROL_WINDOW_MAX_WIDTH_WITH_DRAWER: 1096,
CONTROL_WINDOW_MIN_HEIGHT: 660,
// Потолок высоты — по той же причине и по тому же правилу, что и ширины.
// Был литералом 1100 в createControlWindow, а обработчик
// resize-control-window о нём не знал вовсе. Содержимое панели в покое
// занимает 676px (замер суммы высот блоков), поэтому на окне 1100 пустого
// места было 424px: оно уходило в блок с цифрами, и панель разъезжалась —
// между подсказкой и пресетами зияла дыра в треть окна. 740 — стартовая
// высота окна: запас над содержимым ≤ 64px и читается как воздух.
CONTROL_WINDOW_MAX_HEIGHT: 740,
// С открытым ящиком высота полезна: настроек в нём заведомо больше, чем
// влезает, и высокое окно показывает больше без прокрутки.
CONTROL_WINDOW_MAX_HEIGHT_WITH_DRAWER: 1100,
// Сколько пикселей окна обязано остаться видимым по каждой оси при
// ВОССТАНОВЛЕНИИ сохранённой позиции. Свисать за край экрана разрешено —
// это законное расположение виджета, — но полностью потеряться нельзя:
// ухватить окно мышью больше нечем. Величина взята как ширина уверенного
// захвата курсором; для окна тоньше её требуется половина окна
// (см. fitRestoredBounds в window-geometry.js).
WINDOW_MIN_VISIBLE_PX: 64,
// ============================================
// RENDERER ROBUSTNESS (R5, 2026-09-28-psi-hardening)
// ============================================
// Упавший рендерер (`render-process-gone`) перезагружается не больше
// CRASH_RELOAD_LIMIT раз за скользящее окно CRASH_RELOAD_WINDOW_MS —
// окно, упавшее один раз в час, обязано перезагружаться ВСЕГДА, поэтому
// это не счётчик на жизнь окна, а фильтр по недавним меткам времени
// (main-window-hooks.js: bindRenderCrashHandler).
CRASH_RELOAD_LIMIT: 3,
CRASH_RELOAD_WINDOW_MS: 60000,
// Сколько живёт метка «старт нажат в панели» (control-app.js: start →
// _onTimerState): дольше IPC-круга с запасом, но конечна — отклонённая
// команда не должна глушить звук чужого старта навсегда.
LOCAL_START_SOUND_WINDOW_MS: 3000,
// Порог дельты одного шага перетаскивания (main-geometry.js: moveWindowBy).
// Испорченный или подменённый payload IPC-канала `*-move` мог унести окно
// на координату вне int32 — setBounds на таком значении либо бросает,
// либо утаскивает окно за пределы всех мониторов без возврата. Дельта
// одного движения мыши на экране кратно меньше: порог отрезает подделку,
// а не законный жест.
MAX_MOVE_DELTA: 100000,
// ============================================
// SCALING & ZOOM
// ============================================
// Шаг Ctrl+колеса, В ПРОЦЕНТАХ. Объявлялся как 20 «пикселей», а все три окна
// независимо держали свой литерал 10 процентов — ни одно реестр не читало.
SCALE_STEP: 10,
DEFAULT_SCALE: 1.0, // Масштаб по умолчанию (100%)
// ============================================
// TIMER THRESHOLDS
// ============================================
WARNING_THRESHOLD: 60, // Показать warning при < 60 секунд
WARNING_PERCENTAGE: 25, // Показать warning при < 25% времени
DANGER_PERCENTAGE: 10, // Показать danger при < 10% времени
// ============================================
// PRESET DURATIONS
// ============================================
// Длительности клавиатурных пресетов (клавиши 1-8): 5/10/15/20/25/30/45/60 минут
PRESET_DURATIONS: [300, 900, 1500, 2700],
// ============================================
// FILE LIMITS
// ============================================
MAX_SOUND_FILE_SIZE: 5 * 1024 * 1024, // Максимальный размер звукового файла (5 MB)
MAX_IMAGE_FILE_SIZE: 10 * 1024 * 1024, // Максимальный размер изображения (10 MB)
// ============================================
// DISPLAY SETTINGS
// ============================================
// Границы масштаба таймера на полноэкранном дисплее. Объявлялись как 50/200
// при настоящих 30/300 в display-script.js — реестр не читал никто.
MIN_TIMER_SCALE: 30,
MAX_TIMER_SCALE: 300,
// ============================================
// FLASH ANIMATION
// ============================================
MAX_FLASH_COUNT: 6, // Максимальное количество миганий
// ============================================
// STORAGE KEYS
// ============================================
// РЕЕСТР ключей localStorage. Рендереры обращаются к хранилищу строковыми
// литералами (модулей-обёрток нет, сборщика тоже), поэтому этот объект —
// документация, а не единственная точка доступа. Ровно поэтому он и разъехался
// с реальностью: 16 ключей здесь были фантомными (widgetStyle, timerSound,
// widgetPosition, clockSize, timerConfig и другие — ни одного обращения в коде),
// а 10 реально используемых, наоборот, отсутствовали (widgetGeometry,
// clockGeometry, displayBlockPositions, selectedDisplay и прочие).
//
// Синхронность реестра и кода теперь проверяется тестом в оба конца
// (tests/storage-keys.test.js): и лишний ключ здесь, и незарегистрированный
// ключ в коде роняют прогон.
STORAGE_KEYS: {
// Цвета (по окну). Каждое окно читает СВОЙ ключ: дисплей — только его
// (renderer-shared.storedColors), виджет — с запасным путём ownColors.
//
// TIMER_COLORS — ТОЛЬКО ДЛЯ ОТКАТА. Панель пишет сюда цвета виджета, потому
// что версия до 29.10.2026 (виджет и дисплей) красится этим ключом; читает
// его лишь запасной путь ownColors (виджет и панель) для профиля без своего ключа. Статус
// заперт тестом tests/storage-keys.test.js. Условие удаления: откат на
// версию до 29.10.2026 больше не поддерживается И профили старше
// поканальных цветов перенесены (панель тогда перестаёт его писать,
// запасной путь уходит вместе с ключом).
TIMER_COLORS: 'timerColors',
WIDGET_COLORS: 'widgetColors',
CLOCK_COLORS: 'clockColors',
DISPLAY_COLORS: 'displayColors',
// Настройки окон
WIDGET_SETTINGS: 'widgetSettings',
CLOCK_WIDGET_SETTINGS: 'clockWidgetSettings',
DISPLAY_EXT_SETTINGS: 'displayExtSettings',
// Геометрия виджетов (размер + позиция), см. restoreGeometry/saveGeometry
WIDGET_GEOMETRY: 'widgetGeometry',
CLOCK_GEOMETRY: 'clockGeometry',
// Деления на круглом циферблате (общая настройка двух окон)
CLOCK_SHOW_TICKS: 'clockShowTicks',
// Полноэкранный режим: масштабы и позиции info-блоков
DISPLAY_TIMER_SCALE: 'displayTimerScale',
// Общий масштаб карточек. Ключ ОСТАЁТСЯ: у каждого элемента теперь свой
// масштаб (DISPLAY_BLOCK_SCALES), а этот читается как запасной — профиль,
// переживший обновление, обязан открыться прежним.
DISPLAY_BLOCK_SCALE: 'displayBlockScale',
DISPLAY_BLOCK_SCALES: 'displayBlockScales',
DISPLAY_BLOCK_POSITIONS: 'displayBlockPositions',
SELECTED_DISPLAY: 'selectedDisplay',
// Фон полноэкранного режима
LOCAL_BG_IMAGE: 'localBgImage',
LOCAL_BG_SETTINGS: 'localBgSettings',
// Первый запуск: показана ли подсказка про F1 (ровно один раз за профиль)
ONBOARDING_SHOWN: 'onboardingShown',
// Звук
CUSTOM_SOUNDS: 'customSounds',
SOUND_ENABLED: 'soundEnabled',
// Тема интерфейса ('dark' | 'light'), владелец — ui-theme.js
UI_THEME: 'uiTheme',
// Замок «Закрепить положение» ('1' | '0'), владелец — ui-lock.js
UI_LOCKED: 'uiLocked',
// Четыре пресета вида (стили, блоки, раскладка, цвета), владелец — presets.js
UI_PRESETS: 'uiPresets',
// Какая ячейка вида применена последней ('1'…'4'), владелец — presets.js.
// Отдельно от самих пресетов: это не часть ВИДА, а указатель на ячейку,
// и в снимок он не попадает — иначе применение пресета переносило бы
// чужой указатель.
UI_PRESET_ACTIVE: 'uiPresetActive',
// Однократные подсказки при первом открытии окна
WIDGET_HINT_SHOWN: 'widgetHintShown',
CLOCK_HINT_SHOWN: 'clockHintShown',
DISPLAY_HINT_SHOWN: 'displayHintShown'
},
// Дефолтных цветов таймера здесь НЕТ: владелец умолчания — CSS окна.
// DEFAULT_TIMER_COLORS (#0a84ff/#30d158) удалён 03.11.2026 вместе с
// последним потребителем — стартовой моделью панели, которая отсылала эти
// цвета виджету с первой правкой подложки (docs/lessons.md, «A colour
// default has an owner too»). До него здесь же удалена пятая палитра
// DEFAULT_COLORS с оранжевым перерасходом — её не читал никто.
// ============================================
// TIMER STYLES
// ============================================
/**
* Четыре стиля окон таймера и часов. Список нужен ДВУМ сторонам: окну
* часов (неизвестное имя → круг) и главному процессу, который запоминает
* стиль в relay-state.json (relay-payload.js) — без списка там жило бы
* любое имя. Старое 'digital' переводит migrateTimerStyle, в списке его нет.
*/
TIMER_STYLES: ['circle', 'flip', 'analog', 'digits'],
// ============================================
// ALLOWED FILE TYPES
// ============================================
ALLOWED_AUDIO_TYPES: ['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/mp3', 'audio/webm', 'audio/aac', 'audio/flac'],
ALLOWED_IMAGE_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp']
};
// Freeze объект чтобы предотвратить изменения
Object.freeze(CONFIG);
Object.freeze(CONFIG.STORAGE_KEYS);
Object.freeze(CONFIG.TIMER_STYLES);
if (CONFIG.ALLOWED_AUDIO_TYPES) { Object.freeze(CONFIG.ALLOWED_AUDIO_TYPES); }
if (CONFIG.ALLOWED_IMAGE_TYPES) { Object.freeze(CONFIG.ALLOWED_IMAGE_TYPES); }
if (CONFIG.PRESET_DURATIONS) { Object.freeze(CONFIG.PRESET_DURATIONS); }
// Экспорт — ИМЕНОВАННЫЙ, по ключу на имя: главный процесс берёт файл
// `require(esm)` и получает namespace той же формы, что прежний
// `module.exports = CONFIG` (tests/module-exports-shape.test.js). Ключ CONFIG,
// не попавший в этот список, main не увидит — равенство списков держит
// tests/constants.test.js.
export const {
TIMER_TICK_MARGIN_MS, RESIZE_DEBOUNCE, FLASH_INTERVAL, WIDGET_DEFAULT_WIDTH,
WIDGET_DEFAULT_HEIGHT, WIDGET_MIN_WIDTH, WIDGET_MIN_HEIGHT,
CLOCK_WIDGET_DEFAULT_SIZE, CLOCK_WIDGET_MIN_SIZE, CONTROL_WINDOW_WIDTH,
CONTROL_WINDOW_HEIGHT, CONTROL_WINDOW_MIN_WIDTH, CONTROL_BAR_MIN_WIDTH, CONTROL_BAR_HEIGHT,
CONTROL_BAR_MAX_HEIGHT,
CONTROL_WINDOW_MAX_WIDTH, CONTROL_WINDOW_MAX_WIDTH_WITH_DRAWER,
CONTROL_WINDOW_MIN_HEIGHT, CONTROL_WINDOW_MAX_HEIGHT,
CONTROL_WINDOW_MAX_HEIGHT_WITH_DRAWER, WINDOW_MIN_VISIBLE_PX,
CRASH_RELOAD_LIMIT, CRASH_RELOAD_WINDOW_MS, LOCAL_START_SOUND_WINDOW_MS,
MAX_MOVE_DELTA, SCALE_STEP, DEFAULT_SCALE, WARNING_THRESHOLD,
WARNING_PERCENTAGE, DANGER_PERCENTAGE, PRESET_DURATIONS, MAX_SOUND_FILE_SIZE,
MAX_IMAGE_FILE_SIZE, MIN_TIMER_SCALE, MAX_TIMER_SCALE, MAX_FLASH_COUNT,
STORAGE_KEYS, TIMER_STYLES, ALLOWED_AUDIO_TYPES,
ALLOWED_IMAGE_TYPES
} = CONFIG;
// Крючок e2e и общий объект окон: тот же замороженный CONFIG.
if (typeof window !== 'undefined') {
window.CONFIG = CONFIG;
}