@vernikr/size-report 2.4.0 → 2.6.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.
Files changed (48) hide show
  1. package/README.md +989 -1081
  2. package/bin/postinstall.js +17 -18
  3. package/bin/size.js +2 -2
  4. package/package.json +3 -4
  5. package/src/args.js +72 -72
  6. package/src/artifact.js +14 -14
  7. package/src/check.js +41 -42
  8. package/src/cli.js +26 -29
  9. package/src/config.js +87 -91
  10. package/src/css.js +14 -14
  11. package/src/data.js +26 -50
  12. package/src/derived.js +31 -35
  13. package/src/doctor.js +95 -99
  14. package/src/explain.js +46 -47
  15. package/src/git.js +66 -71
  16. package/src/history.js +74 -83
  17. package/src/hook.js +130 -149
  18. package/src/init.js +37 -37
  19. package/src/journal.js +17 -15
  20. package/src/locales.js +31 -22
  21. package/src/metrics.js +72 -89
  22. package/src/minify.js +28 -27
  23. package/src/modes.js +57 -60
  24. package/src/optional.js +13 -11
  25. package/src/page/app.css +76 -94
  26. package/src/page/app.js +124 -80
  27. package/src/page/build.js +193 -50
  28. package/src/page/dom.js +8 -9
  29. package/src/page/panel.js +157 -69
  30. package/src/page/payload.js +168 -0
  31. package/src/page/state.js +144 -104
  32. package/src/page/table.js +270 -86
  33. package/src/parse-worker.js +10 -10
  34. package/src/parse.js +43 -45
  35. package/src/project.js +100 -104
  36. package/src/refusal.js +75 -76
  37. package/src/size-table.js +41 -76
  38. package/src/strip/forms.js +5 -5
  39. package/src/strip/guard.js +28 -28
  40. package/src/strip/js.js +27 -27
  41. package/src/strip.js +17 -21
  42. package/src/table.css +54 -19
  43. package/src/tokens.js +27 -27
  44. package/src/tool.js +10 -11
  45. package/templates/README.md +71 -77
  46. package/templates/ci.yml +33 -33
  47. package/templates/size-report.config.json +3 -3
  48. package/CHANGELOG.md +0 -690
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 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,20 @@ 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
- * «файла ещё нет» — в подсказке клетки.
125
- *
126
- * Приближённое число помечено пунктиром, а не цветом: цвет в таблице занят дельтой
127
- * (рост и спад), и второй смысл на том же признаке читался бы как первый. */
128
- #grid td.approx { text-decoration: underline dotted; text-underline-offset: 2.5px; }
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, the way each
115
+ * number was counted stands under the metric switches, and the mark of a gap lives in the cell's text. */
129
116
 
130
- /* Таблица в своей рамке и со своим скроллом: шапка и колонка коммита липнут к ней
131
- * (правила липкости в общей части), а не к странице, поэтому при прокрутке вбок
132
- * видно, чей это ряд, а при прокрутке вниз — что за колонка. */
117
+ /* The table has a frame and a scroll of its own: the header and the commit column stick to it (the rules of stickiness
118
+ * live in the shared part) rather than to the page, so scrolling sideways shows whose row it is while scrolling down shows
119
+ * which column it is. */
133
120
  .shell {
134
121
  overflow: auto;
135
122
  max-height: calc(100vh - 300px);
@@ -137,12 +124,12 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
137
124
  border-radius: var(--radius);
138
125
  background: Canvas;
139
126
  }
140
- /* Числа плотнее текста страницы: их больше и они короче, а читаются по разрядам. */
127
+ /* Numbers are denser than the page's text: there are more of them, they are shorter, and they are read by their digits. */
141
128
  #grid { font-size: 12.5px; }
142
129
 
143
- /* Сообщение о присланной ссылке над таблицей, чтобы пропустить его было нельзя,
144
- * но таблицу оно не отодвигает: одна строка на месте страницы. Цвет берётся
145
- * системный (акцент), своих цветов у страницы нет. */
130
+ /* The message about a link that came in stands above the table, so that it cannot be missed, while it does not push the
131
+ * table away: one line in the place of the page. Its colour is the system one (the accent), for the page keeps no colours
132
+ * of its own. */
146
133
  .notice {
147
134
  margin: 0 0 var(--gap);
148
135
  padding: 9px 13px;
@@ -153,8 +140,8 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
153
140
  }
