@goodandready/dsh-kanban 0.2.19 → 0.2.20

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/lib/client.js CHANGED
@@ -1,12 +1,11 @@
1
- // Браузерная половина dsh-kanban: экран доски и карточка настроек.
1
+ // Browser half of dsh-kanban: board screen and settings card.
2
2
  //
3
- // Идентификатор ниже обязан совпадать с `name` в package.json и с `name:` в
4
- // cordis.patch.yml. Расхождение НЕ даёт ошибки в журнале: загрузчик молча не
5
- // разрешает пакет, серверная половина работает, интерфейса нет.
3
+ // The identifier below must match `name` in package.json and `name:` in
4
+ // cordis.patch.yml. A mismatch produces NO log error: the loader silently fails
5
+ // to resolve the package, the server half works, and the UI never appears.
6
6
  //
7
- // Модуль грузится загрузчиком и импортировать файлы репозитория не может,
8
- // поэтому чистые помощники живут здесь же и выставляются наружу как `helpers`
9
- // — тесты поднимают этот файл через node:vm и проверяют их без браузера.
7
+ // This module is loaded by ModuleLoader and cannot import repository files;
8
+ // pure helpers live here and are exported as `helpers` for node:vm testing.
10
9
  window.__ModuleLoader__.load({
11
10
  id: '@goodandready/dsh-kanban',
12
11
  factory: (require) => {
@@ -25,8 +24,8 @@ window.__ModuleLoader__.load({
25
24
 
26
25
 
27
26
  /**
28
- * Соседи для переноса карточки. Хранилище работает не с числовым индексом,
29
- * а с парой соседей: `afterId` — карточка выше, `beforeId` — ниже.
27
+ * Neighbours for card repositioning. The store operates on relative neighbours
28
+ * rather than numeric indexes: `afterId` above, `beforeId` below.
30
29
  */
31
30
  function neighboursFor(columnTasks, dragId, index) {
32
31
  const rest = (columnTasks || []).filter((t) => t.id !== dragId)
@@ -42,13 +41,11 @@ window.__ModuleLoader__.load({
42
41
  }
43
42
 
44
43
  /**
45
- * Порядок задач внутри колонки.
44
+ * Task ordering within a column.
46
45
  *
47
- * `manual` — как прислал сервер: ручной порядок плюс живое вперёд.
48
- * `old`/`new` сортируют по дате заведения, но НЕ ломают ни группировку по
49
- * проектам, ни «живое вперёд»: карточки переставляются внутри своего
50
- * проекта, а идущая работа остаётся наверху группы. Иначе заголовки
51
- * проектов пришлось бы рисовать над каждой карточкой.
46
+ * `manual` — server-provided order: manual order with active tasks on top.
47
+ * `old`/`new` sort by creation date while preserving project grouping
48
+ * and "active work first".
52
49
  */
53
50
  function sortTasks(tasks, order) {
54
51
  const list = tasks || []
@@ -71,21 +68,18 @@ window.__ModuleLoader__.load({
71
68
  }
72
69
 
73
70
  /**
74
- * Карточки одной сессии: номер и общее число.
71
+ * Session task batch progress: sequence index and total count.
75
72
  *
76
- * Пять карточек, запущенных пачкой, выглядят несвязанными, и очередь
77
- * понятна только тому, кто её запускал, и только пока помнит. Порядок тот
78
- * же, в каком агент их берёт: идущая первой, дальше по времени постановки.
73
+ * Orders matching the agent execution order: active task first,
74
+ * followed by queue time.
79
75
  *
80
- * @returns {Object<string, {at: number, total: number}>} по идентификатору задачи
76
+ * @returns {Object<string, {at: number, total: number}>} keyed by task id
81
77
  */
82
78
  /**
83
- * Применить одно действие ко всем отмеченным.
79
+ * Apply a single batch action across all selected tasks.
84
80
  *
85
- * Отдельного группового маршрута не заводим: это те же действия, что для
86
- * одной задачи. Отказ части не срывает остальные — задачи независимы, — но
87
- * и не съедается молча: итог называется числом, иначе человек останется
88
- * уверен, что в архиве десять карточек, а их там семь.
81
+ * Failures on individual tasks do not abort remaining tasks;
82
+ * completed count is reported to the user.
89
83
  *
90
84
  * @returns {Promise<{done: number, failed: number}>}
91
85
  */
@@ -99,12 +93,10 @@ window.__ModuleLoader__.load({
99
93
  }
100
94
 
101
95
  /**
102
- * Чем запускали в прошлый раз, на эту доску.
96
+ * Remembered launch settings in browser localStorage.
103
97
  *
104
- * Живёт у человека в браузере, а не в настройках плагина: это привычка
105
- * одного, а не свойство доски. Хранилище может быть недоступно (закрытое
106
- * окно, запрет на данные сайта) — тогда просто нечего вспомнить, и окно
107
- * запуска обязано открыться как обычно.
98
+ * Stored per user browser rather than plugin settings. Handles storage
99
+ * errors gracefully if localStorage is disabled or restricted.
108
100
  */
109
101
  const RECALL_KEY = 'dsh-kanban:launch:'
110
102
 
@@ -123,13 +115,11 @@ window.__ModuleLoader__.load({
123
115
  }
124
116
 
125
117
  /**
126
- * Подставить запомненное, но только то, что ядро всё ещё предлагает.
118
+ * Re-apply remembered preset only if still available in active runtime.
127
119
  *
128
- * Исчезнувший из сборки уровень доступа или профиль подставлять нельзя:
129
- * задача пошла бы не с теми правами или не собралась бы вовсе. Тогда
130
- * возвращаемся к тому, что предлагает харнесс.
120
+ * Avoids applying obsolete profiles or permission ranks removed from the build.
131
121
  *
132
- * @param {Array<string>} known допустимые значения
122
+ * @param {Array<string>} known valid choices
133
123
  */
134
124
  function stillOffered(value, known, fallback) {
135
125
  return typeof value === 'string' && (known || []).indexOf(value) >= 0 ? value : fallback
@@ -153,11 +143,10 @@ window.__ModuleLoader__.load({
153
143
  }
154
144
 
155
145
  /**
156
- * Разложить колонку по ответственным.
146
+ * Group column tasks by assignee.
157
147
  *
158
- * Группа «никто» идёт первой: свободные задачи — то, ради чего в эту
159
- * раскладку и заглядывают. Дальше по алфавиту, чтобы порядок не плясал от
160
- * того, кто первым попался в списке.
148
+ * Unassigned group ('Nobody') is placed first to highlight available work,
149
+ * followed alphabetically by assignee username.
161
150
  *
162
151
  * @returns {Array<{who: string, tasks: object[]}>}
163
152
  */
@@ -180,13 +169,9 @@ window.__ModuleLoader__.load({
180
169
  }
181
170
 
182
171
  /**
183
- * Проект и номер задачи РАЗДЕЛЬНО.
172
+ * Project repository and issue number rendered separately.
184
173
  *
185
- * Слипшиеся, они читаются как один непонятный идентификатор, а это две
186
- * разные вещи: имя проекта — принадлежность, номер — адрес.
187
- *
188
- * Номер становится ссылкой на issue: чаще всего она нужна именно с
189
- * карточки, а раньше пряталась в окне задачи.
174
+ * Issue number links directly to the remote issue.
190
175
  */
191
176
  function taskRef(task, t) {
192
177
  if (!task || !task.repo) return null
@@ -218,11 +203,9 @@ window.__ModuleLoader__.load({
218
203
  }
219
204
 
220
205
  /**
221
- * Короткая строка плана для карточки: «3 из 7 · пишу тесты».
206
+ * Compact plan summary line for card badge: "3 of 7 · writing tests".
222
207
  *
223
- * План из одного пункта и план из двадцати выглядят одинаково, потому что
224
- * на карточку едут только числа и текущий пункт; сам список остаётся в
225
- * окне задачи.
208
+ * Only progress numbers and current item description are shown on cards.
226
209
  */
227
210
  function planLine(task, t) {
228
211
  const progress = task && task.plan && task.plan.progress
@@ -232,10 +215,7 @@ window.__ModuleLoader__.load({
232
215
  return progress.text ? counter + ' · ' + progress.text : counter
233
216
  }
234
217
 
235
- /**
236
- * Подпись состояния. Ожидание рисуется отдельной точкой и здесь молчит,
237
- * иначе одно и то же сказано дважды.
238
- */
218
+ /** Status badge text. Waiting indicator is rendered separately. */
239
219
  function stateLabel(task, t) {
240
220
  const state = task && task.state
241
221
  if (state === 'running') return t('state.running')
@@ -244,14 +224,13 @@ window.__ModuleLoader__.load({
244
224
  }
245
225
 
246
226
  /**
247
- * Свёрнута ли колонка.
227
+ * Determine whether column is collapsed.
248
228
  *
249
- * Пустая сжимается сама: шесть колонок по 220 пикселей не помещаются на
250
- * ноутбуке. Ручное решение человека сильнее автоматики в обе стороны —
251
- * иначе развёрнутая пустая колонка схлопывалась бы у него на глазах.
229
+ * Empty columns collapse automatically to save horizontal space;
230
+ * explicit user toggle overrides automatic behavior.
252
231
  *
253
- * @param {object} column колонка из ответа доски
254
- * @param {object} manual карта id -> явное решение человека
232
+ * @param {object} column column data from board response
233
+ * @param {object} manual map of column id -> boolean user preference
255
234
  */
256
235
  function isCollapsed(column, manual) {
257
236
  const own = manual && manual[column.id]
@@ -260,12 +239,9 @@ window.__ModuleLoader__.load({
260
239
  }
261
240
 
262
241
  /**
263
- * Относительное время одной строкой: «5 мин», «3 ч», «3 дня».
242
+ * Relative elapsed time string: "5m", "3h", "3d".
264
243
  *
265
- * Разбор на единицу и число делает сервер незачем — правило простое и без
266
- * настроек, а лишний круг данных дороже пяти строк здесь. Абсолютную дату
267
- * отдаём подсказкой при наведении: точная дата требует чтения и сравнения
268
- * с сегодняшним числом, а на доску смотрят ради скорости.
244
+ * Computed client-side with full ISO timestamp displayed on hover tooltip.
269
245
  */
270
246
  function agoText(now, then, t) {
271
247
  if (typeof then !== 'number' || then <= 0) return ''
@@ -278,7 +254,7 @@ window.__ModuleLoader__.load({
278
254
  return t('time.day', { n: Math.floor(hours / 24) })
279
255
  }
280
256
 
281
- /** Дата без времени: «26 авг». Год добавляем, только когда он не нынешний. */
257
+ /** Date formatted without time. Year is included only when not the current year. */
282
258
  function shortDate(then) {
283
259
  if (typeof then !== 'number' || then <= 0) return ''
284
260
  try {
@@ -290,17 +266,16 @@ window.__ModuleLoader__.load({
290
266
  } catch { return '' }
291
267
  }
292
268
 
293
- /** Абсолютная дата для подсказки. Нечитаемая отметка — пустая подсказка. */
269
+ /** Exact timestamp for tooltip. Empty tooltip if timestamp is invalid. */
294
270
  function exactAt(then) {
295
271
  if (typeof then !== 'number' || then <= 0) return ''
296
272
  try { return new Date(then).toLocaleString() } catch { return '' }
297
273
  }
298
274
 
299
275
  /**
300
- * Что сказать о сверке.
276
+ * Sync telemetry status message.
301
277
  *
302
- * Доска, переставшая обновляться, не имеет права молчать: отличить
303
- * «ничего не изменилось» от «мы ослепли» иначе нечем.
278
+ * Alerts user when synchronization has halted or encountered errors.
304
279
  *
305
280
  * @returns {{tone: 'ok'|'bad'|'idle', text: string, title: string}}
306
281
  */
@@ -329,13 +304,11 @@ window.__ModuleLoader__.load({
329
304
  }
330
305
 
331
306
  /**
332
- * Каким цветом писать поверх фона метки.
307
+ * Accessible text color over label background color.
333
308
  *
334
- * Считаем воспринимаемую яркость и выбираем чёрный либо белый — так же
335
- * делает сам Gitea. Без этого `priority/low` на светло-зелёном становится
336
- * нечитаемой, а `type/security` на тёмно-красном сливается.
309
+ * Computes perceived brightness and picks black or white accordingly.
337
310
  *
338
- * @param {string} hex шесть шестнадцатеричных цифр без решётки
311
+ * @param {string} hex 6-character hex code without '#'
339
312
  */
340
313
  function readableOn(hex) {
341
314
  const n = parseInt(String(hex || ''), 16)
@@ -347,11 +320,9 @@ window.__ModuleLoader__.load({
347
320
  }
348
321
 
349
322
  /**
350
- * Цвет полосы срочности слева на карточке.
323
+ * Color accent stripe on left border of card for priority.
351
324
  *
352
- * Только `priority`: полоса одна, и отдавать её первой попавшейся метке
353
- * нельзя. Задача без срочности остаётся без полосы, а не получает серую —
354
- * пустая полоса читалась бы как «низкий приоритет», а это неправда.
325
+ * Only applies to `priority/*` labels. Tasks without priority display no stripe.
355
326
  */
356
327
  function priorityColor(task) {
357
328
  const values = (task && task.facets && task.facets.priority) || []
@@ -362,7 +333,7 @@ window.__ModuleLoader__.load({
362
333
  return ''
363
334
  }
364
335
 
365
- /** Стиль метки. Без цвета — прежний нейтральный вид, а не чёрный прямоугольник. */
336
+ /** Tag style helper. Neutral style when color is not defined. */
366
337
  function tagStyle(task, name) {
367
338
  const hex = task && task.labelColors ? task.labelColors[name] : undefined
368
339
  if (!hex) return undefined
@@ -370,12 +341,9 @@ window.__ModuleLoader__.load({
370
341
  }
371
342
 
372
343
  /**
373
- * Отрисовать разобранное тело задачи.
344
+ * Render parsed task body tree safely using React elements.
374
345
  *
375
- * Собираем ЭЛЕМЕНТАМИ, а не строкой HTML. Тело пишет кто угодно, и
376
- * `dangerouslySetInnerHTML` здесь означал бы выполнение чужого кода в
377
- * интерфейсе владельца. При сборке элементами подстановка невозможна по
378
- * построению — не потому, что кто-то не забыл про экранирование.
346
+ * Constructs DOM elements rather than using dangerouslySetInnerHTML to prevent XSS.
379
347
  */
380
348
  function renderSpans(spans, keyBase) {
381
349
  return (spans || []).map((span, i) => {
@@ -418,18 +386,12 @@ window.__ModuleLoader__.load({
418
386
  }
419
387
 
420
388
  /**
421
- * Подпись пространства меток.
389
+ * Label namespace display title.
422
390
  *
423
- * Ключа нет — показываем имя как есть. Это не пропуск, а решение: набор
424
- * меток в Gitea меняют без нас, и подставлять свой перевод новому
425
- * пространству значило бы врать о том, чего мы не знаем.
391
+ * Falls back to raw namespace name when no translation key exists.
426
392
  */
427
393
  /**
428
- * Название колонки: своё, если задано в настройках, иначе переведённое.
429
- *
430
- * Настройка одна на все колонки — парами `идентификатор=Название` через
431
- * запятую. Шесть отдельных полей ради переименования, которое делают раз в
432
- * жизни, засорили бы карточку настроек сильнее, чем помогли.
394
+ * Column title: custom override if configured, otherwise localized default.
433
395
  */
434
396
  function columnTitle(col, t) {
435
397
  const own = col && col.name
@@ -443,11 +405,9 @@ window.__ModuleLoader__.load({
443
405
  }
444
406
 
445
407
  /**
446
- * Название доски.
408
+ * Board title.
447
409
  *
448
- * Заведённые нами `main` и `simple` переводим по идентификатору: их
449
- * названия лежат в базе на одном языке и на другом интерфейсе остались бы
450
- * чужими. Переименованную человеком доску не трогаем — это его слово.
410
+ * Default 'main' and 'simple' boards use localized titles; custom board names are preserved verbatim.
451
411
  */
452
412
  function boardTitle(board, t) {
453
413
  const key = 'boardName.' + (board && board.id)
@@ -474,18 +434,14 @@ window.__ModuleLoader__.load({
474
434
  }
475
435
  }
476
436
 
477
- /** Отбор по репозиторию стоит в одном ряду с метками; имя должно совпадать с сервером. */
437
+ /** Repository facet filter key matching backend dimension. */
478
438
  const REPO_FACET = 'repo'
479
439
 
480
440
  /**
481
- * Подходит ли задача под выбранное.
482
- *
483
- * Между пространствами — И, внутри пространства — ИЛИ: срочность у задачи
484
- * одна, и «И» внутри пространства всегда давало бы пусто.
441
+ * Check whether task matches active facet filters.
485
442
  *
486
- * Метки уже разобраны сервером и приезжают в `task.facets`. Разбирать их
487
- * здесь во второй раз значило бы завести вторую правду о том, где кончается
488
- * пространство и начинается значение.
443
+ * AND across namespaces, OR within each namespace.
444
+ * Uses pre-parsed task.facets from server response.
489
445
  */
490
446
  function matchesFilters(task, selected) {
491
447
  const keys = Object.keys(selected || {})
@@ -500,14 +456,14 @@ window.__ModuleLoader__.load({
500
456
  return true
501
457
  }
502
458
 
503
- /** Выбрано ли что-нибудь: пустой набор доску не сужает. */
459
+ /** True if any filter is active. */
504
460
  function anySelected(selected) {
505
461
  const keys = Object.keys(selected || {})
506
462
  for (const ns of keys) if ((selected[ns] || []).length > 0) return true
507
463
  return false
508
464
  }
509
465
 
510
- /** Переключить значение. Новая карта, а не правка на месте: иначе React не перерисует. */
466
+ /** Toggle filter value, returning a new map for React immutability. */
511
467
  function toggleValue(selected, ns, value) {
512
468
  const current = (selected || {})[ns] || []
513
469
  const next = current.indexOf(value) >= 0
@@ -519,10 +475,7 @@ window.__ModuleLoader__.load({
519
475
  return out
520
476
  }
521
477
 
522
- /**
523
- * Человеческая причина отказа. Пустой список без объяснения читается как
524
- * «issue нет», и человек идёт искать несуществующую проблему.
525
- */
478
+ /** Human-readable explanation for empty state or error. */
526
479
  function errorKey(error) {
527
480
  switch (error) {
528
481
  case 'gitea-absent': return 'error.giteaAbsent'
@@ -539,13 +492,9 @@ window.__ModuleLoader__.load({
539
492
  }
540
493
 
541
494
  /**
542
- * Открыть чат поднятой сессии.
543
- *
544
- * Служба сессий берётся через `get`, а не объявляется в `inject`: если её в
545
- * сборке не окажется, доска обязана сказать об этом человеку, а не
546
- * отказаться загружаться целиком.
495
+ * Open session conversation in chat panel.
547
496
  *
548
- * @returns {boolean} удалось ли открыть
497
+ * @returns {boolean} true if session opened successfully
549
498
  */
550
499
  function openSession(ctx, sessionId) {
551
500
  if (!sessionId) return false
@@ -559,10 +508,7 @@ window.__ModuleLoader__.load({
559
508
  }
560
509
  }
561
510
 
562
- /**
563
- * Подпись репозитория в списке: имя и сколько в нём открытых задач.
564
- * Архивный помечается словом, а не только положением в списке.
565
- */
511
+ /** Repository option label with open issue count; marks archived repositories. */
566
512
  function repoOption(r, t) {
567
513
  const full = r.fullName || ((r.owner || '') + '/' + (r.repo || ''))
568
514
  const parts = [full]
@@ -571,12 +517,7 @@ window.__ModuleLoader__.load({
571
517
  return parts.join(' ')
572
518
  }
573
519
 
574
- /**
575
- * Разобрать `владелец/репозиторий` в пару.
576
- *
577
- * Владельца отдельным полем не спрашиваем: он однозначно следует из
578
- * выбранного репозитория, а лишнее поле — лишний способ ошибиться.
579
- */
520
+ /** Parse 'owner/repo' string into separate parts. */
580
521
  function splitFullName(fullName) {
581
522
  const parts = String(fullName || '').split('/')
582
523
  if (parts.length !== 2 || parts[0] === '' || parts[1] === '') return undefined
@@ -584,10 +525,7 @@ window.__ModuleLoader__.load({
584
525
  }
585
526
 
586
527
  /**
587
- * Отбор карточек по строке поиска: заголовок, репозиторий, номер issue.
588
- *
589
- * Фильтр держится на стороне браузера намеренно: доска и так вся в памяти,
590
- * а поход на сервер за каждой буквой сделал бы набор дёрганым.
528
+ * Filter cards by client-side text search query: title, repo, issue number.
591
529
  */
592
530
  function matchesQuery(task, query) {
593
531
  const needle = String(query || '').trim().toLowerCase()
@@ -601,10 +539,7 @@ window.__ModuleLoader__.load({
601
539
  return hay.indexOf(needle) >= 0
602
540
  }
603
541
 
604
- /**
605
- * Идентификатор сессии для чипа. Ядро отдаёт его либо свойством, либо
606
- * через useSession — берём первое, что есть.
607
- */
542
+ /** Retrieve session ID for header chip. */
608
543
  function chipSessionId(props, session) {
609
544
  return String((props && props.sessionId) || (session && (session.sessionId || session.id)) || '').trim()
610
545
  }
@@ -2036,7 +1971,7 @@ window.__ModuleLoader__.load({
2036
1971
  return h('li', { className: 'dkb-card dkb-cardOpen' }, head, h('div', { className: 'dkb-body' }, body))
2037
1972
  }
2038
1973
 
2039
- /** Стрелка влево для кнопки возврата: 14x14, обводка цветом текста. */
1974
+ /** Back button arrow icon: 14x14 stroke currentColor. */
2040
1975
  const BACK_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor"'
2041
1976
  + ' stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
2042
1977
  + '<path d="M10 3.5 5.5 8l4.5 4.5"/></svg>'
@@ -2192,14 +2127,8 @@ window.__ModuleLoader__.load({
2192
2127
  })
2193
2128
 
2194
2129
  /**
2195
- * Чем запускать: профиль агента и уровень доступа.
2196
- *
2197
- * Одни и те же поля стоят и в окне задачи, и в окне пачки — пачка идёт
2198
- * ОДНОЙ сессией, значит и профиль с доступом у неё один. Две копии полей
2199
- * разошлись бы при первой правке.
2200
- *
2201
- * Пустого списка не бывает наполовину: службы у ядра необязательны, и если
2202
- * выбирать не из чего, поле не рисуется вовсе.
2130
+ * Execution options: agent preset and permission rank.
2131
+ * Shared across single-task and batch launch modals.
2203
2132
  */
2204
2133
  function LaunchFields(props) {
2205
2134
  const t = props.t
@@ -2483,10 +2412,7 @@ window.__ModuleLoader__.load({
2483
2412
  }, [load, t])
2484
2413
 
2485
2414
  /**
2486
- * Спросить о переносе и, получив согласие, выполнить его.
2487
- *
2488
- * Общая для мыши и для меню на карточке: путь другой, последствия те же,
2489
- * и разойтись им нельзя.
2415
+ * Confirm and execute card move transition.
2490
2416
  */
2491
2417
  const askMove = React.useCallback((task, column, where) => {
2492
2418
  if (!task) return
@@ -4312,9 +4238,7 @@ window.__ModuleLoader__.load({
4312
4238
  }
4313
4239
 
4314
4240
  /**
4315
- * Чип задачи в шапке чата. Большинство сессий подняты не с доски, и это
4316
- * штатное положение: тогда чип не рисуется вовсе. Пока ответ не пришёл,
4317
- * тоже ничего — мигающая заглушка в шапке хуже её отсутствия.
4241
+ * Header task chip in chat conversation. Only rendered when session is linked to a task.
4318
4242
  */
4319
4243
  function TaskChip(props) {
4320
4244
  const t = props.t || fallbackT
@@ -4413,7 +4337,7 @@ window.__ModuleLoader__.load({
4413
4337
  + ' stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
4414
4338
  + '<rect x="2" y="2.5" width="12" height="11" rx="1.5"/><path d="M2 6.5h12M6.5 6.5v7"/></svg>'
4415
4339
 
4416
- /** Открыта ли доска. Крошечное хранилище: подписки нужны и строке, и экрану. */
4340
+ /** Board visibility state manager. */
4417
4341
  /** @see helpers.createToggle */
4418
4342
  function createToggle() {
4419
4343
  let open = false
@@ -4427,12 +4351,12 @@ window.__ModuleLoader__.load({
4427
4351
  for (const fn of listeners) fn()
4428
4352
  },
4429
4353
  toggle() { this.set(!open) },
4430
- /** Открыть доску на конкретной задаче. */
4354
+ /** Open board focused on a specific task. */
4431
4355
  show(taskId, boardId) {
4432
4356
  wanted = { id: String(taskId || ''), board: String(boardId || 'main') }
4433
4357
  if (open) { for (const fn of listeners) fn() } else this.set(true)
4434
4358
  },
4435
- /** Забрать просьбу. Читается один раз: повтор открывал бы окно снова. */
4359
+ /** Consume requested focus task ID (cleared after first read). */
4436
4360
  takeWanted() {
4437
4361
  const asked = wanted
4438
4362
  wanted = undefined
@@ -4442,12 +4366,7 @@ window.__ModuleLoader__.load({
4442
4366
  }
4443
4367
  }
4444
4368
 
4445
- /**
4446
- * Родная кнопка «Новая сессия».
4447
- *
4448
- * Отсеиваем узлы, у которых в классах есть `newSessionLabel`: это подпись
4449
- * внутри кнопки, а не сама кнопка.
4450
- */
4369
+ /** Find native 'New Session' sidebar button. */
4451
4370
  function nativeButton() {
4452
4371
  const nodes = document.querySelectorAll('button[class*="newSession"]')
4453
4372
  for (const node of nodes) {
@@ -4457,11 +4376,7 @@ window.__ModuleLoader__.load({
4457
4376
  }
4458
4377
 
4459
4378
  /**
4460
- * Кнопка доски: КЛОН родной кнопки со своими подписью и значком.
4461
- *
4462
- * Классы оболочки берутся у самой оболочки, а не переписываются у себя:
4463
- * повторять чужие стили руками — значит расходиться с ними при первом же
4464
- * обновлении интерфейса. Соседний плагин «Мастерская» поступает так же.
4379
+ * Kanban entry button cloning native button styles to match host theme.
4465
4380
  */
4466
4381
  function createEntry(target, toggle, label) {
4467
4382
  const entry = target.cloneNode(true)
@@ -4491,7 +4406,7 @@ window.__ModuleLoader__.load({
4491
4406
  return entry
4492
4407
  }
4493
4408
 
4494
- /** Встать последней среди уже вставленных чужих кнопок, а не влезать между. */
4409
+ /** Place button at end of injected extension entries. */
4495
4410
  function placeEntry(target, entry) {
4496
4411
  let anchor = target
4497
4412
  let next = target.nextElementSibling
@@ -4504,7 +4419,7 @@ window.__ModuleLoader__.load({
4504
4419
  return true
4505
4420
  }
4506
4421
 
4507
- /** Кнопка в боковой панели, переживающая перерисовку оболочки. */
4422
+ /** Sidebar entry button persistent across shell re-renders. */
4508
4423
  function mountSidebarEntry(toggle, label) {
4509
4424
  if (typeof document === 'undefined') return () => {}
4510
4425
 
@@ -4538,7 +4453,7 @@ window.__ModuleLoader__.load({
4538
4453
  }
4539
4454
  }
4540
4455
 
4541
- /** Экран доски поверх колонки разговора. */
4456
+ /** Board overlay container mounted over conversation column. */
4542
4457
  function mountBoard(ctx, toggle) {
4543
4458
  if (typeof document === 'undefined') return () => {}
4544
4459
  let root
@@ -4597,15 +4512,9 @@ window.__ModuleLoader__.load({
4597
4512
 
4598
4513
 
4599
4514
  /**
4600
- * Зарегистрировать компонент в первый слот, который его примет.
4601
- *
4602
- * Имя слота в сборке подтвердить нельзя: строки слотов в собранном
4603
- * интерфейсе не литеральны. Регистрация в несуществующий слот проходит
4604
- * молча и без ошибки в журнале, поэтому единственный надёжный способ —
4605
- * перебрать кандидатов и запомнить, какой сработал. Соседний плагин
4606
- * dsh-gitea поступает так же со своей карточкой настроек.
4515
+ * Register component into first available UI slot.
4607
4516
  *
4608
- * @returns {string|undefined} имя сработавшего слота
4517
+ * @returns {string|undefined} name of successfully resolved slot
4609
4518
  */
4610
4519
  function registerFirst(ctx, candidates, component) {
4611
4520
  for (const entry of candidates) {