@vernikr/size-report 2.3.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/README.md +929 -1051
- package/bin/postinstall.js +17 -18
- package/bin/size.js +2 -2
- package/package.json +3 -4
- package/src/args.js +72 -72
- package/src/artifact.js +14 -14
- package/src/check.js +41 -42
- package/src/cli.js +26 -29
- package/src/config.js +87 -91
- package/src/css.js +14 -14
- package/src/data.js +33 -35
- package/src/derived.js +31 -35
- package/src/doctor.js +95 -99
- package/src/explain.js +46 -47
- package/src/git.js +66 -71
- package/src/history.js +81 -69
- package/src/hook.js +130 -149
- package/src/init.js +36 -37
- package/src/journal.js +17 -15
- package/src/locales.js +25 -16
- package/src/metrics.js +56 -55
- package/src/minify.js +28 -27
- package/src/modes.js +57 -60
- package/src/optional.js +13 -11
- package/src/page/app.css +78 -87
- package/src/page/app.js +31 -30
- package/src/page/build.js +40 -39
- package/src/page/dom.js +16 -3
- package/src/page/panel.js +85 -64
- package/src/page/state.js +72 -87
- package/src/page/table.js +21 -24
- package/src/parse-worker.js +10 -10
- package/src/parse.js +43 -45
- package/src/project.js +100 -104
- package/src/refusal.js +75 -76
- package/src/size-table.js +41 -76
- package/src/strip/forms.js +5 -5
- package/src/strip/guard.js +27 -28
- package/src/strip/js.js +27 -27
- package/src/strip.js +18 -21
- package/src/table.css +13 -14
- package/src/tokens.js +28 -27
- package/src/tool.js +10 -11
- package/templates/README.md +71 -77
- package/templates/ci.yml +33 -33
- package/templates/size-report.config.json +3 -3
- package/CHANGELOG.md +0 -639
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
|
-
/*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
*
|
|
35
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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('✗
|
|
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('✗
|
|
70
|
-
console.error('
|
|
71
|
-
console.error('
|
|
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('
|
|
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('
|
|
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
|
-
/*
|
|
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 + '
|
|
94
|
-
+ kmb(byteLen(out.html)) + ' (
|
|
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('
|
|
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('✓
|
|
106
|
-
+ '
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
122
|
-
* `checkMode`
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
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
|
-
/*
|
|
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 (settings → history → coverage →
|
|
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
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
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
|
-
/*
|
|
156
|
-
*
|
|
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
|
-
/*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
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
|
-
*
|
|
6
|
-
* —
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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: '
|
|
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
|
-
/*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
/*
|
|
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,9 +71,13 @@ 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.
|
|
76
|
+
*
|
|
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. */
|
|
80
|
+
.panel .tree li.folded > .tree { display: none; }
|
|
83
81
|
.panel .tree li { position: relative; }
|
|
84
82
|
.panel .tree li > .fold {
|
|
85
83
|
position: absolute;
|
|
@@ -92,9 +90,8 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
92
90
|
}
|
|
93
91
|
.panel .tree li > .fold:hover { color: var(--ink); }
|
|
94
92
|
|
|
95
|
-
/*
|
|
96
|
-
*
|
|
97
|
-
* технологии. */
|
|
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. */
|
|
98
95
|
.box {
|
|
99
96
|
display: inline-flex;
|
|
100
97
|
gap: 6px;
|
|
@@ -106,23 +103,24 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
106
103
|
.box:hover { background: var(--tint); }
|
|
107
104
|
.box input { margin: 0; accent-color: AccentColor; }
|
|
108
105
|
.box.all { font-weight: 600; }
|
|
109
|
-
/*
|
|
110
|
-
*
|
|
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. */
|
|
111
109
|
.box.plain { cursor: default; opacity: .55; }
|
|
112
110
|
.box.plain:hover { background: none; }
|
|
111
|
+
.box.plain input { cursor: default; }
|
|
113
112
|
|
|
114
|
-
/*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* «файла ещё нет» — в подсказке клетки.
|
|
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.
|
|
118
116
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
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. */
|
|
121
119
|
#grid td.approx { text-decoration: underline dotted; text-underline-offset: 2.5px; }
|
|
122
120
|
|
|
123
|
-
/*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
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. */
|
|
126
124
|
.shell {
|
|
127
125
|
overflow: auto;
|
|
128
126
|
max-height: calc(100vh - 300px);
|
|
@@ -130,12 +128,12 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
130
128
|
border-radius: var(--radius);
|
|
131
129
|
background: Canvas;
|
|
132
130
|
}
|
|
133
|
-
/*
|
|
131
|
+
/* Numbers are denser than the page's text: there are more of them, they are shorter, and they are read by their digits. */
|
|
134
132
|
#grid { font-size: 12.5px; }
|
|
135
133
|
|
|
136
|
-
/*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
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. */
|
|
139
137
|
.notice {
|
|
140
138
|
margin: 0 0 var(--gap);
|
|
141
139
|
padding: 9px 13px;
|
|
@@ -146,8 +144,8 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
146
144
|
}
|
|
147
145
|
.notice[hidden] { display: none; }
|
|
148
146
|
|
|
149
|
-
/*
|
|
150
|
-
*
|
|
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. */
|
|
151
149
|
.state {
|
|
152
150
|
margin: var(--gap) 0 0;
|
|
153
151
|
padding: 11px 13px;
|
|
@@ -161,28 +159,24 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
161
159
|
.note { margin: 14px 0 0; max-width: 90em; color: var(--muted); font-size: 12px; }
|
|
162
160
|
.note code { background: var(--tint); padding: 0 3px; border-radius: 3px; }
|
|
163
161
|
|
|
164
|
-
/*
|
|
165
|
-
*
|
|
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. */
|
|
166
164
|
:focus-visible { outline: 2px solid AccentColor; outline-offset: 2px; border-radius: 3px; }
|
|
167
165
|
|
|
168
|
-
/*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* управление за экран. Узкое окно эту же раскладку снимает (ниже): там столбцы
|
|
173
|
-
* снова идут друг под другом, потому что рядом им не хватает ширины.
|
|
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.
|
|
174
170
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* получилось» (тогда она вытолкнула бы таблицу за экран), а та же рабочая строка,
|
|
182
|
-
* внутри которой она прокручивается: список файлов длиннее окна — обычное дело.
|
|
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.
|
|
183
177
|
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
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. */
|
|
186
180
|
@media (min-width: 900px) {
|
|
187
181
|
body {
|
|
188
182
|
box-sizing: border-box;
|
|
@@ -201,22 +195,20 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
201
195
|
#shell { grid-area: 3 / 2 / 4 / 3; }
|
|
202
196
|
#state { grid-area: 4 / 2 / 5 / 3; align-self: start; }
|
|
203
197
|
#note { grid-area: 5 / 2 / 6 / 3; }
|
|
204
|
-
/*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* поля, и уезжает вместе с ним. */
|
|
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. */
|
|
209
202
|
#panel { grid-area: 2 / 1 / -1 / 2; min-height: 0; margin-bottom: 0; padding-top: 0; overflow: auto; }
|
|
210
203
|
.panel > fieldset:first-child { padding-top: 12px; }
|
|
211
|
-
/*
|
|
212
|
-
*
|
|
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. */
|
|
213
206
|
.shell { max-height: none; }
|
|
214
|
-
/*
|
|
215
|
-
*
|
|
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. */
|
|
216
209
|
.panel .files { max-height: none; overflow: visible; }
|
|
217
|
-
/*
|
|
218
|
-
*
|
|
219
|
-
* проезжающие строки списка. */
|
|
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. */
|
|
220
212
|
.panel .cats {
|
|
221
213
|
position: sticky;
|
|
222
214
|
top: 0;
|
|
@@ -224,16 +216,15 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
224
216
|
background-color: Canvas;
|
|
225
217
|
background-image: linear-gradient(var(--tint), var(--tint));
|
|
226
218
|
}
|
|
227
|
-
/*
|
|
228
|
-
*
|
|
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. */
|
|
229
221
|
body:has(#shell[hidden]) #state { grid-area: 3 / 2 / 4 / 3; }
|
|
230
222
|
}
|
|
231
223
|
|
|
232
|
-
/*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* под ней на одном единственном размере окна. */
|
|
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. */
|
|
237
228
|
@media (max-width: 899px) {
|
|
238
229
|
body { padding: 14px 14px 32px; }
|
|
239
230
|
h1 { font-size: 18px; }
|