154
141
  .notice[hidden] { display: none; }
155
142
 
156
- /* Состояния пустоты: когда таблицу не из чего собрать, страница говорит об этом
157
- * словами, а не пустой сеткой. */
143
+ /* The empty states: when there is nothing to assemble a table from, the page says so in words rather than showing an empty
144
+ * grid. */
158
145
  .state {
159
146
  margin: var(--gap) 0 0;
160
147
  padding: 11px 13px;
@@ -168,28 +155,24 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
168
155
  .note { margin: 14px 0 0; max-width: 90em; color: var(--muted); font-size: 12px; }
169
156
  .note code { background: var(--tint); padding: 0 3px; border-radius: 3px; }
170
157
 
171
- /* Клавиатура: рамка фокуса видна на любом фоне (системный цвет акцента) и не
172
- * сдвигает разметку. Ссылки журнала и все переключатели доступны с Tab. */
158
+ /* The keyboard: the focus ring is visible on any background (the system accent colour) and shifts no layout. Journal links
159
+ * and every switch are reachable with Tab. */
173
160
  :focus-visible { outline: 2px solid AccentColor; outline-offset: 2px; border-radius: 3px; }
174
161
 
175
- /* Широкая страница: панель выбора (метрики, дерево файлов) стоит **слева** от
176
- * таблицы, а страница целиком укладывается в окно. Это не украшение: на десктопе
177
- * бокового места много, а вертикального мало переключатели и числа видны
178
- * одновременно, и ни прокрутка чисел, ни прокрутка списка файлов не уводит
179
- * управление за экран. Узкое окно эту же раскладку снимает (ниже): там столбцы
180
- * снова идут друг под другом, потому что рядом им не хватает ширины.
162
+ /* A wide page: the panel of choices (metrics, file tree) stands **left** of the table, and the whole page fits the window.
163
+ * That is not decoration: a desktop has much side room and little vertical room — the switches and the numbers are visible
164
+ * at once, and neither scrolling the numbers nor scrolling the file list takes the controls off the screen. A narrow window
165
+ * drops this layout (below): there the columns run one under another again, because side by side they lack the width.
181
166
  *
182
- * Раскладка сетка на `body`, а не обёртка в разметке: страница собирается
183
- * вклейкой глав (`src/page/build.js`), и добавлять ей узлы ради оформления значило
184
- * бы менять форму страницы в двух местах вместо одного. Строк пять, и они названы
185
- * по предмету: заголовок, сообщение о ссылке, **рабочая строка**, сообщение
186
- * пустоты и подпись. Тянется только рабочая таблица получает всю оставшуюся
187
- * высоту, а панель не больше неё; при этом своя высота у панели не «сколько
188
- * получилось» (тогда она вытолкнула бы таблицу за экран), а та же рабочая строка,
189
- * внутри которой она прокручивается: список файлов длиннее окна — обычное дело.
167
+ * The layout is a grid on `body` rather than a wrapper in the markup: the page is assembled by pasting chapters
168
+ * (`src/page/build.js`), and adding nodes to it for the sake of styling would mean changing the page's shape in two places
169
+ * instead of one. There are five rows, named by subject: the heading, the message about a link, the **working row**, the
170
+ * empty state and the note. Only the working row stretches — the table gets all the remaining height and the panel no more
171
+ * than that; the panel's own height is not "whatever came out" (it would push the table off the screen) but that same
172
+ * working row, inside which it scrolls: a file list longer than the window is the usual case.
190
173
  *
191
- * Порог 900px тот же, что у адаптаций ниже: одна граница на «широко» и «узко»,
192
- * иначе между двумя порогами страница осталась бы без ни одного правила. */
174
+ * The threshold of 900px is the same as the adaptations' below: one border between "wide" and "narrow", or the page would
175
+ * be left with no rule at all between two thresholds. */
193
176
  @media (min-width: 900px) {
194
177
  body {
195
178
  box-sizing: border-box;
@@ -208,22 +191,20 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
208
191
  #shell { grid-area: 3 / 2 / 4 / 3; }
209
192
  #state { grid-area: 4 / 2 / 5 / 3; align-self: start; }
210
193
  #note { grid-area: 5 / 2 / 6 / 3; }
211
- /* Работа на всю высоту, как и таблица: панель кончается там же, где она.
212
- * Прокручивается панель, а не страница. Верхнего отступа у неё здесь нет:
213
- * список проезжает под ним, и липкая строка категорий стояла бы не вплотную к
214
- * краю, а под полосой проезжающих файлов. Отступ никуда не делся — он у первого
215
- * поля, и уезжает вместе с ним. */
194
+ /* The working row runs the full height, as the table does: the panel ends where it ends. The panel scrolls rather than the
195
+ * page. It has no top padding here: the list drives under it, and the sticky row of categories would stand not flush with
196
+ * the edge but under a band of passing files. The padding has not gone anywhere — it is on the first field and travels
197
+ * away with it. */
216
198
  #panel { grid-area: 2 / 1 / -1 / 2; min-height: 0; margin-bottom: 0; padding-top: 0; overflow: auto; }
217
199
  .panel > fieldset:first-child { padding-top: 12px; }
218
- /* Высоту таблицы здесь задаёт строка, а не окно: своё правило высоты ей нужно
219
- * только в узком окне, где страница прокручивается целиком. */
200
+ /* 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,
201
+ * where the whole page scrolls. */
220
202
  .shell { max-height: none; }
221
- /* Прокрутка у панели одна: список файлов больше её не заводит, и поле «Файлы»
222
- * не режет дерево своим потолком. */
203
+ /* The panel has a single scroll: the file list starts none of its own, and the "Files" field no longer cuts the tree with
204
+ * its ceiling. */
223
205
  .panel .files { max-height: none; overflow: visible; }
224
- /* Переключатели категорий остаются на виду, пока листаешь дерево. Фон тот же,
225
- * что у панели (поверхность плюс её подсветка), иначе под ними читались бы
226
- * проезжающие строки списка. */
206
+ /* The category switches stay in sight while the tree is scrolled. The background is the panel's own (the surface plus its
207
+ * tint), or the passing rows of the list would read through them. */
227
208
  .panel .cats {
228
209
  position: sticky;
229
210
  top: 0;
@@ -231,20 +212,21 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
231
212
  background-color: Canvas;
232
213
  background-image: linear-gradient(var(--tint), var(--tint));
233
214
  }
234
- /* Без метрик таблицы нет, и растянутая пустая строка ей не место: свободное
235
- * место этой строки забирает сообщение о пустоте, а не полоса над ним. */
215
+ /* Without metrics there is no table, and a stretched empty row has no place there: the empty state takes the free space of
216
+ * that row rather than the band above it. */
236
217
  body:has(#shell[hidden]) #state { grid-area: 3 / 2 / 4 / 3; }
237
218
  }
238
219
 
239
- /* Адаптации: единственное место, где страница правит общую геометрию,потому что
240
- * общая часть заморожена байтами артефакта, а не потому что так удобнее. Порог
241
- * 899px, а не 900: при ровно 900px обе половины применились бы к одной странице, и
242
- * от «узкой» в «широкой» остался бы потолок высоты таблицы — то есть пустое место
243
- * под ней на одном единственном размере окна. */
220
+ /* Adaptations: the one place where the page overrides the shared geometry because the shared part is frozen by the
221
+ * artifact's bytes, not because it is handier that way. The threshold is 899px rather than 900: at exactly 900px both halves
222
+ * would apply to one page, and the table's height ceiling would survive from the "narrow" into the "wide" one — that is,
223
+ * empty space under it at that single window size. */
244
224
  @media (max-width: 899px) {
245
225
  body { padding: 14px 14px 32px; }
246
226
  h1 { font-size: 18px; }
247
227
  #grid { font-size: 12px; }
248
228
  .shell { max-height: calc(100vh - 260px); }
249
- .clip { width: 190px; }
229
+ /* The commit column needs no rule of its own here any more: its width is the measure `--clip` of the shared part
230
+ * (`src/table.css`), and on a narrow screen that measure stands as it is. A shorter one would be `--clip: 150px`
231
+ * here — the adaptation the shared part cannot make for itself. */
250
232
  }
package/src/page/app.js CHANGED
@@ -1,106 +1,150 @@
1
- import { appData, appUi, appView, appWrite, appNotice, appLinkUse, appRead, appApply, appFoldRead } from './state.js';
2
- import { appBody, appHead, appState } from './table.js';
3
- import { appPanel } from './panel.js';
1
+ import { appUnpack } from './payload.js';
2
+ import { appAddressDrop, appApply, appBoot, appData, appFoldRead, appLinkUse, appNotice, appRead, appUi, appView, appWrite } from './state.js';
3
+ import { appColumn, appContribute, appMetrics, appState, appTable, appTotals, appTotalsReset } from './table.js';
4
+ import { appPanel, appPanelAll, appPanelState } from './panel.js';
4
5
 
5
- /* Сборка таблицы: что показывать (метрики и файлы, оставленные читателем) и куда
6
- * это положить. Разметку шапки и строк строит глава таблицы, числаобщий расчёт:
7
- * здесь остаётся только решение и вставка, своих чисел у сборки нет. */
8
- function appTable() {
9
- const shown = appData.metrics.filter((m) => appView.metrics[m.key]);
10
- const metrics = shown.map((m) => m.key);
11
- const on = appView.files;
12
- const files = [];
13
- appData.files.forEach((f, i) => { if (on[i]) files.push(i); });
14
- /* Колонки, которых коснулся последний коммит, идут впереди: отчёт пересобирается
15
- * после каждого коммита, и первый вопрос читателя — что принесла эта правка. Внутри
16
- * каждой части порядок прежний, из настроек: `sort` устойчив, а порядок колонок — это
17
- * то, к чему читатель привык. Знак берётся из истории (её знает движок), а не из
18
- * чисел: правка без изменения размера тоже правка. */
19
- files.sort((a, b) => (appData.last[a] === true ? 0 : 1) - (appData.last[b] === true ? 0 : 1));
6
+ /* Assembling the report: the table is built once (`appTable` of the table chapter) and everything afterwards only
7
+ * shows, hides and recounts. A click on any switch therefore costs a class, a number and the fields it reached the
8
+ * whole table used to be destroyed and built again, which was 81 % of the cost of a click and produced a hundred
9
+ * thousand dead nodes for the collector to walk.
10
+ *
11
+ * Hence two paths and no third: `appPaint` draws the whole view (the first drawing, a record from the browser's
12
+ * memory, a link in the address), while `appSwitch`, `appSwitchGroup` and `appSwitchMetric` are what one click on a
13
+ * box does. Neither makes a node.
14
+ */
20
15
 
21
- const table = document.getElementById('grid');
22
- table.textContent = '';
23
- appState(metrics.length, files.length);
24
- if (metrics.length === 0) return;
16
+ // The table's cache of node references: made once, at the first drawing.
17
+ let appCache = null;
25
18
 
26
- table.appendChild(appHead(shown, files, metrics));
27
- table.appendChild(appBody(metrics, files));
19
+ /* The note under the table: what a row is and how the report was made. It does not depend on the choice, so it is
20
+ * written once — with the table rather than with every drawing of it. */
21
+ function appNote() {
28
22
  document.getElementById('note').textContent = appUi.note
29
23
  .replace('{rows}', appData.rows.length)
30
24
  .replace('{command}', appData.report.fixCommand);
25
+ }
26
+
27
+ /* What the empty states are told: how many metrics and how many files are left. The table stands there in either
28
+ * case (it is built once) — the words are about what is shown. */
29
+ export function appCounts() {
30
+ appState(appData.metrics.filter((m) => appView.metrics[m.key] === true).length,
31
+ appView.files.filter((on) => on === true).length);
32
+ }
33
+
34
+ /* The whole view drawn: every column, the totals of the whole selection, the metrics, the empty states and the panel's
35
+ * fields. This is what a link, a record from the memory and the first drawing need — and it makes no node either. */
36
+ export function appPaint() {
37
+ appData.files.forEach((_f, i) => appColumn(appCache, i, appView.files[i]));
38
+ appTotalsReset(appCache);
39
+ appMetrics(appCache);
40
+ appCounts();
41
+ appPanelAll();
31
42
  appWrite();
32
43
  }
33
44
 
34
- /* Прокрутка панели свойство панели, а не разметки, поэтому она переживает
35
- * пересборку: иначе каждый клик по галочке возвращал бы список к началу, и до
36
- * нижних файлов дерева было бы не добраться. Запоминается прокрутка панели и
37
- * списка файлов — у каждого она своя, а в узком окне прокручивается список.
38
- * Элементы берутся те, что есть в разметке страницы (`src/page/build.js`):
39
- * второго перечисления мест прокрутки в пакете нет. */
40
- const appScrolled = ['#panel', '#panel .files'];
41
- function appScrollTop() {
42
- return appScrolled.map((sel) => {
43
- const el = document.querySelector(sel);
44
- return el === null ? 0 : el.scrollTop;
45
- });
45
+ /* One file switched by the reader: the view, its column, its share of the totals and the fields it shows in — each in
46
+ * its own place. The message about a link fades here: by this action the reader has read it. */
47
+ export function appSwitch(i, on) {
48
+ if (appView.files[i] === on) return;
49
+ appView.files[i] = on;
50
+ appColumn(appCache, i, on);
51
+ appContribute(appCache, i, on);
52
+ appTotals(appCache);
53
+ appCounts();
54
+ appPanelState([i]);
55
+ appWrite();
56
+ appNotice('');
46
57
  }
47
58
 
48
- function appScrollBack(saved) {
49
- appScrolled.forEach((sel, i) => {
50
- const el = document.querySelector(sel);
51
- if (el !== null) el.scrollTop = saved[i];
59
+ /* A group switched at once — a folder or a category: the same work per file, then the totals once and the fields of
60
+ * the files the choice really reached (switching a folder on when a part of it was already on touches only the rest,
61
+ * and a field that did not move is not written). */
62
+ export function appSwitchGroup(indexes, on) {
63
+ const touched = indexes.filter((i) => appView.files[i] !== on);
64
+ touched.forEach((i) => {
65
+ appView.files[i] = on;
66
+ appColumn(appCache, i, on);
67
+ appContribute(appCache, i, on);
52
68
  });
69
+ appTotals(appCache);
70
+ appCounts();
71
+ appPanelState(touched);
72
+ appWrite();
73
+ appNotice('');
53
74
  }
54
75
 
55
- /* Панель перерисовывается целиком, поэтому поле, стоящее под клавиатурой, и
56
- * прокрутка после каждой пересборки возвращаются на своё место: иначе
57
- * переключение с Tab и Space требовало бы начинать обход панели заново, а
58
- * прокрутка — искать своё место заново. Место поля опознаётся порядковым номером —
59
- * порядок полей панели от данных не зависит. Фокус ставится без прокрутки (`preventScroll`):
60
- * он возвращает клавиатуру, а не двигает список. */
61
- function appRender(keepNotice) {
62
- const at = Array.from(document.querySelectorAll('#panel input')).indexOf(document.activeElement);
63
- const saved = appScrollTop();
76
+ /* One metric switched: a class on the table and the headings' `colSpan`. The totals do not move with a metric — they
77
+ * are sums over files and the metric's own field is the box the reader just clicked. */
78
+ export function appSwitchMetric() {
79
+ appMetrics(appCache);
80
+ appCounts();
81
+ appWrite();
82
+ appNotice('');
83
+ }
84
+
85
+ /* The first drawing: the choice is already in the view (the link and the memory are applied above), the panel is
86
+ * built to match it, the table is built once — every column of every file — and the view is painted over it. */
87
+ function appFirst() {
64
88
  appPanel();
65
- appScrollBack(saved);
66
- if (at >= 0) document.querySelectorAll('#panel input')[at].focus({ preventScroll: true });
67
- appTable();
68
- /* Сообщение о ссылке переживает отрисовку, которая сама же им и вызвана, и
69
- * гаснет от действия читателя: он его уже прочитал. */
70
- if (keepNotice !== true) appNotice('');
89
+ appCache = appTable(document.getElementById('grid'));
90
+ appNote();
91
+ appPaint();
71
92
  }
72
93
 
73
- /* Восстановление до первой отрисовки: у того, кто открыл страницу впервые,
74
- * разметка обязана быть умолчанием, а не чужим выбором. Ссылка старше памяти: это
75
- * явный выбор отправителя, и пока читатель ничего не менял, она его собственный
76
- * выбор не подменяетв память её запись не идёт. Отказ ссылки не пустая
77
- * таблица, а сообщение: читателю видно и что произошло, и что показано вместо. */
78
- const appStart = appLinkUse();
79
- if (appStart === 'ours') appTransient = true;
80
- else if (appStart === 'refused') appForeign = true;
81
- if (appStart !== 'ours') {
82
- const appSaved = appRead();
83
- if (appSaved !== null) appApply(appSaved);
94
+ /* The page's one asynchronous step, and why there is one. The block in the artifact is packed, and the platform's own
95
+ * unpacker answers with a promise, so the first drawing waits for it; everything after the first drawing is as
96
+ * synchronous as it was, and a click costs what it cost. A host that cannot unpack is told in words instead of being
97
+ * left with an empty table the reader would not know whether the report or the browser is at fault. */
98
+ let appBooted = false;
99
+
100
+ /* Restoring happens before the first drawing: for someone opening the page for the first time the view has to be the
101
+ * default rather than someone else's choice. A link outranks the memory: it is the sender's explicit choice, and
102
+ * while the reader has changed nothing it does not replace his own — writing it to the memory is what does not
103
+ * happen. A refused link is not an empty table but a message: the reader sees both what happened and what is shown
104
+ * instead. */
105
+ async function appBegin() {
106
+ try {
107
+ appBoot(await appUnpack(document.getElementById('data')));
108
+ } catch (_e) {
109
+ appNotice(appUi.unpack);
110
+ return;
111
+ }
112
+ const appStart = appLinkUse();
113
+ if (appStart === 'ours') appTransient = true;
114
+ else if (appStart === 'refused') appForeign = true;
115
+ if (appStart !== 'ours') {
116
+ const appSaved = appRead();
117
+ if (appSaved !== null) appApply(appSaved);
118
+ }
119
+ /* The folded tree is the onlooker's memory rather than the reader's choice: it comes back even when someone
120
+ * else's link is open (otherwise a link sent over would unfold the tree again on every visit). */
121
+ appFoldRead();
122
+ appFirst();
123
+ appBooted = true;
124
+ appStartup = false;
125
+ appForeign = false;
126
+ appTransient = false;
84
127
  }
85
- /* Сложенное дерево — память смотрящего, а не выбор читателя: она возвращается и
86
- * тогда, когда открыта чужая ссылка (иначе присланная ссылка разложила бы дерево
87
- * заново на каждом заходе). */
88
- appFoldRead();
89
- appRender(true);
90
- appStartup = false;
91
- appForeign = false;
92
- appTransient = false;
93
128
 
94
- /* Якорь сменился на открытой странице: выбор из нового адреса применяется тем же
95
- * кодом, что и при открытии. Свой собственный адрес такого события не поднимает
96
- * (`replaceState` его не вызывает), поэтому петли здесь нет. Отказ не трогает ни
97
- * вид — читатель продолжает смотреть то, что смотрел, — ни адрес: его прислали
98
- * читателю, и до первого его действия это не наше. */
129
+ /* Whoever opened the page and has to know when the first drawing is over waits for this promise — the checks do
130
+ * (`tools/page-harness.js`); the page itself has no use for it. */
131
+ window.appDrawn = appBegin();
132
+
133
+ /* The anchor changed on an open page: the choice in the new address is applied by the same code as at opening. The
134
+ * page's own address raises no such event (`replaceState` does not), so there is no loop here. A refusal touches
135
+ * neither the view — the reader keeps looking at what he looked at — nor the address: it was sent to the reader,
136
+ * and until he acts it is not ours. The message about the link stays: this drawing is exactly what it explains. */
99
137
  window.addEventListener('hashchange', () => {
138
+ /* The first drawing has not happened yet: the address the page was opened with is the business of that drawing,
139
+ * and a change that arrives before it has drawn nothing to replace. */
140
+ if (!appBooted) return;
141
+ /* An address that came in from outside is read first and a write this page was still holding is dropped: the reader
142
+ * has the address they were sent, not the one the previous click armed (see `appAddressDrop`). */
143
+ appAddressDrop();
100
144
  const state = appLinkUse();
101
145
  if (state === 'refused') appForeign = true;
102
146
  appTransient = state === 'ours';
103
- appRender(true);
147
+ appPaint();
104
148
  appForeign = false;
105
149
  appTransient = false;
106
150
  });