@vernikr/size-report 2.3.0 → 2.4.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/CHANGELOG.md +51 -0
- package/README.md +50 -20
- package/package.json +2 -2
- package/src/data.js +5 -1
- package/src/history.js +16 -1
- package/src/page/app.css +11 -4
- package/src/page/app.js +6 -0
- package/src/page/dom.js +14 -0
- package/src/page/panel.js +55 -31
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,57 @@
|
|
|
18
18
|
где за основу взяты `fixtures/synthetic/config.json`, метрики — `raw`, `min`, `tok`, а
|
|
19
19
|
способ минификации — `strip` или `esbuild`.
|
|
20
20
|
|
|
21
|
+
## 2.4.0 — 2026-09-15
|
|
22
|
+
|
|
23
|
+
Вид дерева и порядок колонок: чего в отчёте нет — после остальных и со снятой
|
|
24
|
+
галочкой, колонки последнего коммита — впереди, складка папки не считает числа.
|
|
25
|
+
|
|
26
|
+
- **Файл вне отчёта был подписью без галочки, а папка, где измерять нечего, —
|
|
27
|
+
без неё вовсе.** Один и тот же вопрос — «почему у этого нет числа» — читался
|
|
28
|
+
по-разному в двух строках подряд, а рядом с колонками такие строки выбивались из
|
|
29
|
+
ряда. Теперь у всего, что вне отчёта, галочка **стоит на месте, снята и
|
|
30
|
+
недоступна**: ряд строк ровный (глаз сравнивает одно с одним), а недоступность
|
|
31
|
+
говорит, что это не выбор читателя, — причина во всплывающей строке, как и
|
|
32
|
+
раньше. Заодно всё, чего в отчёте нет (и папки, и листья), идёт **после** того,
|
|
33
|
+
что в нём есть: в списке, где половина строк не переключается, отчёт виден сразу,
|
|
34
|
+
а не среди чужого.
|
|
35
|
+
- **Складывание папки считало числа, которых не меняет.** Клик по знаку пересобирал
|
|
36
|
+
панель и таблицу целиком — то есть платил за всю таблицу (в этом репозитории 97
|
|
37
|
+
строк × 136 колонок, 39 576 клеток) ради того, чтобы спрятать строки. Замер в
|
|
38
|
+
настоящем Chrome на этой же странице: **107 + 380 мс** перерисовки — столько же,
|
|
39
|
+
сколько стоит переключение одного файла. Стало **0,6 мс** в обработчике (и 6 мс на
|
|
40
|
+
перекладку страницы, которая прячет 42 строки): поддерево лежит в разметке и
|
|
41
|
+
прячется классом на строке, а клик меняет только то, что читатель и видит, —
|
|
42
|
+
класс, знак и запись в памяти.
|
|
43
|
+
- **Колонки, которых коснулся последний коммит, идут впереди остальных.** Отчёт
|
|
44
|
+
пересобирается после каждого коммита, и первый вопрос читателя — что принесла эта
|
|
45
|
+
правка. Знак приходит из истории (`last` в контракте `--data`), а не из чисел:
|
|
46
|
+
правка без изменения размера — тоже правка. Берётся последний коммит, задевший
|
|
47
|
+
хотя бы одну колонку: коммиты мимо колонок (и прежде всего сам отчёт, который
|
|
48
|
+
коммитит хук) пропускаются — иначе знак зависел бы от собственного коммита
|
|
49
|
+
отчёта, тот же прогон давал бы другие байты, и хук коммитил бы отчёт второй раз
|
|
50
|
+
на пустом месте. Внутри каждой части порядок прежний, из настроек.
|
|
51
|
+
|
|
52
|
+
### Что изменится в числах
|
|
53
|
+
|
|
54
|
+
**Ничего.** Числа считает тот же движок теми же датчиками: выпуск про то, что и в
|
|
55
|
+
каком порядке видно на странице, а не про измерение. Таблица та же, что у 2.3.0, до
|
|
56
|
+
последней клетки.
|
|
57
|
+
|
|
58
|
+
| Файл | raw | min со strip | min с esbuild | tok |
|
|
59
|
+
|---|---|---|---|---|
|
|
60
|
+
| code.js | 735 | 276 | 185 | 168 |
|
|
61
|
+
| modern.js | 246 | 51 | 45 | 44 |
|
|
62
|
+
| config.mjs | 172 | 45 | 40 | 40 |
|
|
63
|
+
| заметки.md | 306 | 303 | 303 | 53 |
|
|
64
|
+
| crlf.txt | 63 | 60 | 60 | 10 |
|
|
65
|
+
| package.json | 87 | 69 | 69 | 34 |
|
|
66
|
+
| style.css | 156 | 55 | 43 | 38 |
|
|
67
|
+
| table.toml | 300 | 299 | 299 | 52 |
|
|
68
|
+
| empty.js | 0 | 0 | 0 | 0 |
|
|
69
|
+
| WORKLOG.md | 446 | 439 | 439 | 101 |
|
|
70
|
+
| **ИТОГО** | **2511** | **1597** | **1483** | **540** |
|
|
71
|
+
|
|
21
72
|
## 2.3.0 — 2026-09-15
|
|
22
73
|
|
|
23
74
|
Отчёт описывает проект целиком: колонкой идёт каждый отслеживаемый файл, а дерево
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
## Статус
|
|
12
12
|
|
|
13
|
-
**Выпуск 2.
|
|
13
|
+
**Выпуск 2.4.0 (2026-09-15).** Инструмент живёт отдельным пакетом: имя в
|
|
14
14
|
реестре — `@vernikr/size-report` (публикуется тегом из CI, без секрета). Настроек
|
|
15
15
|
проект может не заводить вовсе: без файла инструмент выводит их из самого проекта и
|
|
16
16
|
говорит об этом строкой, а `--init` закрепляет выведенное файлом (чем этот шаг
|
|
@@ -22,10 +22,12 @@
|
|
|
22
22
|
в числах»:
|
|
23
23
|
таблица чисел в нём не пересказ, а замер на фикстуре, который сверяется с живым
|
|
24
24
|
прогоном (`test/changelog.test.js`). Числа на фикстуре этим выпуском не меняются —
|
|
25
|
-
правки в том, **что
|
|
26
|
-
отслеживаемый файл (а не выборка из двенадцати),
|
|
27
|
-
складываются
|
|
28
|
-
|
|
25
|
+
правки в том, **что и в каком порядке видно**: без файла настроек колонкой идёт
|
|
26
|
+
каждый отслеживаемый файл (а не выборка из двенадцати), в дереве панели
|
|
27
|
+
складываются папки, а всё, чего в отчёте нет, стоит после остальных и со снятой
|
|
28
|
+
галочкой; в таблице колонки, которых коснулся последний коммит, идут впереди.
|
|
29
|
+
`schema: 1` данных, замороженная 1.0.0, остаётся той же (выпуск добавил поле, а не
|
|
30
|
+
поменял смысл прежних).
|
|
29
31
|
|
|
30
32
|
**Шаг 1 плана пройден — перенос без изменения поведения.** Команда —
|
|
31
33
|
`bin/size.js`, точка входа пакета — `src/size-table.js` (только реэкспорт),
|
|
@@ -224,10 +226,10 @@
|
|
|
224
226
|
|
|
225
227
|
| Прогон | Команда | Проверок |
|
|
226
228
|
|---|---|---|
|
|
227
|
-
| Быстрый — каждая правка | `pnpm test` | **
|
|
228
|
-
| Полный — выкладка и CI | `pnpm test:all` | **
|
|
229
|
+
| Быстрый — каждая правка | `pnpm test` | **72 из 177** |
|
|
230
|
+
| Полный — выкладка и CI | `pnpm test:all` | **177** |
|
|
229
231
|
|
|
230
|
-
Ни одна проверка не потеряна и не ослаблена: полный прогон запускает все
|
|
232
|
+
Ни одна проверка не потеряна и не ослаблена: полный прогон запускает все 177 теми же
|
|
231
233
|
файлами, а быстрый берёт их часть. Умолчание — полный: файл становится быстрым только
|
|
232
234
|
явно и с причиной, поэтому новое дорогое не может тихо уехать в быстрый. Стерегут это
|
|
233
235
|
объявление `test/suites.test.js` (полнота классификации и причина у каждого файла) и
|
|
@@ -320,13 +322,19 @@ deльт задан один раз и по артефакту: рост зел
|
|
|
320
322
|
строки в `src/table.css` плюс пересборка эталона артефакта, больше цвета нигде нет.
|
|
321
323
|
|
|
322
324
|
**Левая панель страницы — дерево файлов проекта** (`REFACTOR.md` R-2.4, каталог
|
|
323
|
-
путей — `CHANGELOG.md` 2.2.0, складывание папок — 2.3.0
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
325
|
+
путей — `CHANGELOG.md` 2.2.0, складывание папок — 2.3.0, вид того, чего в отчёте
|
|
326
|
+
нет, — 2.4.0). Дерево строится по путям проекта, а не по одним
|
|
327
|
+
колонкам, поэтому в нём видно и то, что в отчёт не попало: у такого листа (и у
|
|
328
|
+
папки, где измерять нечего) галочка стоит на месте, но **снята и недоступна** —
|
|
329
|
+
включать нечего, — а причина в всплывающей строке («колонкой быть не может» —
|
|
330
|
+
правило пакета — или «в набор колонок не попал» — выбор проекта). Снятая, а не
|
|
331
|
+
убранная: ряд строк остаётся ровным (глаз сравнивает одно с одним), а недоступность
|
|
332
|
+
говорит, что это не выбор читателя. Заодно счётчик папки со смешанным
|
|
327
333
|
составом написан долей («2/5»: два файла в отчёте из пяти в папке). Сам отчёт в
|
|
328
334
|
дереве назван всегда: его отслеживаемость — свойство момента, и от неё содержимое
|
|
329
|
-
страницы не зависит.
|
|
335
|
+
страницы не зависит. **Всё, чего в отчёте нет, стоит после того, что в нём есть** —
|
|
336
|
+
и папки, и листья: в списке, где половина строк не переключается, отчёт должен
|
|
337
|
+
быть виден сразу, а не среди чужого (`test/page-tree.test.js`). У
|
|
330
338
|
папки три состояния — все её файлы включены, часть, ни одного, — и переключатель
|
|
331
339
|
папки ведёт за собой всё поддерево; рядом стоит число файлов. Своего состояния у
|
|
332
340
|
папки и у быстрой кнопки категории нет: обе переставляют галочки файлов, поэтому
|
|
@@ -341,6 +349,28 @@ deльт задан один раз и по артефакту: рост зел
|
|
|
341
349
|
панель растёт вместе со страницей. Дерево стало длинным (в этом репозитории 148
|
|
342
350
|
подписей), поэтому папки и складываются: иначе до его середины не добраться.
|
|
343
351
|
|
|
352
|
+
**Складывание папки — чистый вид, и оно не считает числа** (`CHANGELOG.md` 2.4.0).
|
|
353
|
+
Поддерево лежит в разметке и прячется классом на строке: клик по знаку меняет три
|
|
354
|
+
вещи, которые читатель и видит, — класс, знак и запись в памяти. Пересборка здесь
|
|
355
|
+
была бы честной работой впустую: она строит таблицу целиком (в этом репозитории —
|
|
356
|
+
97 строк × 136 колонок, 39 576 клеток), то есть платит за числа, которых складывание
|
|
357
|
+
не меняет, — и это было видно глазом как задержка. Замер в настоящем Chrome на этой
|
|
358
|
+
же странице: клик по знаку папки `src/` (42 строки поддерева) — **0,6 мс** в
|
|
359
|
+
обработчике и 6 мс на перекладку против **107 + 380 мс** перерисовки, которую он
|
|
360
|
+
вызывал раньше (столько же стоит переключение одного файла у той же страницы).
|
|
361
|
+
Стережёт это проверка о том, что после складывания таблица осталась той же самой
|
|
362
|
+
разметкой (`test/page-tree.test.js`), а не собранной заново.
|
|
363
|
+
|
|
364
|
+
**Колонки, которых коснулся последний коммит, идут впереди** (`CHANGELOG.md` 2.4.0).
|
|
365
|
+
Отчёт пересобирается после каждого коммита, и первый вопрос читателя — что принесла
|
|
366
|
+
эта правка. Знак приходит из истории, а не из чисел: правка без изменения размера —
|
|
367
|
+
тоже правка. Берётся последний коммит, задевший хотя бы одну колонку, — считая от
|
|
368
|
+
верхушки назад: коммиты мимо колонок (и, прежде всего, сам отчёт, который коммитит
|
|
369
|
+
хук) пропускаются, — иначе знак зависел бы от собственного коммита отчёта, а тот же
|
|
370
|
+
прогон давал бы другие байты. Внутри каждой части порядок прежний, из настроек
|
|
371
|
+
(`sort` устойчив): порядок колонок — то, к чему читатель привык, и своим выбором
|
|
372
|
+
файлов он его не переставляет (`test/page-view.test.js`, поле `last` контракта).
|
|
373
|
+
|
|
344
374
|
**На широком экране панель стоит слева от таблицы и не вытесняет числа**
|
|
345
375
|
(от 900 px, `src/page/app.css`). Это не украшение: на десктопе бокового места
|
|
346
376
|
много, а вертикального мало — переключатели, дерево и числа видны одновременно, и
|
|
@@ -569,7 +599,7 @@ deльт задан один раз и по артефакту: рост зел
|
|
|
569
599
|
| `src/page/app.css` | Оформление страницы сверх общей части: панель с деревом файлов и липкой строкой категорий (на широком экране — колонка слева, страница в окно), состояния пустоты, узкое окно |
|
|
570
600
|
| `src/page/state.js` | Состояние страницы: данные отчёта, вид галочек, указатель «какой путь — какая колонка», сложенные папки, паспорт записи, память браузера и обмен ссылкой — глава программы страницы |
|
|
571
601
|
| `src/page/dom.js` | Узлы страницы: мелкие помощники разметки (`appEl`, `appBox`) — одни на панель и таблицу |
|
|
572
|
-
| `src/page/panel.js` | Панель выбора: галочки метрик и файлов, категории, дерево путей проекта (файлы вне отчёта —
|
|
602
|
+
| `src/page/panel.js` | Панель выбора: галочки метрик и файлов, категории, дерево путей проекта (файлы вне отчёта — снятой галочкой с причиной, после остальных; папки — со знаком складывания, который прячет поддерево классом, а не пересборкой); перерисовку просит у главы сборки |
|
|
573
603
|
| `src/page/table.js` | Таблица страницы: клетка, подпись коммита, шапка и состояния пустоты — разметка поверх общего расчёта |
|
|
574
604
|
| `src/page/app.js` | Сборка и запуск страницы: таблица целиком, перерисовка по выбору читателя (с возвратом фокуса и прокрутки), первая отрисовка и смена якоря; вклеивается в собранную страницу
|
|
575
605
|
| `src/page/build.js` | Сборка страницы: данные, оформление и программа в одном файле без внешних ссылок |
|
|
@@ -584,7 +614,7 @@ deльт задан один раз и по артефакту: рост зел
|
|
|
584
614
|
| `src/minify.js` | Настоящий минификатор: необязательная зависимость, загружается один раз и не роняет прогон, если её нет |
|
|
585
615
|
| `src/tokens.js` | Токены: словарь по семейству и кодировке, оценка по длине как запасной счёт, форматы без текста |
|
|
586
616
|
| `src/optional.js` | Общее устройство необязательных зависимостей (минификатор и словарь): ленивая загрузка, версия пакета, шов отсутствия |
|
|
587
|
-
| `src/history.js` | Обход истории: измерение по коммитам, сдвиг чисел, сборка, сверка с
|
|
617
|
+
| `src/history.js` | Обход истории: измерение по коммитам, сдвиг чисел, сборка, сверка с деревом, знак «какой колонки коснулся последний коммит» и причина пропуска у каждого выпавшего коммита |
|
|
588
618
|
| `src/check.js` | Полнота покрытия (`size check`): настройки, история, пути, датчики — что прошло мимо колонок и чем это чинится |
|
|
589
619
|
| `src/explain.js` | Объяснение пропущенной строки (`size explain <коммит>`): причина, улики и готовая починка |
|
|
590
620
|
| `src/doctor.js` | Диагностика одним ответом (`size doctor`): окружение, зависимости, настройки, покрытие, состояние хука — сборкой из существующих кусков |
|
|
@@ -620,7 +650,7 @@ deльт задан один раз и по артефакту: рост зел
|
|
|
620
650
|
| `test/contract-data.test.js` | Контракт данных: числа против эталона, состав полей против производных, пометки приближения против подписи метрики |
|
|
621
651
|
| `test/contract-derived.test.js` | Производные против чисел артефакта: итоги строки, дельты клетки и дельта итога — на коде, который лежит в дереве |
|
|
622
652
|
| `test/page-view.test.js` | Собранная страница: вклейка без копий расчёта, самодостаточность, состояния пустоты, оформление и переключатели |
|
|
623
|
-
| `test/page-tree.test.js` | Дерево файлов панели: папки по путям проекта, три состояния, поддерево, файлы вне отчёта и прокрутка при пересборке |
|
|
653
|
+
| `test/page-tree.test.js` | Дерево файлов панели: папки по путям проекта, три состояния, поддерево, файлы и папки вне отчёта (снятая галочка и место после остальных), складывание без пересборки и прокрутка при пересборке |
|
|
624
654
|
| `test/page-choice.test.js` | Память выбора и обмен ссылкой: перезаход, чужой отчёт, чужая и битая запись, смена адреса на открытой странице |
|
|
625
655
|
| `test/module.test.js` | Модуль в расширении `.js`: измеряется без правок настроек; гард стриппера жив (доказано мутацией) и не обвиняет невиновного |
|
|
626
656
|
| `test/guard.test.js` | Разбор модуля: идёт потоком, оба пути дают один вердикт, отступление работает без файла потока, сотни разборов дешевле запуска |
|
|
@@ -692,7 +722,7 @@ pnpm add -D @vernikr/size-report
|
|
|
692
722
|
```
|
|
693
723
|
|
|
694
724
|
Пакет **опубликован в реестре**, и публично: `npm view @vernikr/size-report
|
|
695
|
-
version` отвечает `2.
|
|
725
|
+
version` отвечает `2.4.0`, `npm access get status @vernikr/size-report` — `public`,
|
|
696
726
|
а анонимный запрос тарболла — код 200. `npm i -D` и `yarn add -D` принимают то же
|
|
697
727
|
имя; ни ключа, ни ссылки на репозиторий не нужно.
|
|
698
728
|
|
|
@@ -700,11 +730,11 @@ version` отвечает `2.3.0`, `npm access get status @vernikr/size-report`
|
|
|
700
730
|
реестра, но остаётся привязанной к ревизии:
|
|
701
731
|
|
|
702
732
|
```bash
|
|
703
|
-
pnpm add -D github:vernikr/size-report#v2.
|
|
733
|
+
pnpm add -D github:vernikr/size-report#v2.4.0
|
|
704
734
|
```
|
|
705
735
|
|
|
706
736
|
Без сети (или если тянуть из codeload нечем) — тарболл: `pnpm pack` в клоне
|
|
707
|
-
пакета, затем `pnpm add -D ./vernikr-size-report-2.
|
|
737
|
+
пакета, затем `pnpm add -D ./vernikr-size-report-2.4.0.tgz`.
|
|
708
738
|
|
|
709
739
|
**Почему тег, а не sha.** Короткий sha pnpm разрешает только через видимые рефы, а
|
|
710
740
|
`git ls-remote` отдаёт одни верхушки веток: пока ревизия — верхушка, короткий sha
|
|
@@ -712,7 +742,7 @@ pnpm add -D github:vernikr/size-report#v2.3.0
|
|
|
712
742
|
<sha> to a commit`. Это не рассуждение, а проба: короткий пин `6530237` ставился,
|
|
713
743
|
пока `main` стоял на нём, и перестал — на следующем же коммите, а тот же sha
|
|
714
744
|
целиком поставился. Имя ветки (`#main`) или тег принимаются оба, но ветка —
|
|
715
|
-
движущаяся цель, а тег постоянен: этот выпуск стоит на теге `v2.
|
|
745
|
+
движущаяся цель, а тег постоянен: этот выпуск стоит на теге `v2.4.0`, он же и в
|
|
716
746
|
примере (сорок знаков тоже годятся, но их придётся брать глазами из истории).
|
|
717
747
|
|
|
718
748
|
Ревизия в примере — не украшение, а часть утверждения: она закреплена за тем, что
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vernikr/size-report",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"author": "vernikr",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"report"
|
|
67
67
|
],
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@vernikr/size-report": "2.
|
|
69
|
+
"@vernikr/size-report": "2.3.0",
|
|
70
70
|
"c8": "10",
|
|
71
71
|
"dependency-cruiser": "17",
|
|
72
72
|
"eslint": "^9.18.0",
|
package/src/data.js
CHANGED
|
@@ -39,7 +39,7 @@ export function categoryOf(col) {
|
|
|
39
39
|
* вопроса, на который пригодилось бы поле («почему у коммита нет строки» задают
|
|
40
40
|
* командой `explain`, и там причина уже разложена). */
|
|
41
41
|
export function reportData(cfg, root) {
|
|
42
|
-
const { rows, state, dropped } = build(cfg, root);
|
|
42
|
+
const { rows, state, dropped, last } = build(cfg, root);
|
|
43
43
|
const loc = LOCALES[cfg.locale];
|
|
44
44
|
const files = cfg.columns.map((col, i) => {
|
|
45
45
|
const cat = categoryOf(col);
|
|
@@ -74,6 +74,10 @@ export function reportData(cfg, root) {
|
|
|
74
74
|
rows: rows.map((r) => Object.assign(rowShape(r),
|
|
75
75
|
{ href: rowHref(r.section, r.sha, cfg), values: r.cells })),
|
|
76
76
|
now: state.map((s) => (s === null ? null : s.cells)),
|
|
77
|
+
/* Какие колонки тронул последний коммит: страница по этому знаку ставит их
|
|
78
|
+
* впереди — читателю нужнее то, что изменилось только что. Это факт из истории,
|
|
79
|
+
* а не производная величина: она не считает, а говорит, чего коснулась правка. */
|
|
80
|
+
last: last,
|
|
77
81
|
approx: approxMarks(rows, state, cfg),
|
|
78
82
|
skipped: dropped.map(skipLine)
|
|
79
83
|
};
|
package/src/history.js
CHANGED
|
@@ -158,7 +158,22 @@ export function measureHistory(cfg, root, known) {
|
|
|
158
158
|
journalPrev: ''
|
|
159
159
|
};
|
|
160
160
|
commits.forEach((c, ci) => stepCommit(pass, c, ci));
|
|
161
|
-
|
|
161
|
+
/* Какие колонки тронул последний коммит — по тому же плану чтения, по которому
|
|
162
|
+
* идёт перенос состояния: путь колонки есть в списке изменённых путей коммита.
|
|
163
|
+
* Страница ставит эти колонки впереди остальных: отчёт пересобирается после
|
|
164
|
+
* каждого коммита, и первый вопрос читателя — что принесла эта правка.
|
|
165
|
+
*
|
|
166
|
+
* Берётся последний коммит, задевший хотя бы одну колонку, — считая от верхушки
|
|
167
|
+
* назад. Коммиты мимо колонок (и, прежде всего, сам отчёт, который коммитит хук)
|
|
168
|
+
* пропускаются: правка отчёта — не правка проекта. Иначе знак зависел бы от
|
|
169
|
+
* собственного коммита отчёта: тот же прогон давал бы другие байты, отчёт
|
|
170
|
+
* перестал бы быть неподвижной точкой, а хук коммитил бы его по второму разу. */
|
|
171
|
+
let last = cfg.columns.map(() => false);
|
|
172
|
+
for (let i = commits.length - 1; i >= 0 && !last.some(Boolean); i--) {
|
|
173
|
+
const picks = reads.plan[i].picks.map((paths) => paths.length > 0);
|
|
174
|
+
if (picks.some(Boolean)) last = picks;
|
|
175
|
+
}
|
|
176
|
+
return { rows: pass.rows, dropped: pass.dropped, mixed: pass.mixed, state: pass.state, last: last };
|
|
162
177
|
}
|
|
163
178
|
|
|
164
179
|
/* Сверка с рабочим деревом отвечает на два вопроса, и оба обязательны: состояние
|
package/src/page/app.css
CHANGED
|
@@ -77,9 +77,14 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
77
77
|
.box.dir { font-weight: 600; }
|
|
78
78
|
.box .n { margin-left: 1px; color: var(--muted); font-size: 11px; }
|
|
79
79
|
|
|
80
|
-
/*
|
|
81
|
-
*
|
|
80
|
+
/* Сложенная папка (класс на строке, ставится кликом по знаку): поддерево лежит в
|
|
81
|
+
* разметке и просто не показывается. Так складывание ничего не пересобирает —
|
|
82
|
+
* иначе каждый клик считал бы таблицу заново.
|
|
83
|
+
*
|
|
84
|
+
* Знак складки: место под него есть у каждой строки (отступ строки), а сам он
|
|
85
|
+
* только у папок — так листья и папки выстроены в один столбец. Он не часть
|
|
82
86
|
* галочки: галочка отвечает за числа, знак — за то, сколько дерева видно. */
|
|
87
|
+
.panel .tree li.folded > .tree { display: none; }
|
|
83
88
|
.panel .tree li { position: relative; }
|
|
84
89
|
.panel .tree li > .fold {
|
|
85
90
|
position: absolute;
|
|
@@ -106,10 +111,12 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
106
111
|
.box:hover { background: var(--tint); }
|
|
107
112
|
.box input { margin: 0; accent-color: AccentColor; }
|
|
108
113
|
.box.all { font-weight: 600; }
|
|
109
|
-
/* Файл вне отчёта:
|
|
110
|
-
*
|
|
114
|
+
/* Файл или папка вне отчёта: галочка на месте, но снята и недоступна — чисел для
|
|
115
|
+
* него не измеряли, и переключать нечего. Строка приглушена и на мышь не
|
|
116
|
+
* отзывается: обещать нажатие нечем. */
|
|
111
117
|
.box.plain { cursor: default; opacity: .55; }
|
|
112
118
|
.box.plain:hover { background: none; }
|
|
119
|
+
.box.plain input { cursor: default; }
|
|
113
120
|
|
|
114
121
|
/* Расшифровки под деревом файлов нет намеренно: под списком она отодвигала
|
|
115
122
|
* числа, а её содержимое и так стоит рядом с тем, что объясняет, — цвет дельт
|
package/src/page/app.js
CHANGED
|
@@ -11,6 +11,12 @@ function appTable() {
|
|
|
11
11
|
const on = appView.files;
|
|
12
12
|
const files = [];
|
|
13
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));
|
|
14
20
|
|
|
15
21
|
const table = document.getElementById('grid');
|
|
16
22
|
table.textContent = '';
|
package/src/page/dom.js
CHANGED
|
@@ -20,3 +20,17 @@ export function appBox(label, title, checked, onChange, cls) {
|
|
|
20
20
|
box.appendChild(appEl('span', null, label));
|
|
21
21
|
return box;
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
/* Галочка, которую нечем переключить: место в дереве есть, а включать нечего —
|
|
25
|
+
* файла нет в колонках, отчёт его не измеряет. Она снята и недоступна: так строка
|
|
26
|
+
* выглядит как все прочие (глаз сравнивает одно с одним), но видно, что это не
|
|
27
|
+
* «выключено читателем», а «не измеряется». Причина — во всплывающей строке. */
|
|
28
|
+
export function appOffBox(label, title, cls) {
|
|
29
|
+
const box = appBox(label, title, false, null, cls);
|
|
30
|
+
box.classList.add('plain');
|
|
31
|
+
box.querySelector('input').disabled = true;
|
|
32
|
+
/* Подсказка — на всей строке, а не только на поле: у недоступного поля браузер её
|
|
33
|
+
* не показывает, а причина читателю нужна именно здесь. */
|
|
34
|
+
box.title = title;
|
|
35
|
+
return box;
|
|
36
|
+
}
|
package/src/page/panel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appEl, appBox } from './dom.js';
|
|
1
|
+
import { appEl, appBox, appOffBox } from './dom.js';
|
|
2
2
|
import { appData, appUi, appView, appFileAt, appFoldSet, appMeasured } from './state.js';
|
|
3
3
|
|
|
4
4
|
/* Галочку файла ставит только файл: и категория, и папка в дереве — способы
|
|
@@ -14,14 +14,12 @@ function appFileBox(i) {
|
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/* Файл, которого в отчёте нет: он стоит в дереве на своём месте, но
|
|
18
|
-
* него
|
|
19
|
-
* видит во всплывающей строке, а не догадывается по виду. */
|
|
17
|
+
/* Файл, которого в отчёте нет: он стоит в дереве на своём месте, но галочка у
|
|
18
|
+
* него снята и недоступна — чисел для него не измеряли, и переключать нечего.
|
|
19
|
+
* Причину читатель видит во всплывающей строке, а не догадывается по виду. */
|
|
20
20
|
function appUnmeasuredBox(entry) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
+ (entry.why === 'rule' ? appUi.notMeasuredRule : appUi.notMeasuredChoice);
|
|
24
|
-
return box;
|
|
21
|
+
return appOffBox(entry.path.split('/').pop(), entry.path + ' · '
|
|
22
|
+
+ (entry.why === 'rule' ? appUi.notMeasuredRule : appUi.notMeasuredChoice));
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
/* Все измеряемые файлы поддерева — то, чем управляет переключатель папки: файл
|
|
@@ -48,15 +46,14 @@ function appNode() {
|
|
|
48
46
|
* состояния — все файлы включены, часть, ни одного. Рядом число файлов; если в
|
|
49
47
|
* папке есть и те, что вне отчёта, оно написано долей («2/5»): читателю важно, что
|
|
50
48
|
* в папке пять файлов, а измеряются два. Папка без единого измеряемого файла
|
|
51
|
-
*
|
|
49
|
+
* остаётся на месте, но её галочка снята и недоступна: включать в ней нечего. */
|
|
52
50
|
function appDirHead(name, sub) {
|
|
53
51
|
const idx = appIndexes(sub);
|
|
54
52
|
const total = appCount(sub);
|
|
55
53
|
const label = name + '/';
|
|
56
54
|
let head;
|
|
57
55
|
if (idx.length === 0) {
|
|
58
|
-
head =
|
|
59
|
-
head.title = appUi.dirNone.replace('{name}', name).replace('{n}', total);
|
|
56
|
+
head = appOffBox(label, appUi.dirNone.replace('{name}', name).replace('{n}', total), 'dir');
|
|
60
57
|
} else {
|
|
61
58
|
const on = idx.map((i) => appView.files[i]);
|
|
62
59
|
const every = on.every((v) => v);
|
|
@@ -76,12 +73,25 @@ function appDirHead(name, sub) {
|
|
|
76
73
|
* два разных действия означала бы, что сложить папку можно только вместе с
|
|
77
74
|
* включением её файлов. Знак нарисован спаном, а не кнопкой, потому что в строке
|
|
78
75
|
* папки рядом уже стоит метка-галочка, а вложенная в метку кнопка поднимала бы её
|
|
79
|
-
* же нажатие.
|
|
76
|
+
* же нажатие.
|
|
77
|
+
*
|
|
78
|
+
* Клик по знаку ничего не пересобирает: поддерево лежит в разметке, а прячет его
|
|
79
|
+
* класс на строке. Пересборка здесь была бы честной работой впустую — она считает
|
|
80
|
+
* таблицу целиком (в этом проекте — сотни строк на полторы сотни колонок), то есть
|
|
81
|
+
* платит за числа, которых складывание не меняет. Поэтому меняются только три
|
|
82
|
+
* вещи, которые читатель и видит: класс, знак и запись в памяти. */
|
|
80
83
|
function appFoldBox(name, path) {
|
|
81
84
|
const folded = appView.folded[path] === true;
|
|
82
85
|
const box = appEl('span', 'fold', folded ? '▸' : '▾');
|
|
83
86
|
box.title = (folded ? appUi.foldOpen : appUi.foldClose).replace('{name}', name);
|
|
84
|
-
box.addEventListener('click', () => {
|
|
87
|
+
box.addEventListener('click', () => {
|
|
88
|
+
const now = !(appView.folded[path] === true);
|
|
89
|
+
appFoldSet(path, now);
|
|
90
|
+
const li = box.closest('li');
|
|
91
|
+
if (li !== null) li.classList.toggle('folded', now);
|
|
92
|
+
box.textContent = now ? '▸' : '▾';
|
|
93
|
+
box.title = (now ? appUi.foldOpen : appUi.foldClose).replace('{name}', name);
|
|
94
|
+
});
|
|
85
95
|
return box;
|
|
86
96
|
}
|
|
87
97
|
|
|
@@ -95,26 +105,40 @@ function appLeaves(node) {
|
|
|
95
105
|
return items.sort((a, b) => (a.name < b.name ? -1 : (a.name > b.name ? 1 : 0)));
|
|
96
106
|
}
|
|
97
107
|
|
|
98
|
-
/*
|
|
99
|
-
*
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
/* Строка папки: знак складывания, галочка с числом файлов и поддерево. Сложенная
|
|
109
|
+
* папка отличается только классом — разметка одна и та же. */
|
|
110
|
+
function appDir(name, sub, prefix) {
|
|
111
|
+
const here = prefix === '' ? name : prefix + '/' + name;
|
|
112
|
+
const folded = appView.folded[here] === true;
|
|
113
|
+
const li = appEl('li', folded ? 'folded' : null);
|
|
114
|
+
li.appendChild(appFoldBox(name, here));
|
|
115
|
+
li.appendChild(appDirHead(name, sub));
|
|
116
|
+
li.appendChild(appTreeList(sub, here));
|
|
117
|
+
return li;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Строка листа: измеряемый файл — с галочкой, файл вне отчёта — со снятой.
|
|
121
|
+
function appLeaf(leaf) {
|
|
122
|
+
const li = appEl('li');
|
|
123
|
+
li.appendChild(leaf.entry === null ? appFileBox(leaf.i) : appUnmeasuredBox(leaf.entry));
|
|
124
|
+
return li;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Узлы одного уровня: сперва всё, что в отчёте (папки по алфавиту, затем листья —
|
|
128
|
+
* их порядок из `appLeaves`), потом то, чего в отчёте нет: папки без измеряемых
|
|
129
|
+
* файлов и файлы вне колонок. Так решено не из вкуса: у всего вне отчёта галочка
|
|
130
|
+
* снята и недоступна, и в конце списка оно не отвлекает от того, что в таблице,
|
|
131
|
+
* а найти его по-прежнему можно — оно там же, где было. */
|
|
102
132
|
function appTreeList(node, prefix) {
|
|
103
133
|
const list = appEl('ul', 'tree');
|
|
104
|
-
[...node.dirs.keys()].sort()
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
113
|
-
appLeaves(node).forEach((leaf) => {
|
|
114
|
-
const li = appEl('li');
|
|
115
|
-
li.appendChild(leaf.entry === null ? appFileBox(leaf.i) : appUnmeasuredBox(leaf.entry));
|
|
116
|
-
list.appendChild(li);
|
|
117
|
-
});
|
|
134
|
+
const dirs = [...node.dirs.keys()].sort()
|
|
135
|
+
.map((name) => ({ name: name, sub: node.dirs.get(name), inReport: appIndexes(node.dirs.get(name)).length > 0 }));
|
|
136
|
+
const leaves = appLeaves(node);
|
|
137
|
+
const inside = leaves.filter((leaf) => leaf.entry === null);
|
|
138
|
+
dirs.filter((d) => d.inReport).forEach((d) => list.appendChild(appDir(d.name, d.sub, prefix)));
|
|
139
|
+
inside.forEach((leaf) => list.appendChild(appLeaf(leaf)));
|
|
140
|
+
dirs.filter((d) => !d.inReport).forEach((d) => list.appendChild(appDir(d.name, d.sub, prefix)));
|
|
141
|
+
leaves.filter((leaf) => leaf.entry !== null).forEach((leaf) => list.appendChild(appLeaf(leaf)));
|
|
118
142
|
return list;
|
|
119
143
|
}
|
|
120
144
|
|