@vernikr/size-report 2.4.0 → 2.5.0

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/src/modes.js CHANGED
@@ -13,28 +13,24 @@ import { artifact, rebuild } from './artifact.js';
13
13
  import { sensorGaps } from './metrics.js';
14
14
  import { totalsOf } from './derived.js';
15
15
 
16
- /* Режимы: что инструмент делает по запросу. Разбор аргументов в `src/args.js`, а
17
- * сюда приходит готовый план: какой режим, какой ключ, что печатать. Здесь же их
18
- * общие мелочи (знак «!» о другом счёте, вердикт, размер словами) один владелец
19
- * на все режимы, потому что один и тот же счёт и один и тот же знак не должны
20
- * разойтись между `--write`, `--data`, `--page` и `size check`.
21
- *
22
- * Что где: сборка и сверка отчёта (`--write`, проверка), данные контракта
23
- * (`--data`), полнота покрытия (`size check`), диагностика (`doctor`), хук и
24
- * объяснение пропущенной строки. Файл знает про все остальные модули сразу — это
25
- * его работа: связать их в одну команду.
16
+ /* Modes: what the tool does on request. The arguments are parsed in `src/args.js`, and a
17
+ * ready plan arrives here which mode, which flag, what to print. Their shared bits live
18
+ * here too (the "!" note about a different count, the verdict, a size in words), one owner
19
+ * for all modes, so that one count and one mark cannot diverge between `--write`, `--data`,
20
+ * `check` and the rest. Knowing every other module at once is this file's job: it ties them
21
+ * into one command.
26
22
  */
27
23
 
28
24
  function kmb(bytes) {
29
- return Math.round(bytes / 1024) + ' КБ';
25
+ return Math.round(bytes / 1024) + ' KB';
30
26
  }
31
27
 
32
- /* Деградация не ошибка, а факт отчёта: числа получены другим счётом (упрощение
33
- * вместо сжатия, оценка вместо точного счёта), потому что необязательной
34
- * зависимости нет. Факт печатается один раз на датчик и становится кодом 4 иначе
35
- * приближение уезжало бы в CI как успех. */
28
+ /* Degradation is a fact of the report, not an error: the numbers came from a different
29
+ * method (stripping instead of minification, an estimate instead of an exact count) because
30
+ * an optional dependency is missing. The fact is printed once per sensor and becomes code
31
+ * 4 otherwise an approximation would travel into CI as success. */
36
32
  function note(gaps) {
37
- gaps.forEach((gap) => console.error('! ' + gap.why + '\n починка: ' + gap.fix));
33
+ gaps.forEach((gap) => console.error('! ' + gap.why + '\n fix: ' + gap.fix));
38
34
  return gaps.length === 0 ? EXIT.OK : EXIT.SENSOR;
39
35
  }
40
36
 
@@ -42,12 +38,12 @@ function sensorNote(cfg) {
42
38
  return note(sensorGaps(cfg));
43
39
  }
44
40
 
45
- /* Вердикт режима вместе с заметками о датчиках: заметка печатается всегдамолчание
46
- * о другом счёте читается как точное число, и расхождение остаётся без причины, а
47
- * код остаётся первым по важности. Нарушение старше приближения (тот же порядок, что
48
- * у `size check` и у `doctor`): код 4 говорит «числа честные, но другим счётом», а
49
- * когда таблица расходится, этого никто не проверял расхождение может быть и
50
- * настоящей правкой мимо отчёта. */
41
+ /* The mode's verdict together with the sensor notes: the note is printed always silence
42
+ * about a different count reads as an exact number, and a disagreement would be left without
43
+ * a cause while the code stays the more important one. A violation outranks an
44
+ * approximation (the same order as in `check` and `doctor`): code 4 claims the numbers are
45
+ * honest but counted differently, and when the table disagrees nobody checked that — the
46
+ * disagreement may be a real edit that went past the report. */
51
47
  function verdict(code, gaps) {
52
48
  const sensors = note(gaps);
53
49
  return code === EXIT.OK ? sensors : code;
@@ -56,7 +52,7 @@ function verdict(code, gaps) {
56
52
  export function check(cfg, want, root) {
57
53
  const out = path.join(root, cfg.output);
58
54
  if (!fs.existsSync(out)) {
59
- console.error('✗ таблица размеров: нет файла ' + cfg.output + ' — соберите её: ' + cfg.fixCommand);
55
+ console.error('✗ size table: no file ' + cfg.output + ' — build it: ' + cfg.fixCommand);
60
56
  return 1;
61
57
  }
62
58
  const have = fs.readFileSync(out, 'utf8');
@@ -66,22 +62,22 @@ export function check(cfg, want, root) {
66
62
  const b = want.split('\n');
67
63
  let i = 0;
68
64
  while (i < a.length && i < b.length && a[i] === b[i]) i++;
69
- console.error('✗ таблица размеров: ' + cfg.output + ' расходится с историей git (строка ' + (i + 1) + '):');
70
- console.error(' в файле: ' + (a[i] === undefined ? '<строк нет>' : a[i].trim().slice(0, 160)));
71
- console.error(' по истории: ' + (b[i] === undefined ? '<строк нет>' : b[i].trim().slice(0, 160)));
65
+ console.error('✗ size table: ' + cfg.output + ' diverged from the git history (line ' + (i + 1) + '):');
66
+ console.error(' in the file: ' + (a[i] === undefined ? '<no rows>' : a[i].trim().slice(0, 160)));
67
+ console.error(' by the history: ' + (b[i] === undefined ? '<no rows>' : b[i].trim().slice(0, 160)));
72
68
  const missing = [...want.matchAll(/id="c-([^"]+)"/g)].map((m) => m[1])
73
69
  .filter((id) => have.indexOf('id="c-' + id + '"') === -1);
74
70
  if (missing.length > 0) {
75
- console.error(' строк нет в файле: ' + missing.length + ' (' + missing.slice(0, 5).join(', ')
71
+ console.error(' rows missing in the file: ' + missing.length + ' (' + missing.slice(0, 5).join(', ')
76
72
  + (missing.length > 5 ? ', …' : '') + ')');
77
73
  }
78
- console.error(' починка: ' + cfg.fixCommand + ' — и закоммитить ' + cfg.output + ' отдельным коммитом.');
74
+ console.error(' fix: ' + cfg.fixCommand + ' — and commit ' + cfg.output + ' in a commit of its own.');
79
75
  return 1;
80
76
  }
81
77
 
82
- /* Путь, названный ключом (`--write <файл>`), это настройка `output` этого
83
- * запуска: отчёт обязан называть себя тем путём, по которому лежит, иначе подпись в
84
- * нём указывала бы на чужое место. */
78
+ /* A path named on the command line (`--write <file>`) is this run's `output` setting: the
79
+ * report has to name itself by the path it lies at, or the note inside it would point
80
+ * somewhere else. */
85
81
  function withOutput(cfg, root, file) {
86
82
  if (typeof file !== 'string') return cfg;
87
83
  return Object.assign({}, cfg, { output: path.relative(root, path.resolve(file)) });
@@ -90,10 +86,10 @@ function withOutput(cfg, root, file) {
90
86
  export function writeMode(cfg, root, file) {
91
87
  const out = rebuild(withOutput(cfg, root, file), root);
92
88
  const { rows, files, now, skipped } = out.data;
93
- console.log('✓ ' + path.relative(root, out.file) + ': ' + rows.length + ' строк × ' + files.length + ' файлов, '
94
- + kmb(byteLen(out.html)) + ' (пропущено без строки: ' + skipped.length + ' — '
89
+ console.log('✓ ' + path.relative(root, out.file) + ': ' + rows.length + ' rows × ' + files.length + ' files, '
90
+ + kmb(byteLen(out.html)) + ' (skipped without a row: ' + skipped.length + ' — '
95
91
  + skipped.join(', ') + ')');
96
- console.log(' состояние на HEAD: ' + files.map((f, i) => f.label + ' '
92
+ console.log(' state at HEAD: ' + files.map((f, i) => f.label + ' '
97
93
  + (now[i] === null ? '—' : cfg.metrics.map((m) => now[i][m]).join('/'))).join(', '));
98
94
  return sensorNote(cfg);
99
95
  }
@@ -102,45 +98,45 @@ export function checkMode(cfg, root) {
102
98
  const out = artifact(cfg, root);
103
99
  const code = check(cfg, out.html, root);
104
100
  if (code === 0) {
105
- console.log('✓ отчёт: ' + out.data.rows.length + ' коммитов × ' + out.data.files.length + ' файлов '
106
- + 'совпадает с историей (' + cfg.output + ', ' + kmb(byteLen(out.html)) + ')');
101
+ console.log('✓ report: ' + out.data.rows.length + ' commits × ' + out.data.files.length + ' files '
102
+ + 'matches the history (' + cfg.output + ', ' + kmb(byteLen(out.html)) + ')');
107
103
  }
108
104
  return verdict(code, sensorGaps(cfg));
109
105
  }
110
106
 
111
- /* Ответ команды: `--json` машинная форма того же ответа, а не второй ответ.
112
- * Одна на три команды, чтобы «кто печатает и в каком виде» не разошёлся между
113
- * ними: разойтись он может только здесь, а байты ответа то, чем пользуется
114
- * агент. Текст берётся функцией: в машинной форме он не нужен вовсе. */
107
+ /* A command's answer: `--json` is the machine form of the same answer, not a second one.
108
+ * Shared by three commands so that "who prints and in which shape" cannot diverge between
109
+ * them that can only diverge here, and the bytes of the answer are what an agent consumes.
110
+ * The text comes as a function: the machine form does not need it at all. */
115
111
  function answer(rep, asJson, text) {
116
112
  if (asJson) process.stdout.write(JSON.stringify(rep, null, 2) + '\n');
117
113
  else console.log(text(rep));
118
114
  return rep;
119
115
  }
120
116
 
121
- /* Полнота покрытия (`size check`): настройки, история, пути, датчики. Не путать с
122
- * `checkMode` выше тот про таблицу и историю («файл совпадает с тем, что
123
- * сосчитано»), а этот про то, что сосчитано **всё**: ни один путь истории не
124
- * прошёл мимо колонок. Разные вопросы, поэтому и разные команды: держать отчёт в
125
- * git не обязательно, а вот полноту терять нельзя она той же проверкой и
126
- * заменяется. */
117
+ /* Coverage (`size check`): settings, history, paths, sensors. Not to be confused with
118
+ * `checkMode` above, which asks whether the file matches what was computed; this one asks
119
+ * whether **everything** was computed no path of the history went past the columns.
120
+ * Different questions, hence different commands: keeping the report in git is optional,
121
+ * losing completeness is not and this command is what replaces that check. */
127
122
  export function coverageMode(cfg, root, configFile, asJson) {
128
123
  const rep = answer(coverage(cfg, root, configFile), asJson, coverageText);
129
124
  return verdict(rep.ok ? EXIT.OK : EXIT.VIOLATION, rep.sensors);
130
125
  }
131
126
 
132
- /* Диагностика одним ответом (`size doctor`): окружение, зависимости, настройки и
133
- * покрытие сборкой из тех же кусков, что и остальные режимы. Код выхода не
134
- * «что-то не так», а первый по важности (настройкиисторияпокрытие
135
- * приближение): по нему агент ветвится, а текст читает человек. */
127
+ /* Diagnostics in one answer (`size doctor`): environment, dependencies, settings and
128
+ * coverage, assembled from the same pieces as the other modes. The exit code is not
129
+ * "something is wrong" but the first by importance (settingshistorycoverage
130
+ * approximation): an agent branches on it, a human reads the text. */
136
131
  export function doctorMode(root, configFile, asJson) {
137
132
  return answer(doctor(root, configFile), asJson, doctorText).exit;
138
133
  }
139
134
 
140
- /* Хук: установка, снятие и то, что он зовёт сам. Ставится и снимается только
141
- * явной командой; `hook-run` зовётся хуком и всегда отвечает кодом 0 — коммит уже
142
- * сделан, и валить его нечем (устройство и причины `src/hook.js`). Строка о
143
- * сделанном идёт в stderr: она часть вывода git, а не данных инструмента. */
135
+ /* The hook: installing, removing, and the call the hook itself makes. Installing and
136
+ * removing happen by explicit command only; `hook-run` is called by the hook and always
137
+ * answers 0 the commit is already made and there is nothing to fail it for (design and
138
+ * reasons: `src/hook.js`). What it did goes to stderr: it is part of git's output, not tool
139
+ * data. */
144
140
  export function hookMode(verb, root, configFile) {
145
141
  if (verb === 'hook-run') {
146
142
  const rep = hookRun(root, configFile);
@@ -152,17 +148,18 @@ export function hookMode(verb, root, configFile) {
152
148
  return rep.code;
153
149
  }
154
150
 
155
- /* Объяснение пропущенной строки (`size explain <коммит>`): ответ есть у любого
156
- * коммита, поэтому код выхода 0 и у «строка есть», и у «строки нет»; 2 только
157
- * когда названного коммита в истории нет или префикс подходит нескольким. */
151
+ /* Explaining a skipped row (`size explain <commit>`): any resolvable commit has an answer,
152
+ * so the exit code is 0 both when the row is there and when it is not; 2 belongs to a commit
153
+ * that cannot be resolved an unknown name, an ambiguous prefix, or one outside the
154
+ * history. */
158
155
  export function explainMode(cfg, root, target, asJson) {
159
156
  answer(explainCommit(cfg, root, target), asJson, explainText);
160
157
  return EXIT.OK;
161
158
  }
162
159
 
163
- /* Данные контракта в stdout — для страницы и для агента: та же правда, что в
164
- * артефакте, но без вёрстки и без производных величин. Прежняя форма `--json`
165
- * остаётся нетронутой: она заморожена эталоном паритета (fixtures/parity). */
160
+ /* Contract data on stdout — for the page and for an agent: the same truth as in the
161
+ * artifact, without markup and without derived numbers. The older `--json` form stays
162
+ * untouched: the parity fixture freezes it (`fixtures/parity`). */
166
163
  export function dataMode(cfg, root) {
167
164
  process.stdout.write(JSON.stringify(reportData(cfg, root), null, 2) + '\n');
168
165
  return sensorNote(cfg);
package/src/optional.js CHANGED
@@ -1,22 +1,24 @@
1
1
  import { createRequire } from 'module';
2
2
 
3
- /* Необязательные зависимости: минификатор и токенизатор. Их отсутствие не отказ,
4
- * а другой счёт (упрощение вместо сжатия, оценка вместо точного счёта), поэтому
5
- * загрузка у них общая и с одним устройством: ленивая, синхронная (`createRequire`
6
- * — замер синхронный проход, и `import()` сделал бы асинхронной всю цепочку ради
7
- * одного датчика) и без исключения наружу недоступность возвращается ответом.
3
+ /* Optional dependencies: the minifier and the tokenizer. Missing ones are not a refusal but a
4
+ * different count (a simplification instead of compression, an estimate instead of an exact
5
+ * count), so their loading is shared and shaped the same way: lazy, synchronous
6
+ * (`createRequire` measuring is one synchronous pass, and `import()` would make the whole
7
+ * chain asynchronous for the sake of a single sensor) and without an exception escaping —
8
+ * unavailability comes back as an answer.
8
9
  *
9
- * Шов отсутствия окружение `SIZE_REPORT_NO_OPTIONAL`: тем же путём идёт установка
10
- * без необязательных зависимостей и платформа, для которой пакета нет. Им же
11
- * проверяется, что инструмент работает и без них. */
10
+ * The seam of their absence is the `SIZE_REPORT_NO_OPTIONAL` environment variable: the same
11
+ * path serves an install without optional dependencies and a platform the package is not built
12
+ * for. It is also how the tests check that the tool works without them. */
12
13
 
13
14
  export const NO_OPTIONAL = 'SIZE_REPORT_NO_OPTIONAL';
14
15
 
15
- /* Версия берётся у самого пакета: число зависит от словаря и от алгоритма, поэтому
16
- * она попадает в способ, которым получено значение, а не остаётся в `node_modules`. */
16
+ /* The version is read from the package itself: the number depends on the dictionary and on the
17
+ * algorithm, so it belongs in the method the value came from rather than staying inside
18
+ * `node_modules`. */
17
19
  export function loadOptional(spec) {
18
20
  if (process.env[NO_OPTIONAL]) {
19
- return { tool: null, version: null, why: 'необязательные зависимости выключены (' + NO_OPTIONAL + ')' };
21
+ return { tool: null, version: null, why: 'the optional dependencies are switched off (' + NO_OPTIONAL + ')' };
20
22
  }
21
23
  const require = createRequire(import.meta.url);
22
24
  try {
package/src/page/app.css CHANGED
@@ -1,16 +1,12 @@
1
- /* Оформление страницы отчётато, что стоит сверх общей таблицы (`src/table.css`):
2
- * холст и типографика, панель выбора, состояния пустоты и адаптации под
3
- * узкое окно. Чисел и цвета дельт здесь нет намеренно: их задаёт общая часть, и она
4
- * же попадает в статический артефакт, поэтому двух наборов одной таблицы не
5
- * бывает. Страница открывается с диска, без сервера и без сети, поэтому ни одной
6
- * внешней ссылки в ней быть не может: только системные семейства шрифтов
7
- * (`ui-sans-serif`) и системные цвета (`Canvas`, `CanvasText`, `AccentColor`),
8
- * которые есть в любой теме.
1
+ /* The report page's styling what stands on top of the shared table (`src/table.css`): the canvas and the typography,
2
+ * the panel of choices, the empty states and the adaptation to a narrow window. Numbers and the colour of a delta are
3
+ * absent here on purpose: the shared part sets them, and the package holds one set of styles rather than two. The page
4
+ * opens from disk, without a server and without a network, so it can hold no external reference at all: only system font
5
+ * families (`ui-sans-serif`) and system colours (`Canvas`, `CanvasText`, `AccentColor`), which every theme has.
9
6
  *
10
- * Общая часть заморожена байтами артефакта (`src/css.js`), поэтому всё, где
11
- * страница расходится с её геометрией, собрано в разделе «адаптации» с причиной:
12
- * растить общую часть нельзя, а на узком экране колонка коммита в 300px съедает
13
- * весь экран. */
7
+ * The shared part is frozen by the artifact's bytes (`src/css.js`), which is why everything where the page departs from
8
+ * its geometry is gathered in the "adaptations" section together with its reason: the shared part cannot grow, while on a
9
+ * narrow screen the 300px commit column would eat the whole screen. */
14
10
 
15
11
  :root {
16
12
  color-scheme: light dark;
@@ -22,8 +18,8 @@
22
18
  --tint: rgba(127, 127, 127, .07);
23
19
  }
24
20
 
25
- /* Типографика и ритм: один шаг между блоками (--gap), крупный заголовок,
26
- * приглушённая подпись чтобы взгляд доходил до чисел, а не до служебного текста. */
21
+ /* Typography and rhythm: one step between blocks (--gap), a large heading, a muted note — so that the eye reaches the
22
+ * numbers rather than the service text. */
27
23
  body {
28
24
  margin: 0;
29
25
  padding: var(--pad) var(--pad) 48px;
@@ -35,8 +31,8 @@ body {
35
31
  h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em; }
36
32
  .sub { margin: 0 0 calc(var(--gap) + 4px); color: var(--muted); font-size: 12.5px; }
37
33
 
38
- /* Панель выбора карточка: она отделяет управление от данных и не сливается с
39
- * таблицей, которая начинается ниже. */
34
+ /* The panel of choices is a card: it separates the controls from the data and does not merge with the table that begins
35
+ * below. */
40
36
  .panel {
41
37
  margin: 0 0 var(--gap);
42
38
  padding: 12px 14px 13px;
@@ -56,20 +52,18 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
56
52
  }
57
53
  .panel .cap { display: block; margin-bottom: 4px; }
58
54
  .panel .row { display: flex; flex-wrap: wrap; gap: 3px 6px; align-items: center; }
59
- /* Способ замера видимым текстом под переключателями: словарь токенов и способ
60
- * сжатия выбираются настройками запуска, а не галочкой, поэтому читателю мало
61
- * навести мышь — он должен видеть, чем получено число. */
55
+ /* The way of counting is visible text under the switches: the token dictionary and the way of compression come from the
56
+ * settings of the run rather than from a checkbox, so pointing a mouse is not enough — the reader has to see what produced
57
+ * the number. */
62
58
  .panel .about { margin: 5px 0 0; color: var(--muted); font-size: 11.5px; }
63
59
 
64
- /* Дерево файлов: вложенность показана отступом и линией уровня, папкатакой же
65
- * переключатель, как файл, только его галочка отвечает за всё поддерево, а число
66
- * рядом говорит, за сколько файлов. Список файлов длиннее окна, поэтому панель
67
- * прокручивается сама: иначе управление вытолкнуло бы таблицу за экран.
60
+ /* The file tree: nesting is shown by an indent and a level line, and a folder is a switch like a file only its checkbox
61
+ * answers for the whole subtree, while the number beside it says for how many files. The list is longer than the window,
62
+ * so the panel scrolls itself: otherwise the controls would push the table off the screen.
68
63
  *
69
- * На узком экране прокручивается сам список (панель там растёт вместе со
70
- * страницей), на широком панель целиком (ниже): прокрутка одна, и она у того,
71
- * кто и вправду ограничен окном. Шрифт файлов тот же, что у чисел таблицы:
72
- * подписей в списке много и они короткие, а рядом с ними стоит таблица. */
64
+ * On a narrow screen the list itself scrolls (the panel grows with the page there), on a wide one the whole panel does
65
+ * (below): there is one scroll, and it belongs to whoever is really bounded by the window. Files use the same font as the
66
+ * table's numbers: the list holds many short captions, and the table stands right next to it. */
73
67
  .panel .files { max-height: min(30vh, 320px); overflow: auto; font-size: 12.5px; }
74
68
  .panel .tree { margin: 0; padding: 0 0 0 14px; list-style: none; }
75
69
  .panel .tree .tree { margin-left: 14px; padding-left: 9px; border-left: 1px solid var(--line); }
@@ -77,13 +71,12 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
77
71
  .box.dir { font-weight: 600; }
78
72
  .box .n { margin-left: 1px; color: var(--muted); font-size: 11px; }
79
73
 
80
- /* Сложенная папка (класс на строке, ставится кликом по знаку): поддерево лежит в
81
- * разметке и просто не показывается. Так складывание ничего не пересобирает
82
- * иначе каждый клик считал бы таблицу заново.
74
+ /* A folded folder (a class on the row, set by a click on the sign): the subtree lies in the markup and is simply not shown.
75
+ * That way folding rebuilds nothing otherwise every click would count the table anew.
83
76
  *
84
- * Знак складки: место под него есть у каждой строки (отступ строки), а сам он
85
- * только у папок так листья и папки выстроены в один столбец. Он не часть
86
- * галочки: галочка отвечает за числа, знак за то, сколько дерева видно. */
77
+ * The folding sign: every row has the room for it (the row's indent) while only folders carry one, which lines the leaves
78
+ * and the folders up in a single column. It is not part of the checkbox: the checkbox answers for the numbers, the sign for
79
+ * how much of the tree is visible. */
87
80
  .panel .tree li.folded > .tree { display: none; }
88
81
  .panel .tree li { position: relative; }
89
82
  .panel .tree li > .fold {
@@ -97,9 +90,8 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
97
90
  }
98
91
  .panel .tree li > .fold:hover { color: var(--ink); }
99
92
 
100
- /* Переключатель метка вокруг поля ввода: и подпись, и цель нажатия одна, поэтому
101
- * по нему попадает и мышь, и клавиатура (Space на поле ввода), и вспомогательные
102
- * технологии. */
93
+ /* A switch is a label around an input: one label and one click target, which is why a mouse, the keyboard (`Space` on the
94
+ * input) and assistive technology all reach it. */
103
95
  .box {
104
96
  display: inline-flex;
105
97
  gap: 6px;
@@ -111,25 +103,24 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
111
103
  .box:hover { background: var(--tint); }
112
104
  .box input { margin: 0; accent-color: AccentColor; }
113
105
  .box.all { font-weight: 600; }
114
- /* Файл или папка вне отчёта: галочка на месте, но снята и недоступначисел для
115
- * него не измеряли, и переключать нечего. Строка приглушена и на мышь не
116
- * отзывается: обещать нажатие нечем. */
106
+ /* A file or folder outside the report: the checkbox is there but off and unavailable no numbers were measured for it, so
107
+ * there is nothing to switch. The row is muted and does not respond to the mouse: there is nothing to promise a click
108
+ * with. */
117
109
  .box.plain { cursor: default; opacity: .55; }
118
110
  .box.plain:hover { background: none; }
119
111
  .box.plain input { cursor: default; }
120
112
 
121
- /* Расшифровки под деревом файлов нет намеренно: под списком она отодвигала
122
- * числа, а её содержимое и так стоит рядом с тем, что объясняет, цвет дельт
123
- * называет сам знак числа, точность стоит под переключателями метрик, а знак
124
- * «файла ещё нет» — в подсказке клетки.
113
+ /* There is no legend under the file tree, and on purpose: below the list it pushed the numbers away, while its content
114
+ * already stands next to what it explains the colour of a delta is named by the sign of the number itself, accuracy
115
+ * stands under the metric switches, and the mark of a file that is not there yet lives in the cell's tooltip.
125
116
  *
126
- * Приближённое число помечено пунктиром, а не цветом: цвет в таблице занят дельтой
127
- * (рост и спад), и второй смысл на том же признаке читался бы как первый. */
117
+ * An approximate number is marked by a dashed line rather than a colour: colour in the table is taken by the delta (growth
118
+ * and fall), and a second meaning on the same sign would read as the first. */
128
119
  #grid td.approx { text-decoration: underline dotted; text-underline-offset: 2.5px; }
129
120
 
130
- /* Таблица в своей рамке и со своим скроллом: шапка и колонка коммита липнут к ней
131
- * (правила липкости в общей части), а не к странице, поэтому при прокрутке вбок
132
- * видно, чей это ряд, а при прокрутке вниз — что за колонка. */
121
+ /* The table has a frame and a scroll of its own: the header and the commit column stick to it (the rules of stickiness
122
+ * live in the shared part) rather than to the page, so scrolling sideways shows whose row it is while scrolling down shows
123
+ * which column it is. */
133
124
  .shell {
134
125
  overflow: auto;
135
126
  max-height: calc(100vh - 300px);
@@ -137,12 +128,12 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
137
128
  border-radius: var(--radius);
138
129
  background: Canvas;
139
130
  }
140
- /* Числа плотнее текста страницы: их больше и они короче, а читаются по разрядам. */
131
+ /* Numbers are denser than the page's text: there are more of them, they are shorter, and they are read by their digits. */
141
132
  #grid { font-size: 12.5px; }
142
133
 
143
- /* Сообщение о присланной ссылке над таблицей, чтобы пропустить его было нельзя,
144
- * но таблицу оно не отодвигает: одна строка на месте страницы. Цвет берётся
145
- * системный (акцент), своих цветов у страницы нет. */
134
+ /* The message about a link that came in stands above the table, so that it cannot be missed, while it does not push the
135
+ * table away: one line in the place of the page. Its colour is the system one (the accent), for the page keeps no colours
136
+ * of its own. */
146
137
  .notice {
147
138
  margin: 0 0 var(--gap);
148
139
  padding: 9px 13px;
@@ -153,8 +144,8 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
153
144
  }
154
145
  .notice[hidden] { display: none; }
155
146
 
156
- /* Состояния пустоты: когда таблицу не из чего собрать, страница говорит об этом
157
- * словами, а не пустой сеткой. */
147
+ /* The empty states: when there is nothing to assemble a table from, the page says so in words rather than showing an empty
148
+ * grid. */
158
149
  .state {
159
150
  margin: var(--gap) 0 0;
160
151
  padding: 11px 13px;
@@ -168,28 +159,24 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
168
159
  .note { margin: 14px 0 0; max-width: 90em; color: var(--muted); font-size: 12px; }
169
160
  .note code { background: var(--tint); padding: 0 3px; border-radius: 3px; }
170
161
 
171
- /* Клавиатура: рамка фокуса видна на любом фоне (системный цвет акцента) и не
172
- * сдвигает разметку. Ссылки журнала и все переключатели доступны с Tab. */
162
+ /* The keyboard: the focus ring is visible on any background (the system accent colour) and shifts no layout. Journal links
163
+ * and every switch are reachable with Tab. */
173
164
  :focus-visible { outline: 2px solid AccentColor; outline-offset: 2px; border-radius: 3px; }
174
165
 
175
- /* Широкая страница: панель выбора (метрики, дерево файлов) стоит **слева** от
176
- * таблицы, а страница целиком укладывается в окно. Это не украшение: на десктопе
177
- * бокового места много, а вертикального мало переключатели и числа видны
178
- * одновременно, и ни прокрутка чисел, ни прокрутка списка файлов не уводит
179
- * управление за экран. Узкое окно эту же раскладку снимает (ниже): там столбцы
180
- * снова идут друг под другом, потому что рядом им не хватает ширины.
166
+ /* A wide page: the panel of choices (metrics, file tree) stands **left** of the table, and the whole page fits the window.
167
+ * That is not decoration: a desktop has much side room and little vertical room — the switches and the numbers are visible
168
+ * at once, and neither scrolling the numbers nor scrolling the file list takes the controls off the screen. A narrow window
169
+ * drops this layout (below): there the columns run one under another again, because side by side they lack the width.
181
170
  *
182
- * Раскладка сетка на `body`, а не обёртка в разметке: страница собирается
183
- * вклейкой глав (`src/page/build.js`), и добавлять ей узлы ради оформления значило
184
- * бы менять форму страницы в двух местах вместо одного. Строк пять, и они названы
185
- * по предмету: заголовок, сообщение о ссылке, **рабочая строка**, сообщение
186
- * пустоты и подпись. Тянется только рабочая таблица получает всю оставшуюся
187
- * высоту, а панель не больше неё; при этом своя высота у панели не «сколько
188
- * получилось» (тогда она вытолкнула бы таблицу за экран), а та же рабочая строка,
189
- * внутри которой она прокручивается: список файлов длиннее окна — обычное дело.
171
+ * The layout is a grid on `body` rather than a wrapper in the markup: the page is assembled by pasting chapters
172
+ * (`src/page/build.js`), and adding nodes to it for the sake of styling would mean changing the page's shape in two places
173
+ * instead of one. There are five rows, named by subject: the heading, the message about a link, the **working row**, the
174
+ * empty state and the note. Only the working row stretches — the table gets all the remaining height and the panel no more
175
+ * than that; the panel's own height is not "whatever came out" (it would push the table off the screen) but that same
176
+ * working row, inside which it scrolls: a file list longer than the window is the usual case.
190
177
  *
191
- * Порог 900px тот же, что у адаптаций ниже: одна граница на «широко» и «узко»,
192
- * иначе между двумя порогами страница осталась бы без ни одного правила. */
178
+ * The threshold of 900px is the same as the adaptations' below: one border between "wide" and "narrow", or the page would
179
+ * be left with no rule at all between two thresholds. */
193
180
  @media (min-width: 900px) {
194
181
  body {
195
182
  box-sizing: border-box;
@@ -208,22 +195,20 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
208
195
  #shell { grid-area: 3 / 2 / 4 / 3; }
209
196
  #state { grid-area: 4 / 2 / 5 / 3; align-self: start; }
210
197
  #note { grid-area: 5 / 2 / 6 / 3; }
211
- /* Работа на всю высоту, как и таблица: панель кончается там же, где она.
212
- * Прокручивается панель, а не страница. Верхнего отступа у неё здесь нет:
213
- * список проезжает под ним, и липкая строка категорий стояла бы не вплотную к
214
- * краю, а под полосой проезжающих файлов. Отступ никуда не делся — он у первого
215
- * поля, и уезжает вместе с ним. */
198
+ /* The working row runs the full height, as the table does: the panel ends where it ends. The panel scrolls rather than the
199
+ * page. It has no top padding here: the list drives under it, and the sticky row of categories would stand not flush with
200
+ * the edge but under a band of passing files. The padding has not gone anywhere — it is on the first field and travels
201
+ * away with it. */
216
202
  #panel { grid-area: 2 / 1 / -1 / 2; min-height: 0; margin-bottom: 0; padding-top: 0; overflow: auto; }
217
203
  .panel > fieldset:first-child { padding-top: 12px; }
218
- /* Высоту таблицы здесь задаёт строка, а не окно: своё правило высоты ей нужно
219
- * только в узком окне, где страница прокручивается целиком. */
204
+ /* Here the row rather than the window sets the table's height: it needs a height rule of its own only in a narrow window,
205
+ * where the whole page scrolls. */
220
206
  .shell { max-height: none; }
221
- /* Прокрутка у панели одна: список файлов больше её не заводит, и поле «Файлы»
222
- * не режет дерево своим потолком. */
207
+ /* The panel has a single scroll: the file list starts none of its own, and the "Files" field no longer cuts the tree with
208
+ * its ceiling. */
223
209
  .panel .files { max-height: none; overflow: visible; }
224
- /* Переключатели категорий остаются на виду, пока листаешь дерево. Фон тот же,
225
- * что у панели (поверхность плюс её подсветка), иначе под ними читались бы
226
- * проезжающие строки списка. */
210
+ /* The category switches stay in sight while the tree is scrolled. The background is the panel's own (the surface plus its
211
+ * tint), or the passing rows of the list would read through them. */
227
212
  .panel .cats {
228
213
  position: sticky;
229
214
  top: 0;
@@ -231,16 +216,15 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
231
216
  background-color: Canvas;
232
217
  background-image: linear-gradient(var(--tint), var(--tint));
233
218
  }
234
- /* Без метрик таблицы нет, и растянутая пустая строка ей не место: свободное
235
- * место этой строки забирает сообщение о пустоте, а не полоса над ним. */
219
+ /* Without metrics there is no table, and a stretched empty row has no place there: the empty state takes the free space of
220
+ * that row rather than the band above it. */
236
221
  body:has(#shell[hidden]) #state { grid-area: 3 / 2 / 4 / 3; }
237
222
  }
238
223
 
239
- /* Адаптации: единственное место, где страница правит общую геометрию,потому что
240
- * общая часть заморожена байтами артефакта, а не потому что так удобнее. Порог
241
- * 899px, а не 900: при ровно 900px обе половины применились бы к одной странице, и
242
- * от «узкой» в «широкой» остался бы потолок высоты таблицы — то есть пустое место
243
- * под ней на одном единственном размере окна. */
224
+ /* Adaptations: the one place where the page overrides the shared geometry because the shared part is frozen by the
225
+ * artifact's bytes, not because it is handier that way. The threshold is 899px rather than 900: at exactly 900px both halves
226
+ * would apply to one page, and the table's height ceiling would survive from the "narrow" into the "wide" one — that is,
227
+ * empty space under it at that single window size. */
244
228
  @media (max-width: 899px) {
245
229
  body { padding: 14px 14px 32px; }
246
230
  h1 { font-size: 18px; }