@vernikr/size-report 2.8.0 → 2.8.2
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 +71 -33
- package/package.json +2 -2
- package/src/page/app.css +11 -12
- package/src/page/app.js +23 -29
- package/src/page/build.js +8 -8
- package/src/page/panel.js +34 -45
- package/src/page/payload.js +4 -5
- package/src/page/state.js +48 -63
- package/src/page/table.js +49 -18
- package/src/table.css +60 -46
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ shows.
|
|
|
10
10
|
|
|
11
11
|
## Status
|
|
12
12
|
|
|
13
|
-
**Release 2.8.
|
|
13
|
+
**Release 2.8.2 (2026-09-18).** The tool lives as a package of its own: the registry name is
|
|
14
14
|
`@vernikr/size-report` (published by tag from CI, with no secret). A project may keep no settings at
|
|
15
15
|
all: without a config file the tool derives them from the project itself and says so in one line,
|
|
16
16
|
and `--init` pins what was derived into a file. The report is **one file**, the self-contained page
|
|
@@ -19,6 +19,41 @@ is installed and on the first run. The version is in the manifest, and every rel
|
|
|
19
19
|
the journal — `worklog/` for today's entries, `worklog/archive/WORKLOG.md` for the earlier ones:
|
|
20
20
|
what changes in the numbers is measured rather than retold.
|
|
21
21
|
|
|
22
|
+
2.8.2 is about the page's own sources and the checks that guard them, and its figures are measured rather than retold.
|
|
23
|
+
**The four page suites that went quiet with the `<table>` are alive again against the window:** `test/page-view.test.js`,
|
|
24
|
+
`page-cols`, `page-tree` and `page-choice` read the grid and the panel they guard rather than markup that no longer
|
|
25
|
+
exists, and what they promise is what they hold — the pasted program with no second calculation, the sparse block, the
|
|
26
|
+
recount by the choice, the order of the columns, the tree's folds and its memory, the link's refusals. **Their readers
|
|
27
|
+
are no longer each suite's own:** `tools/page-harness.js` keeps one set of them (a file's path at HEAD, the rows the
|
|
28
|
+
window built, the numbers of a row and its captions, a folder's and a category's switch, the place the shell stands at),
|
|
29
|
+
and the copies the suites carried left with it. **And the page's chapters shed the duplication the same request
|
|
30
|
+
found:** the choice's record and the folded tree share one reading and one writing of the browser's memory
|
|
31
|
+
(`appLoad`/`appKeep`), a folder's two figures come from one walk of its subtree (`appSub`), the three walks over the
|
|
32
|
+
metrics and the files of a record became the panels' one question asked twice (`appReach`), and the three paths of a
|
|
33
|
+
click became one drawing (`appChanged`) — while two rules that never took effect went with them (a dead `.cap` of the
|
|
34
|
+
styling, an undefined colour under the folding sign). Measured in bytes for the same history: the chapters **−745 B**
|
|
35
|
+
(37 lines), the suites **−18 462 B** (251 lines), the harness **+4 328 B** — the tree loses **14 879 B and 220 lines**,
|
|
36
|
+
and the artifact **85 730 → 84 991 B**, its program and its styling being exactly what the chapters are. The checks go
|
|
37
|
+
89 → **86** in the fast profile and 194 → **191** in the full one: what a reader of the page does is one place now
|
|
38
|
+
rather than five, and a check that read the table it no longer has is not a check. The figures and the reasoning stand
|
|
39
|
+
in `worklog/0210-page-suites-on-the-window.md`.
|
|
40
|
+
|
|
41
|
+
2.8.1 is the drawing of the page put right after its table became a window, and its figures are measured rather than
|
|
42
|
+
retold. **The panel and the table were sharing one class:** the table's rows were styled by a bare `.row` while the
|
|
43
|
+
panel's own rows are `.panel .row`, so the table's rule took the metrics row and the three method lines out of the flow
|
|
44
|
+
and painted them over one another — every rule of the shared part now hangs off `#grid`, and a check reads the selector
|
|
45
|
+
list of that sheet rather than a sample of the page. **The numbers were placed from the grid's left edge while the commit
|
|
46
|
+
column is pinned over it**, which hid the first column — the overall total — completely; they begin at the column's right
|
|
47
|
+
edge now, a 220px figure written in the script and in the styling and held together like the three beside it. **The order
|
|
48
|
+
of the columns comes from the numbers rather than from the commit's list of paths**: a commit can touch a file without
|
|
49
|
+
moving it — this package's own attachment to itself, 2.7.0 → 2.8.0, is one character for another — and a column of empty
|
|
50
|
+
cells standing in front of the table is what a reader calls a broken order, so a file's newest move decides its place;
|
|
51
|
+
the engine's mark `last` stays in `--data` while the page's block no longer carries it. **One border and one height:**
|
|
52
|
+
every line of the table is 1px of one colour (the header's two lines were 2px and another shade) and the header is two rows
|
|
53
|
+
of the table's own height, while a file's name is centred over its group. The artifact grows 351 B for it — 85 495 B
|
|
54
|
+
against 85 144 B of one history. The checks go 88 → **89** in the fast profile and 193 → **194** in the full one; the
|
|
55
|
+
figures and the reasoning stand in `worklog/0209-page-drawing-order.md`.
|
|
56
|
+
|
|
22
57
|
2.8.0 is about what the report costs whoever opens it, and its figures are measured rather than retold.
|
|
23
58
|
**The table is a window of itself:** every row and every column in sight is built, plus four beyond each edge so that
|
|
24
59
|
the edge of the window is never seen empty, while the rest of the table exists as the extent of the scrolled box and
|
|
@@ -33,11 +68,10 @@ stripe over a drawing is gone with the freeze it was drawn for**: a switch costs
|
|
|
33
68
|
for an indicator to indicate, and `src/page/work.js` left the tree with it. A virtualizer library was measured and not
|
|
34
69
|
taken — `@tanstack/virtual-core` is ~6.7 kB gzip and headless, `virtua`'s grid is experimental and `Clusterize.js`
|
|
35
70
|
knows rows and not columns — because each would be vendored into the artifact, which the report then measures as its own
|
|
36
|
-
bytes.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
fast profile and 186 → **193** in the full one.
|
|
71
|
+
bytes. The checks of the old table went quiet with it, each naming its reason, and 2.8.2 brought them back against the
|
|
72
|
+
window; `test/page-grid.test.js` is what guards the window itself. The figures and the reasons stand in
|
|
73
|
+
`worklog/0208-table-window.md`. The checks grow with the work: 81 → **88** in the fast profile and 186 → **193**
|
|
74
|
+
in the full one.
|
|
41
75
|
|
|
42
76
|
2.7.0 is about the page and what a reader does with it, and its figures are measured rather than retold.
|
|
43
77
|
**The report's address stays clean:** a switch writes the record into the browser's memory and nothing into the
|
|
@@ -237,11 +271,10 @@ commits and installs hooks; the reason for each expensive file is named line by
|
|
|
237
271
|
|
|
238
272
|
| Run | Command | Checks |
|
|
239
273
|
|---|---|---|
|
|
240
|
-
| Fast — every edit | `pnpm test` | **
|
|
241
|
-
| Full — release and CI | `pnpm test:all` | **
|
|
274
|
+
| Fast — every edit | `pnpm test` | **86 of 191** |
|
|
275
|
+
| Full — release and CI | `pnpm test:all` | **191** |
|
|
242
276
|
|
|
243
|
-
No check is lost or weakened: the full run starts
|
|
244
|
-
of them. The default is the full run — a file becomes fast only explicitly and with a reason — so new
|
|
277
|
+
No check is lost or weakened: the full run starts every file, the fast one takes part of them. The default is the full run — a file becomes fast only explicitly and with a reason — so new
|
|
245
278
|
expensive work cannot quietly move into the fast one. Two declarations guard that:
|
|
246
279
|
`test/suites.test.js` (every file classified, and a reason for each) and the documentation guard
|
|
247
280
|
`test/docs-numbers.test.js` (the numbers in the table above).
|
|
@@ -367,10 +400,14 @@ a *single* cell of this repository's report, 539 ms for 6 000 (`probes/step-12-c
|
|
|
367
400
|
table is **2 460 nodes and 0.15 GB**, a hundredth of the nodes and a tenth of the memory, and the whole-table scroll
|
|
368
401
|
pass that took 36 s of task time over 60 steps takes **1.1 s over 251** (`worklog/0208-table-window.md`). **The columns
|
|
369
402
|
are one width and fixed, 70px each**: the numbers are short and of one kind, and a width that came out of the text is a
|
|
370
|
-
measurement of every cell of the column — the very cost this step removed. A file's name over its group
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
403
|
+
measurement of every cell of the column — the very cost this step removed. A file's name stands centred over its group
|
|
404
|
+
and is cut with an ellipsis rather than wrapped — the whole name stays in the tooltip. The header is two lines of one
|
|
405
|
+
row each, the numbers begin at the
|
|
406
|
+
right edge of the pinned commit column rather than under it, and every line of the table — under a row, under a header
|
|
407
|
+
cell, along a group's left edge — is one pixel of one colour: a heavier line would promise a hierarchy to work out
|
|
408
|
+
rather than read. The geometry is four figures — a column, a row, the header, the pinned column — written in the styling
|
|
409
|
+
and in the script that counts the window's ordinals in them, and `test/page-grid.test.js` reads both and holds them
|
|
410
|
+
together.
|
|
374
411
|
|
|
375
412
|
**A click builds the window again, and that is cheap now.** A file switched off is simply not among the columns that
|
|
376
413
|
are built, and a metric switched off not among the metrics: there is nothing to hide and nothing to carry, and the
|
|
@@ -387,15 +424,16 @@ virtualizes rows out of a string of all of them and knows nothing of columns —
|
|
|
387
424
|
the artifact, whose bytes this very tool measures. What is left to write after any of them is what the chapter is: the
|
|
388
425
|
window, the cells, the header and the pinned column.
|
|
389
426
|
|
|
390
|
-
**The columns
|
|
391
|
-
reader's first question is what that edit brought. The mark
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
427
|
+
**The columns whose numbers last moved come first.** The report is rebuilt after every commit, and a
|
|
428
|
+
reader's first question is what that edit brought. The mark is taken from the numbers rather than from
|
|
429
|
+
the commit's list of paths, and that is the whole of the difference: a commit can touch a column without
|
|
430
|
+
moving it — a version bumped inside a line of the same length, this package's own attachment to itself is
|
|
431
|
+
one — and a column of empty cells standing in front of the table is what a reader calls a broken order.
|
|
432
|
+
The rest follows in the settings' order (the sort is stable), so the order of the columns is what the
|
|
433
|
+
reader is used to, and his choice of files does not rearrange it (`src/page/table.js`; the rule is counted
|
|
434
|
+
from the contract's own rows in `test/page-grid.test.js`). The engine still answers with `last` — the
|
|
435
|
+
columns the newest commit touched — in `--data`; the page no longer reads it, and the block it carries no
|
|
436
|
+
longer holds it.
|
|
399
437
|
|
|
400
438
|
**On a wide window the panel stands to the left of the table and takes no room from the numbers** (from
|
|
401
439
|
900px, `src/page/app.css`). That is not decoration: a desktop has much side room and little vertical
|
|
@@ -578,13 +616,13 @@ acceptance for each.
|
|
|
578
616
|
| `src/size-table.js` | The package's entry point: a re-export of the public API (55 names) and no calculation of its own |
|
|
579
617
|
| `src/derived.js` | The report's shared calculation: totals, deltas, a cell, a commit's caption — one for the engine and the page's program |
|
|
580
618
|
| `src/css.js` | Reading the styling from disk: which sets of styles exist and what role each has |
|
|
581
|
-
| `src/table.css` | The report's table: the geometry of the window — one width per column, one height per row, the header — the sticky header and commit column, a file's caption cut with an ellipsis, the colour of deltas |
|
|
619
|
+
| `src/table.css` | The report's table: the geometry of the window — one width per column, one height per row, the two lines of the header, the pinned commit column — the sticky header and commit column, one 1px line of one colour for every border, a file's caption centred over its group and cut with an ellipsis, the colour of deltas |
|
|
582
620
|
| `src/page/app.css` | The page's styling on top of the shared part: the panel with the file tree and its sticky row of categories (a column on the left on a wide screen, the page fitting the window), the empty states, a narrow window |
|
|
583
621
|
| `src/page/payload.js` | The page's block in sparse form, and the one place that unrolls it back: the history as changes (a file's appearance, its moves, its disappearance) turned into the snapshots the calculation and the table already speak — a value that did not move is one object shared by the rows that hold it |
|
|
584
622
|
| `src/page/state.js` | The page's state: the report's data (the block unrolled by the payload chapter), the view of the checkboxes, the pointer "which path is which column", the unfolded folders, the record's passport, the browser's memory and the link that is read out of the address — a chapter of the page's program |
|
|
585
623
|
| `src/page/dom.js` | The page's nodes: the small helpers of markup (`appEl`, `appBox`) — one set for the panel and the table alike |
|
|
586
624
|
| `src/page/panel.js` | The panel of choices: the switches of metrics and files, the categories, the tree of the project's paths (files outside the report keep a checkbox off with a reason and stand after the rest, and a hidden name after every visible one; the tree opens folded and folders carry a sign that hides the subtree by a class rather than by a rebuild); built once, with the fields of the switches and of the folders and categories written where they stand |
|
|
587
|
-
| `src/page/table.js` | The page's table as a window: the rows and the columns the reader can see (plus four beyond each edge), a cell, a commit's caption, the header, the empty states and the
|
|
625
|
+
| `src/page/table.js` | The page's table as a window: the rows and the columns the reader can see (plus four beyond each edge), a cell, a commit's caption, the header, the empty states and the order of the columns — markup over the shared calculation, with the totals counted per row from the choice, the geometry in pixels that the styling mirrors, and the reason a library was not taken |
|
|
588
626
|
| `src/page/app.js` | Assembling and starting the page: the first drawing, then a switch that builds the window again and writes the fields it reached without making a node of the panel; an anchor change; pasted into the assembled page |
|
|
589
627
|
| `src/page/build.js` | Assembling the page: data, styling and program in one file with no external references — the pasted text is **squeezed** on the way in (comments and indentation out, the same stripping the `min` metric counts) while the sources keep them, and the result is guarded by the stripper's own `assertCompilable` |
|
|
590
628
|
| `src/git.js` | The only border where git is called: the pinned settings, blobs by the batch, the history, the comparison with the working tree |
|
|
@@ -617,7 +655,7 @@ acceptance for each.
|
|
|
617
655
|
| `test/api.test.js` | The package's public API: the list of names is frozen, and splitting the engine may not change it |
|
|
618
656
|
| `eslint.config.js` | The rules of formatting: the same as the consumer project's, plus a ban on gluing operators into one line (`pnpm run lint`, `pnpm run lint:strict`) |
|
|
619
657
|
| `tools/harness.js` | The harness of the checks: paths, clones of the fixture (including one shared per suite and one with CRLF), running the tool, reading refusals, hashes |
|
|
620
|
-
| `tools/page-harness.js` | The harness of the contract and page checks: the contract data, the assembled page, reading it in a real DOM, the
|
|
658
|
+
| `tools/page-harness.js` | The harness of the contract and page checks: the contract data, the assembled page, reading it in a real DOM, one set of readers for every page suite (a file's path at HEAD, the rows the window built and their numbers, the captions over them, the order of the columns, a folder's and a category's switch, the place the shell stands at), the page's calculation and its decoder evaluated from their sources, the block unpacked, and the platform's unpacker put into jsdom (which has none) — one for seven suites |
|
|
621
659
|
| `tools/suites.js` | The split of the suite: which files go into the fast run (with a reason for each) and why every dear one is in the full run |
|
|
622
660
|
| `tools/run-tests.js` | Running the suite (`pnpm test`, `pnpm test:all`, `pnpm run suites:measure`): each file's duration measured on its own, and the counts of checks adding up |
|
|
623
661
|
| `tools/docs-facts.js` | Reading facts out of the documentation — one layer for the four checks of the documentation guard: what a document names (paths, calls, section addresses) against what the repository holds |
|
|
@@ -633,11 +671,11 @@ acceptance for each.
|
|
|
633
671
|
| `test/refusals-catalog.test.js` | The guard of the refusal catalogue: every refusal site in the sources has an entry, every entry declares its advice, and refusals handed to another check are really accepted by it (the named file and line are checked) |
|
|
634
672
|
| `test/contract-data.test.js` | The data contract: the numbers against the reference, the set of fields against the derived quantities, the metric's method against the way the numbers were counted — and the round trip through the page's sparse block, which restores the contract whole and twice over the same bytes |
|
|
635
673
|
| `test/contract-derived.test.js` | The derived quantities against the artifact's numbers: a row's totals, a cell's delta and the delta of a total — on the code that lies in the tree |
|
|
636
|
-
| `test/page-grid.test.js` | The grid of the page as a window: what the reader sees is built and no more, the geometry of the styling against the script's, the rows and the columns that a scroll builds and drops, a number under its own caption, a file and a metric switched off,
|
|
637
|
-
| `test/page-view.test.js` | The assembled page: pasted with no copy of the calculation, self-contained, the
|
|
638
|
-
| `test/page-tree.test.js` | The panel's file tree: folders by the project's paths, three states, the subtree, files and folders outside the report (a checkbox off, a place after the rest), the hidden names at the end of a level, the tree that opens folded and the unfolding the memory keeps, folding without a rebuild and a scroll a click does not touch
|
|
639
|
-
| `test/page-choice.test.js` | The reader's choice
|
|
640
|
-
| `test/page-cols.test.js` | The
|
|
674
|
+
| `test/page-grid.test.js` | The grid of the page as a window: what the reader sees is built and no more, the geometry of the styling against the script's, the rows and the columns that a scroll builds and drops, a number under its own caption, a file and a metric switched off, every row of the window against the engine's own calculation, and the drawing's own rules — the order of the columns by the numbers, one border, the header's rows of one height, and the panel out of the shared styling's reach |
|
|
675
|
+
| `test/page-view.test.js` | The assembled page: pasted with no copy of the calculation, self-contained, the sparse block, the recount by the choice, every total of the window against the engine's own sums, no mark of precision left on a cell, the shared styling pasted once and the delta colour set once, the empty states, and the switches' focus |
|
|
676
|
+
| `test/page-tree.test.js` | The panel's file tree: folders by the project's paths, three states, the subtree, files and folders outside the report (a checkbox off, a place after the rest), the hidden names at the end of a level, the tree that opens folded and the unfolding the memory keeps, folding without a rebuild and a scroll a click does not touch |
|
|
677
|
+
| `test/page-choice.test.js` | The reader's choice: the memory and a revisit, someone else's report, a foreign and a broken record, an address that stays clean, a link read at opening and on an open page, and a host without an unpacker that is told so in words |
|
|
678
|
+
| `test/page-cols.test.js` | The columns of the grid: their order by the newest row a file's numbers moved in, a caption over exactly the metrics of its group, and a choice that moves no other column |
|
|
641
679
|
| `test/module.test.js` | A module under a `.js` extension: measured without touching the settings; the stripper's guard is alive (proved by mutation) and does not accuse the innocent |
|
|
642
680
|
| `test/guard.test.js` | Parsing a module: it goes through a thread, both paths give one verdict, the fallback works with the thread's file away, and hundreds of parses are cheaper than a launch |
|
|
643
681
|
| `test/runner.test.js` | Reading a process's output: chunks are glued as buffers rather than appended to a string — a multi-byte character at a chunk border does not turn into two replacement characters |
|
|
@@ -717,7 +755,7 @@ The same release can be taken by a reference to the repository — installation
|
|
|
717
755
|
registry, but stays tied to a revision:
|
|
718
756
|
|
|
719
757
|
```bash
|
|
720
|
-
pnpm add -D github:vernikr/size-report#v2.8.
|
|
758
|
+
pnpm add -D github:vernikr/size-report#v2.8.2
|
|
721
759
|
```
|
|
722
760
|
|
|
723
761
|
With no network (or nothing to fetch from codeload) — the tarball: `pnpm pack` in the package clone, then
|
|
@@ -729,7 +767,7 @@ the branch moves on the installation fails with `Could not resolve <sha> to a co
|
|
|
729
767
|
observation rather than reasoning: the short pin `6530237` installed while `main` stood on it and stopped
|
|
730
768
|
working at the very next commit, while the same sha in full installed. A branch name (`#main`) and a tag
|
|
731
769
|
are both accepted, but a branch is a moving target and a tag is constant: this release stands on the tag
|
|
732
|
-
`v2.8.
|
|
770
|
+
`v2.8.2`, which is also the one in the example (forty characters work as well, but they have to be copied
|
|
733
771
|
out of the history by eye).
|
|
734
772
|
|
|
735
773
|
The revision in the example is a part of the claim rather than decoration: what is described below is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vernikr/size-report",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.2",
|
|
4
4
|
"author": "vernikr",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"report"
|
|
66
66
|
],
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@vernikr/size-report": "2.
|
|
68
|
+
"@vernikr/size-report": "2.8.1",
|
|
69
69
|
"c8": "10",
|
|
70
70
|
"dependency-cruiser": "17",
|
|
71
71
|
"eslint": "^9.18.0",
|
package/src/page/app.css
CHANGED
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
--pad: clamp(16px, 3vw, 34px);
|
|
15
15
|
--radius: 10px;
|
|
16
16
|
--muted: rgba(127, 127, 127, .85);
|
|
17
|
-
|
|
17
|
+
/* The same grey as the table's own lines (`--grid-line` of `src/table.css`): the frame around the numbers and the rows
|
|
18
|
+
* inside it are one drawing, and a second grey for the chrome is what makes a page look assembled from two. The two
|
|
19
|
+
* declarations are held together by the check that reads the borders of both sheets. */
|
|
20
|
+
--line: rgba(127, 127, 127, .25);
|
|
18
21
|
--tint: rgba(127, 127, 127, .07);
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -42,7 +45,7 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
42
45
|
}
|
|
43
46
|
.panel fieldset { margin: 0; padding: 0; border: 0; }
|
|
44
47
|
.panel fieldset + fieldset { margin-top: 10px; }
|
|
45
|
-
.panel legend
|
|
48
|
+
.panel legend {
|
|
46
49
|
padding: 0;
|
|
47
50
|
color: var(--muted);
|
|
48
51
|
font-size: 11px;
|
|
@@ -50,7 +53,6 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
50
53
|
text-transform: uppercase;
|
|
51
54
|
letter-spacing: .07em;
|
|
52
55
|
}
|
|
53
|
-
.panel .cap { display: block; margin-bottom: 4px; }
|
|
54
56
|
.panel .row { display: flex; flex-wrap: wrap; gap: 3px 6px; align-items: center; }
|
|
55
57
|
/* The way of counting is visible text under the switches: the token dictionary and the way of compression come from the
|
|
56
58
|
* settings of the run rather than from a checkbox, so pointing a mouse is not enough — the reader has to see what produced
|
|
@@ -88,7 +90,7 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
88
90
|
cursor: pointer;
|
|
89
91
|
user-select: none;
|
|
90
92
|
}
|
|
91
|
-
.panel .tree li > .fold:hover { color:
|
|
93
|
+
.panel .tree li > .fold:hover { color: CanvasText; }
|
|
92
94
|
|
|
93
95
|
/* 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
96
|
* input) and assistive technology all reach it. */
|
|
@@ -128,11 +130,7 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
128
130
|
/* Numbers are denser than the page's text: there are more of them, they are shorter, and they are read by their digits. */
|
|
129
131
|
#grid { font-size: 12.5px; }
|
|
130
132
|
|
|
131
|
-
/*
|
|
132
|
-
* over the browser's relayout of the whole table, and the table is built as a window now — a switch costs a few
|
|
133
|
-
* milliseconds (`src/page/table.js`), so an indicator would be a promise the page does not keep.
|
|
134
|
-
*
|
|
135
|
-
* The message about a link that came in stands above the table, so that it cannot be missed, while it does not push the
|
|
133
|
+
/* The message about a link that came in stands above the table, so that it cannot be missed, while it does not push the
|
|
136
134
|
* table away: one line in the place of the page. Its colour is the system one (the accent), for the page keeps no colours
|
|
137
135
|
* of its own. */
|
|
138
136
|
.notice {
|
|
@@ -231,7 +229,8 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
231
229
|
h1 { font-size: 18px; }
|
|
232
230
|
#grid { font-size: 12px; }
|
|
233
231
|
.shell { max-height: calc(100vh - 260px); }
|
|
234
|
-
/* The commit column
|
|
235
|
-
* (`
|
|
236
|
-
*
|
|
232
|
+
/* The commit column keeps its width on a narrow screen, and that is deliberate rather than forgotten: the numbers
|
|
233
|
+
* are placed beside it by the script (`APP_COMMIT`), so a narrower column here would leave a band of empty grid
|
|
234
|
+
* between the two — one number would have to be read from the styling by the script, which is a second copy of a
|
|
235
|
+
* geometry the check holds together (`test/page-grid.test.js`). */
|
|
237
236
|
}
|
package/src/page/app.js
CHANGED
|
@@ -4,15 +4,11 @@ import { appState, appTable, appWindow } from './table.js';
|
|
|
4
4
|
import { appPanel, appPanelAll, appPanelState } from './panel.js';
|
|
5
5
|
|
|
6
6
|
/* Assembling the report: the table is a window of the grid (`appWindow` of the table chapter) and nothing is built
|
|
7
|
-
* that the reader cannot see. Two paths and no third
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* this repository's report — instead of a class on every node of a column of a table the browser lays out whole. The
|
|
13
|
-
* stripe that used to stand over a long drawing (`src/page/work.js`) is gone with the reason for it: a switch no
|
|
14
|
-
* longer has anything to wait for, and an indicator over work that is over before it could be painted would be a
|
|
15
|
-
* promise the page does not keep.
|
|
7
|
+
* that the reader cannot see. Two paths and no third: `appPaint` draws the whole view (the first drawing, a record from
|
|
8
|
+
* the browser's memory, a link in the address), while a click on a box does the same work for the choice it made — the
|
|
9
|
+
* difference between them is only how much of the view moved. Neither has anything to wait for: a switch builds the
|
|
10
|
+
* window of the grid, a few hundred cells and milliseconds, so the page needs no indicator and keeps no promise it
|
|
11
|
+
* cannot measure.
|
|
16
12
|
*/
|
|
17
13
|
|
|
18
14
|
// The table's window: made once, at the first drawing.
|
|
@@ -42,38 +38,36 @@ export function appPaint() {
|
|
|
42
38
|
appWrite();
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
/* One
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
/* One drawing after a click, whatever was clicked: the window of the grid (the columns of what is switched off are
|
|
42
|
+
* simply not among the columns that are built), the counts of the empty states, the fields the choice really reached
|
|
43
|
+
* (`touched` — a whole group is one click, and a field that did not move is not written) and the memory. The message
|
|
44
|
+
* about a link fades here: by this action the reader has read it. */
|
|
45
|
+
function appChanged(touched) {
|
|
50
46
|
appWindow(appCache, true);
|
|
51
47
|
appCounts();
|
|
52
|
-
appPanelState(
|
|
48
|
+
if (touched.length > 0) appPanelState(touched);
|
|
53
49
|
appWrite();
|
|
54
50
|
appNotice('');
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
// One file switched by the reader. A switch that changes nothing is not a change: the click costs no drawing.
|
|
54
|
+
export function appSwitch(i, on) {
|
|
55
|
+
if (appView.files[i] === on) return;
|
|
56
|
+
appView.files[i] = on;
|
|
57
|
+
appChanged([i]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* A group switched at once — a folder or a category: the same work per file, and the fields of the files the choice
|
|
61
|
+
* really reached (switching a folder on when a part of it was already on touches only the rest). */
|
|
60
62
|
export function appSwitchGroup(indexes, on) {
|
|
61
63
|
const touched = indexes.filter((i) => appView.files[i] !== on);
|
|
62
64
|
touched.forEach((i) => { appView.files[i] = on; });
|
|
63
|
-
|
|
64
|
-
appCounts();
|
|
65
|
-
appPanelState(touched);
|
|
66
|
-
appWrite();
|
|
67
|
-
appNotice('');
|
|
65
|
+
appChanged(touched);
|
|
68
66
|
}
|
|
69
67
|
|
|
70
|
-
/* One metric switched
|
|
71
|
-
* same drawing as a file's switch. The metric's own field is the box the reader just clicked. */
|
|
68
|
+
/* One metric switched. Only the panel's metric box shows it, and that box is the one the reader just clicked. */
|
|
72
69
|
export function appSwitchMetric() {
|
|
73
|
-
|
|
74
|
-
appCounts();
|
|
75
|
-
appWrite();
|
|
76
|
-
appNotice('');
|
|
70
|
+
appChanged([]);
|
|
77
71
|
}
|
|
78
72
|
|
|
79
73
|
/* The first drawing: the choice is already in the view (the link and the memory are applied above), the panel is
|
package/src/page/build.js
CHANGED
|
@@ -126,11 +126,13 @@ function uiText(page, loc) {
|
|
|
126
126
|
* one (`src/page/payload.js`, `appDecode`), so the two places it is read are the encoder here and the decoder there.
|
|
127
127
|
* `test/contract-data.test.js` holds the round trip between them.
|
|
128
128
|
*
|
|
129
|
-
* What the block leaves out besides the sparse form
|
|
130
|
-
* commit (one with nothing to say lands in the list), and the file would stop being a
|
|
131
|
-
* after its own commit would yield different bytes and the hook would commit the report
|
|
132
|
-
*
|
|
133
|
-
*
|
|
129
|
+
* What the block leaves out besides the sparse form are two facts the page does not read. The list of skipped commits
|
|
130
|
+
* changes with the report's own commit (one with nothing to say lands in the list), and the file would stop being a
|
|
131
|
+
* **fixed point** — a rebuild after its own commit would yield different bytes and the hook would commit the report
|
|
132
|
+
* forever; the page does not show it. The other is `last`, the columns the newest commit touched: the page orders the
|
|
133
|
+
* columns by the numbers themselves, which is a different fact (`src/page/table.js` says why), and the block would
|
|
134
|
+
* carry a list nothing asks for. Both stay available to the reader — `--data`, `--json` and `explain` answer from the
|
|
135
|
+
* same run.
|
|
134
136
|
*
|
|
135
137
|
* How much this is worth: the artifact of this repository carried 1 370 627 B of data as a snapshot per commit — 95 %
|
|
136
138
|
* of the whole file — while nine tenths of the cells repeat the row above; the same history as changes is about 84 000
|
|
@@ -139,7 +141,7 @@ function uiText(page, loc) {
|
|
|
139
141
|
/* The block's fields, in the order the encoder writes them. The shape is closed: a field added here has to be read in
|
|
140
142
|
* the decoder, and the page's checks compare the block with this list rather than with a description of it. */
|
|
141
143
|
export const PAGE_KEYS = ['schema', 'tool', 'report', 'hrefPrefix', 'strs', 'metrics', 'cats', 'files',
|
|
142
|
-
'catalog', 'rows', '
|
|
144
|
+
'catalog', 'rows', 'hist'];
|
|
143
145
|
|
|
144
146
|
/* The texts of the block, each written once. The dictionary is extended in the order of the walk `pagePayload` makes
|
|
145
147
|
* — files in the column order, rows in the history order — and the order of the first appearance is what decides an
|
|
@@ -227,10 +229,8 @@ export function pagePayload(data) {
|
|
|
227
229
|
r.section === null ? null : dict.of(r.section.head),
|
|
228
230
|
r.section !== null && r.section.added ? 1 : 0,
|
|
229
231
|
r.href === null ? null : dict.of(r.href.slice(prefix.length))]),
|
|
230
|
-
last: [],
|
|
231
232
|
hist: history(keys, data.files, data.rows)
|
|
232
233
|
};
|
|
233
|
-
data.last.forEach((on, i) => { if (on) out.last.push(i); });
|
|
234
234
|
return out;
|
|
235
235
|
}
|
|
236
236
|
|
package/src/page/panel.js
CHANGED
|
@@ -22,19 +22,18 @@ function appUnmeasuredBox(entry) {
|
|
|
22
22
|
+ (entry.why === 'rule' ? appUi.notMeasuredRule : appUi.notMeasuredChoice));
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return n;
|
|
25
|
+
/* A subtree in the two figures its folder shows: the measured files its checkbox controls, and how many files the
|
|
26
|
+
* folder holds at all — including the ones that made it into no report. One walk, because the two are asked together
|
|
27
|
+
* (a folder of five files with two measured reads "2/5"). */
|
|
28
|
+
function appSub(node) {
|
|
29
|
+
const idx = node.files.slice();
|
|
30
|
+
let total = node.files.length + node.others.length;
|
|
31
|
+
node.dirs.forEach((sub) => {
|
|
32
|
+
const inner = appSub(sub);
|
|
33
|
+
idx.push(...inner.idx);
|
|
34
|
+
total += inner.total;
|
|
35
|
+
});
|
|
36
|
+
return { idx: idx, total: total };
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
/* A tree node: the measured files (columns), the project's other files and the subfolders. */
|
|
@@ -48,8 +47,9 @@ function appNode() {
|
|
|
48
47
|
* without a single measured file stays in place with its checkbox off and unavailable: there is nothing to switch on
|
|
49
48
|
* in it. */
|
|
50
49
|
function appDirHead(name, here, sub) {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
50
|
+
const own = appSub(sub);
|
|
51
|
+
const idx = own.idx;
|
|
52
|
+
const total = own.total;
|
|
53
53
|
const label = name + '/';
|
|
54
54
|
let head;
|
|
55
55
|
if (idx.length === 0) {
|
|
@@ -141,13 +141,14 @@ function appLeaf(leaf) {
|
|
|
141
141
|
function appTreeList(node, prefix) {
|
|
142
142
|
const list = appEl('ul', 'tree');
|
|
143
143
|
const dirs = [...node.dirs.keys()].sort(appName)
|
|
144
|
-
.map((name) => ({ name: name, sub: node.dirs.get(name), inReport:
|
|
144
|
+
.map((name) => ({ name: name, sub: node.dirs.get(name), inReport: appSub(node.dirs.get(name)).idx.length > 0 }));
|
|
145
145
|
const leaves = appLeaves(node);
|
|
146
146
|
const inside = leaves.filter((leaf) => leaf.entry === null);
|
|
147
|
+
const outside = leaves.filter((leaf) => leaf.entry !== null);
|
|
147
148
|
dirs.filter((d) => d.inReport).forEach((d) => list.appendChild(appDir(d.name, d.sub, prefix)));
|
|
148
149
|
inside.forEach((leaf) => list.appendChild(appLeaf(leaf)));
|
|
149
150
|
dirs.filter((d) => !d.inReport).forEach((d) => list.appendChild(appDir(d.name, d.sub, prefix)));
|
|
150
|
-
|
|
151
|
+
outside.forEach((leaf) => list.appendChild(appLeaf(leaf)));
|
|
151
152
|
return list;
|
|
152
153
|
}
|
|
153
154
|
|
|
@@ -237,10 +238,10 @@ function appRowBoxes(box) {
|
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
// A folder's field from its files: all on — checked, some — the third state, none — simply unchecked.
|
|
240
|
-
function appDirState(
|
|
241
|
-
const boxes = appRowBoxes(
|
|
241
|
+
function appDirState(path) {
|
|
242
|
+
const boxes = appRowBoxes(appFields.dir[path]);
|
|
242
243
|
const on = boxes.filter((b) => b.checked).length;
|
|
243
|
-
const input =
|
|
244
|
+
const input = appFields.dir[path].querySelector('input');
|
|
244
245
|
input.checked = on === boxes.length;
|
|
245
246
|
input.indeterminate = on > 0 && on < boxes.length;
|
|
246
247
|
}
|
|
@@ -267,29 +268,17 @@ function appDirPath(path) {
|
|
|
267
268
|
return parts.slice(0, -1).map((_part, i) => parts.slice(0, i + 1).join('/'));
|
|
268
269
|
}
|
|
269
270
|
|
|
270
|
-
/* A click reaches a folder
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
indexes.forEach((i) => {
|
|
276
|
-
appDirPath(appFileAt(i)).forEach((path) => {
|
|
277
|
-
if (seen[path] === true) return;
|
|
278
|
-
seen[path] = true;
|
|
279
|
-
if (appFields.dir[path] !== undefined) appDirState(appFields.dir[path]);
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// The same for the quick buttons of the categories: every category one of the switched files belongs to.
|
|
285
|
-
function appCatsOf(indexes) {
|
|
271
|
+
/* A click reaches the fields of a folder and of a category through the files below them — one walk for both, because
|
|
272
|
+
* it is one question: which groups the reader sees hold a switched file, and what do they show now. A field that is not
|
|
273
|
+
* there is left alone: a folder without measured files has a box of its own, off and unavailable, which is not the
|
|
274
|
+
* reader's state. */
|
|
275
|
+
function appReach(indexes, fields, namesOf, stateOf) {
|
|
286
276
|
const seen = {};
|
|
287
|
-
indexes.forEach((i) => {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
});
|
|
277
|
+
indexes.forEach((i) => namesOf(i).forEach((name) => {
|
|
278
|
+
if (seen[name] === true || fields[name] === undefined) return;
|
|
279
|
+
seen[name] = true;
|
|
280
|
+
stateOf(name);
|
|
281
|
+
}));
|
|
293
282
|
}
|
|
294
283
|
|
|
295
284
|
/* What a click changed, written where it stands: the files' own boxes, then the fields of the folders and categories
|
|
@@ -299,8 +288,8 @@ export function appPanelState(indexes) {
|
|
|
299
288
|
const input = appFields.file[i];
|
|
300
289
|
if (input !== undefined) input.checked = appView.files[i] === true;
|
|
301
290
|
});
|
|
302
|
-
|
|
303
|
-
|
|
291
|
+
appReach(indexes, appFields.dir, (i) => appDirPath(appFileAt(i)), appDirState);
|
|
292
|
+
appReach(indexes, appFields.cat, (i) => [appData.files[i].category], appCatState);
|
|
304
293
|
}
|
|
305
294
|
|
|
306
295
|
/* The whole panel from the view: what a link, a record from the memory and the first drawing need. The metric boxes
|
|
@@ -309,5 +298,5 @@ export function appPanelAll() {
|
|
|
309
298
|
appData.categories.forEach((c) => appCatState(c.key));
|
|
310
299
|
appData.metrics.forEach((m) => { appFields.metric[m.key].checked = appView.metrics[m.key] === true; });
|
|
311
300
|
appData.files.forEach((_f, i) => { appFields.file[i].checked = appView.files[i] === true; });
|
|
312
|
-
Object.keys(appFields.dir).forEach((path) => appDirState(
|
|
301
|
+
Object.keys(appFields.dir).forEach((path) => appDirState(path));
|
|
313
302
|
}
|
package/src/page/payload.js
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
* moved (deltas against its own previous record) or disappeared, and this chapter puts the snapshots back
|
|
16
16
|
* together. The whole history walk is O(number of changes) rather than O(rows × files).
|
|
17
17
|
*
|
|
18
|
-
* The model after `appDecode` is exactly `--data
|
|
18
|
+
* The model after `appDecode` is exactly `--data` but for one field: the history's mark `last` (the columns the
|
|
19
|
+
* newest commit touched) is not carried, because the page orders its columns by the numbers rather than by the
|
|
20
|
+
* commit's list of paths (`src/page/table.js`). The calculation (`rowModel`, `totalsOf`, `cellParts`,
|
|
19
21
|
* `valueParts`), the table and the panel know nothing about the sparse form, so there is no second way to
|
|
20
22
|
* count a row. A value that did not move is **one object shared by the rows that hold it** — the heap keeps
|
|
21
23
|
* the distinct numbers rather than a copy per commit — and because every consumer reads `v[metric]` and
|
|
@@ -149,8 +151,6 @@ function appRowOf(p, r, values) {
|
|
|
149
151
|
export function appDecode(p) {
|
|
150
152
|
const keys = p.metrics.map((m) => appText(p, m[0]));
|
|
151
153
|
const hist = appUnroll(p, keys);
|
|
152
|
-
const last = p.files.map(() => false);
|
|
153
|
-
p.last.forEach((i) => { last[i] = true; });
|
|
154
154
|
return {
|
|
155
155
|
schema: p.schema,
|
|
156
156
|
tool: p.tool,
|
|
@@ -162,7 +162,6 @@ export function appDecode(p) {
|
|
|
162
162
|
paths: f[2].map((i) => p.strs[i]), category: appText(p, f[3]), categoryBy: appText(p, f[4]) })),
|
|
163
163
|
catalog: p.catalog.map((e) => ({ path: appText(p, e[0]), why: appText(p, e[1]) })),
|
|
164
164
|
rows: p.rows.map((r, ri) => appRowOf(p, r, hist.rows[ri])),
|
|
165
|
-
now: hist.now
|
|
166
|
-
last: last
|
|
165
|
+
now: hist.now
|
|
167
166
|
};
|
|
168
167
|
}
|
package/src/page/state.js
CHANGED
|
@@ -106,15 +106,16 @@ function appPassport() {
|
|
|
106
106
|
return appPassportValue;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/* One record of the choice
|
|
110
|
-
*
|
|
111
|
-
*
|
|
109
|
+
/* One record of the choice: only what is switched off is kept, by name. "Switched on" and "no record" are the same
|
|
110
|
+
* state, hence `null` rather than an empty record — turning every checkbox back on removes the record instead of leaving
|
|
111
|
+
* a trace indistinguishable from a choice. */
|
|
112
112
|
function appRecord() {
|
|
113
113
|
const metrics = {};
|
|
114
114
|
const files = {};
|
|
115
115
|
appData.metrics.forEach((m) => { if (!appView.metrics[m.key]) metrics[m.key] = false; });
|
|
116
116
|
appData.files.forEach((_f, i) => { if (!appView.files[i]) files[appFileAt(i)] = false; });
|
|
117
|
-
|
|
117
|
+
const empty = Object.keys(metrics).length === 0 && Object.keys(files).length === 0;
|
|
118
|
+
return empty ? null : { v: 1, passport: appPassport(), metrics: metrics, files: files };
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
// Whether a record is ours and of the right format — one rule for the memory and the address alike.
|
|
@@ -122,24 +123,40 @@ function appRecordOk(rec) {
|
|
|
122
123
|
return rec !== null && typeof rec === 'object' && rec.v === 1 && rec.passport === appPassport();
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
/*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const empty = Object.keys(rec.metrics).length === 0 && Object.keys(rec.files).length === 0;
|
|
126
|
+
/* A record into the browser's memory or out of it, and what is read back from it: the two halves of the memory, written
|
|
127
|
+
* once, because the page keeps two records of the same kind under two keys — the choice and the unfolded tree — and
|
|
128
|
+
* their rules are one. Nothing but the browser's memory is written: it is written on the click itself, which is what
|
|
129
|
+
* survives a closing, and the page's address keeps a clean tail — the report is a local page whose address is copied as
|
|
130
|
+
* it is, and a reader's choice belongs in the browser that made it rather than in the tab's title bar. What a link sent
|
|
131
|
+
* from an earlier release holds is still read (`appLinkUse`), and it is not written into the reader's memory: what came
|
|
132
|
+
* in is not his choice until he changes something. */
|
|
133
|
+
function appKeep(key, rec) {
|
|
134
134
|
try {
|
|
135
|
-
if (
|
|
136
|
-
else window.localStorage.setItem(
|
|
135
|
+
if (rec === null) window.localStorage.removeItem(key);
|
|
136
|
+
else window.localStorage.setItem(key, JSON.stringify(rec));
|
|
137
137
|
} catch (_e) {
|
|
138
138
|
/* There is no memory (the browser grants this page none): the choice will not survive a closing, while the numbers
|
|
139
139
|
* and the markup do not depend on it. */
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
export function appWrite() {
|
|
144
|
+
if (!appTransient) appKeep(appKey, appRecord());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* What the browser saved under a key: its absence, a foreign record, a broken one and a record of another report amount
|
|
148
|
+
* to the same thing here — nothing to read. `JSON.parse` of a missing value answers `null`, so the two absences need no
|
|
149
|
+
* telling apart. */
|
|
150
|
+
function appLoad(key) {
|
|
151
|
+
let rec = null;
|
|
152
|
+
try {
|
|
153
|
+
rec = JSON.parse(window.localStorage.getItem(key));
|
|
154
|
+
} catch (_e) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return appRecordOk(rec) ? rec : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
143
160
|
/* A reset to "everything on": the border between "this is no longer in the report" and "switched off" is the record
|
|
144
161
|
* rather than a missing value. A link carries the sender's whole choice, which is why it is applied to a clean view
|
|
145
162
|
* rather than on top of someone else's. */
|
|
@@ -174,15 +191,12 @@ function appLinkRead() {
|
|
|
174
191
|
}
|
|
175
192
|
|
|
176
193
|
/* How many names in the link are unknown to this report: the reader has to be told about them — otherwise he would look
|
|
177
|
-
* in the table for something that was never there.
|
|
194
|
+
* in the table for something that was never there. The two tables the model already holds answer it: a file by its path
|
|
195
|
+
* at HEAD (`appMeasured`) and a metric by its key (`appMetric`), the same names a record is written in (`appFileAt`). */
|
|
178
196
|
function appUnknown(rec) {
|
|
179
|
-
const known = {};
|
|
180
|
-
const metricKeys = {};
|
|
181
|
-
appData.files.forEach((_f, i) => { known[appFileAt(i)] = true; });
|
|
182
|
-
appData.metrics.forEach((m) => { metricKeys[m.key] = true; });
|
|
183
197
|
let n = 0;
|
|
184
|
-
Object.keys(rec.metrics || {}).forEach((k) => { if (
|
|
185
|
-
Object.keys(rec.files || {}).forEach((k) => { if (
|
|
198
|
+
Object.keys(rec.metrics || {}).forEach((k) => { if (appMetric[k] === undefined) n++; });
|
|
199
|
+
Object.keys(rec.files || {}).forEach((k) => { if (appMeasured[k] === undefined) n++; });
|
|
186
200
|
return n;
|
|
187
201
|
}
|
|
188
202
|
|
|
@@ -213,23 +227,9 @@ export function appLinkUse() {
|
|
|
213
227
|
return 'none';
|
|
214
228
|
}
|
|
215
229
|
|
|
216
|
-
|
|
217
|
-
* while a foreign, outdated or broken one amounts to its absence. */
|
|
230
|
+
// Reading: the reader's own choice, as he left it — another report's record lies under another key.
|
|
218
231
|
export function appRead() {
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
text = window.localStorage.getItem(appKey);
|
|
222
|
-
} catch (_e) {
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
if (text === null) return null;
|
|
226
|
-
let rec = null;
|
|
227
|
-
try {
|
|
228
|
-
rec = JSON.parse(text);
|
|
229
|
-
} catch (_e) {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
return appRecordOk(rec) ? rec : null;
|
|
232
|
+
return appLoad(appKey);
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/* Applying goes by name: a file is recognised by its path, a metric by its key. A name the report does not hold matches
|
|
@@ -248,37 +248,22 @@ export function appApply(rec) {
|
|
|
248
248
|
* what the onlooker looks at rather than about which numbers are read, which is why it never goes into a link and never
|
|
249
249
|
* leaves the browser. The tree opens folded, so **the unfolded folders are what is kept** (`true`) — the default is the
|
|
250
250
|
* absence of the name, the same way "switched on" is the absence of a choice. A folder's name is its path ("src/page"),
|
|
251
|
-
* so a vanished name simply means nothing
|
|
252
|
-
*
|
|
253
|
-
* itself is. */
|
|
251
|
+
* so a vanished name simply means nothing. `appFoldKey` is set with the model (`appBoot`), for the reason the key itself
|
|
252
|
+
* is. */
|
|
254
253
|
export function appFoldRead() {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
text = window.localStorage.getItem(appFoldKey);
|
|
258
|
-
} catch (_e) {
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
if (text === null) return;
|
|
262
|
-
let rec = null;
|
|
263
|
-
try {
|
|
264
|
-
rec = JSON.parse(text);
|
|
265
|
-
} catch (_e) {
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
if (!appRecordOk(rec)) return;
|
|
254
|
+
const rec = appLoad(appFoldKey);
|
|
255
|
+
if (rec === null) return;
|
|
269
256
|
const open = rec.open || {};
|
|
270
257
|
Object.keys(open).forEach((p) => { if (open[p] === true) appView.open[p] = true; });
|
|
271
258
|
}
|
|
272
259
|
|
|
260
|
+
/* What a folder's sign does with the memory: the unfolded folders are what is kept (`true`), so a tree folded whole is
|
|
261
|
+
* the absence of the record — the default — and the record goes away with the last unfolded folder. */
|
|
273
262
|
export function appFoldSet(path, open) {
|
|
274
263
|
if (open) appView.open[path] = true;
|
|
275
264
|
else delete appView.open[path];
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
} catch (_e) {
|
|
281
|
-
/* There is no memory: what is unfolded will not survive a closing, while the view does not depend on it — the tree
|
|
282
|
-
* is unfolded exactly the way the reader unfolded it just now. */
|
|
283
|
-
}
|
|
265
|
+
const names = Object.keys(appView.open);
|
|
266
|
+
appKeep(appFoldKey, names.length === 0
|
|
267
|
+
? null
|
|
268
|
+
: { v: 1, passport: appPassport(), open: appView.open });
|
|
284
269
|
}
|
package/src/page/table.js
CHANGED
|
@@ -13,7 +13,8 @@ import { appData, appUi, appView } from './state.js';
|
|
|
13
13
|
* to build less rather than to promise the browser will skip it. A grid of `position: absolute` rows has no layout to
|
|
14
14
|
* be redone: a row is placed by its `top`, a column by the `left` of the group of cells that starts it, and the
|
|
15
15
|
* browser never measures a cell to decide a width — every column is `--col` wide (70px), which is what the numbers
|
|
16
|
-
* need and no more (the counted widths this step replaced were 47–70px)
|
|
16
|
+
* need and no more (the counted widths this step replaced were 47–70px), and they begin at the right edge of the
|
|
17
|
+
* pinned commit column (`APP_COMMIT`) rather than under it.
|
|
17
18
|
*
|
|
18
19
|
* **Why not a library.** A virtualizer for two axes is not a solved problem for a page like this one — measured from
|
|
19
20
|
* the tarballs, `@tanstack/virtual-core` is ~6.7 kB gzip and headless (the rows and columns are two virtualizers and
|
|
@@ -37,10 +38,12 @@ import { appData, appUi, appView } from './state.js';
|
|
|
37
38
|
*/
|
|
38
39
|
|
|
39
40
|
/* The geometry in pixels: written here and read by the styling (`src/table.css`), which is one copy too many — hence
|
|
40
|
-
* `test/page-grid.test.js` holds the two together, and the report says the same numbers in its journal.
|
|
41
|
+
* `test/page-grid.test.js` holds the two together, and the report says the same numbers in its journal. The header is
|
|
42
|
+
* two lines of one row each, and the commit column is as wide as the styling pins it. */
|
|
41
43
|
export const APP_COL = 70;
|
|
42
44
|
export const APP_ROW = 25;
|
|
43
|
-
export const APP_HEAD =
|
|
45
|
+
export const APP_HEAD = APP_ROW * 2;
|
|
46
|
+
export const APP_COMMIT = 220;
|
|
44
47
|
|
|
45
48
|
/* How much more than the visible window is built, in rows and in columns. A window that ends exactly at the edge of
|
|
46
49
|
* the shell shows an empty band while the browser scrolls a notch; four rows and four columns of slack are cheaper
|
|
@@ -53,15 +56,41 @@ export const APP_OVER = 4;
|
|
|
53
56
|
export const APP_MIN_ROWS = 24;
|
|
54
57
|
export const APP_MIN_COLS = 10;
|
|
55
58
|
|
|
56
|
-
/* The order of the columns: the files
|
|
57
|
-
* commit, and
|
|
58
|
-
*
|
|
59
|
-
*
|
|
59
|
+
/* The order of the columns: the files whose numbers last moved come first, and the older the move the further right the
|
|
60
|
+
* column stands. A reader opens the report after a commit, and what he looks for is what that edit brought — while the
|
|
61
|
+
* rest may as well be ordered by the settings, which is the order of the ties.
|
|
62
|
+
*
|
|
63
|
+
* The mark is taken from the numbers rather than from the history's list of paths, and that is the whole of the
|
|
64
|
+
* difference: a commit can touch a column without moving it (a version bumped inside a line of the same length, this
|
|
65
|
+
* package's own attachment to itself is one), and a column of empty cells standing in front of the table is what a
|
|
66
|
+
* reader sees as a broken order. The list of paths is the engine's (`--data`), which is where a fact about a commit
|
|
67
|
+
* belongs; what stands here is a fact about the numbers.
|
|
68
|
+
*
|
|
69
|
+
* The order depends on the files rather than on the choice: that is what lets a column be switched off without moving
|
|
70
|
+
* the others. Counted once per document — the model does not change while the page is open — because every window the
|
|
71
|
+
* reader scrolls to asks for it. */
|
|
72
|
+
function appRank() {
|
|
73
|
+
const rank = appData.files.map(() => -1);
|
|
74
|
+
let was = null;
|
|
75
|
+
appData.rows.forEach((row, i) => {
|
|
76
|
+
/* The rows of a file are the same object until it moves (`src/page/payload.js`), so one comparison per file and
|
|
77
|
+
* row says whether the file appeared, moved or went away at this commit — the three cases a cell is not empty. */
|
|
78
|
+
row.values.forEach((v, j) => { if (was === null || was[j] !== v) rank[j] = i; });
|
|
79
|
+
was = row.values;
|
|
80
|
+
});
|
|
81
|
+
return rank;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let appOrderValue = null;
|
|
85
|
+
|
|
60
86
|
export function appOrder() {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
87
|
+
if (appOrderValue === null) {
|
|
88
|
+
const rank = appRank();
|
|
89
|
+
const files = [];
|
|
90
|
+
appData.files.forEach((_f, i) => files.push(i));
|
|
91
|
+
appOrderValue = files.sort((a, b) => rank[b] - rank[a]);
|
|
92
|
+
}
|
|
93
|
+
return appOrderValue;
|
|
65
94
|
}
|
|
66
95
|
|
|
67
96
|
/* The files whose columns are built, in the order of the columns: what is switched off is not among them, which is
|
|
@@ -141,7 +170,9 @@ function appRow(cache, r, span) {
|
|
|
141
170
|
else commit.appendChild(appCommit(appData.rows[i]));
|
|
142
171
|
row.appendChild(commit);
|
|
143
172
|
const cells = appEl('div', 'cells');
|
|
144
|
-
|
|
173
|
+
/* The numbers stand beside the commit column rather than under it: the column is pinned over the content, so what the
|
|
174
|
+
* grid holds begins where the column ends. */
|
|
175
|
+
cells.style.left = (APP_COMMIT + span.c0 * APP_COL) + 'px';
|
|
145
176
|
for (let c = span.c0; c <= span.c1; c++) cells.appendChild(appCell(model, c, now, cache));
|
|
146
177
|
row.appendChild(cells);
|
|
147
178
|
return row;
|
|
@@ -173,7 +204,7 @@ function appHead(cache, span) {
|
|
|
173
204
|
const count = cache.keys.length;
|
|
174
205
|
const g0 = Math.floor(span.c0 / count);
|
|
175
206
|
const g1 = Math.floor(span.c1 / count);
|
|
176
|
-
const left = (g0 * count * APP_COL) + 'px';
|
|
207
|
+
const left = (APP_COMMIT + g0 * count * APP_COL) + 'px';
|
|
177
208
|
const head = appEl('div', 'head');
|
|
178
209
|
head.appendChild(appEl('div', 'c-commit', appUi.commit));
|
|
179
210
|
const groups = appEl('div', 'hgroups');
|
|
@@ -214,7 +245,7 @@ export function appSpan(cache) {
|
|
|
214
245
|
cache.slot = cache.list.map((i) => cache.rank[i]);
|
|
215
246
|
const cols = count * (cache.list.length + 1);
|
|
216
247
|
const rows = appData.rows.length + 1;
|
|
217
|
-
cache.grid.style.width = (cols * APP_COL) + 'px';
|
|
248
|
+
cache.grid.style.width = (APP_COMMIT + cols * APP_COL) + 'px';
|
|
218
249
|
cache.grid.style.height = (APP_HEAD + rows * APP_ROW) + 'px';
|
|
219
250
|
if (count === 0) return { r0: 0, r1: -1, c0: 0, c1: -1 };
|
|
220
251
|
const high = shell.clientHeight || APP_MIN_ROWS * APP_ROW;
|
|
@@ -237,11 +268,11 @@ export function appWindow(cache, redraw) {
|
|
|
237
268
|
const was = cache.win;
|
|
238
269
|
cache.win = span;
|
|
239
270
|
if (redraw === true || was === null || was.c0 !== span.c0 || was.c1 !== span.c1) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
cache.grid.
|
|
271
|
+
/* The grid holds the header and the rows of the window and nothing else, so the whole of it is thrown away rather
|
|
272
|
+
* than tracked piece by piece — and the rows that are made below say in the map that they are there. */
|
|
273
|
+
cache.grid.textContent = '';
|
|
243
274
|
cache.rows.clear();
|
|
244
|
-
|
|
275
|
+
cache.grid.appendChild(appHead(cache, span));
|
|
245
276
|
for (let r = span.r0; r <= span.r1; r++) appPlace(cache, r, span);
|
|
246
277
|
return;
|
|
247
278
|
}
|
package/src/table.css
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
/* The report's table: a grid of plain elements rather than a `<table>`, and only the part of it the reader looks at is
|
|
2
2
|
* built (`src/page/table.js` says why, and what was measured).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Everything here hangs off `#grid`, and that is not a habit: this sheet is the package's **shared** part (`src/css.js`)
|
|
5
|
+
* while the page brings a `.panel .row` of its own, and an unscoped rule for the table's rows took that row out of the
|
|
6
|
+
* flow with it (the panel's switches stood on top of one another until the rules were scoped).
|
|
7
|
+
*
|
|
8
|
+
* Four figures are the whole geometry — a metric column, a row, the header and the commit column — and they are written
|
|
9
|
+
* twice, here and in the script that counts the window's ordinals in them. Two copies of a number are two answers waiting
|
|
10
|
+
* to happen, hence `test/page-grid.test.js` reads these four declarations and holds the script to them.
|
|
11
|
+
*
|
|
12
|
+
* One border: 1px of one colour — a row's bottom edge, a group's left edge, the lines of the header. A heavier line or a
|
|
13
|
+
* second colour would promise a hierarchy to be worked out rather than read.
|
|
9
14
|
*
|
|
10
15
|
* One type face for the whole table; numbers line up by their digits thanks to tabular-nums rather than through a
|
|
11
|
-
* monospaced font. Every column is the same width and fixed: the numbers are short and of one kind, and a width that
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
16
|
+
* monospaced font. Every column is the same width and fixed: the numbers are short and of one kind, and a width that came
|
|
17
|
+
* out of the text would have to be measured over every cell of the column — the very cost this step removed. The header is
|
|
18
|
+
* two lines of one row each — `--head` is exactly two `--row` — and a name too long for its group is cut with an ellipsis
|
|
19
|
+
* rather than wrapped (`appCaption`) or run over its neighbour.
|
|
15
20
|
*/
|
|
16
21
|
#grid {
|
|
17
22
|
--col: 70px;
|
|
18
23
|
--row: 25px;
|
|
19
|
-
--head:
|
|
24
|
+
--head: 50px;
|
|
20
25
|
--commit: 220px;
|
|
21
26
|
--grid-line: rgba(127, 127, 127, .25);
|
|
22
|
-
--grid-edge: rgba(127, 127, 127, .35);
|
|
23
27
|
position: relative;
|
|
24
28
|
font-variant-numeric: tabular-nums;
|
|
25
29
|
}
|
|
@@ -28,8 +32,8 @@
|
|
|
28
32
|
* painting — no layout and no script — while the width of the content gives the scrollbar its length and the script its
|
|
29
33
|
* ordinals. The cells of a row are one grid of fixed tracks (`grid-auto-columns`): a cell cannot change the width of a
|
|
30
34
|
* column, and it cannot push its neighbour. */
|
|
31
|
-
.row { position: absolute; left: 0; width: 100%; height: var(--row); }
|
|
32
|
-
.cells {
|
|
35
|
+
#grid .row { position: absolute; left: 0; width: 100%; height: var(--row); }
|
|
36
|
+
#grid .cells {
|
|
33
37
|
position: absolute;
|
|
34
38
|
top: 0;
|
|
35
39
|
display: grid;
|
|
@@ -37,32 +41,42 @@
|
|
|
37
41
|
grid-auto-columns: var(--col);
|
|
38
42
|
height: var(--row);
|
|
39
43
|
}
|
|
40
|
-
.cells > span, .hgroups > span, .hmetrics > span { box-sizing: border-box; white-space: nowrap; }
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
#grid .cells > span, #grid .hgroups > span, #grid .hmetrics > span { box-sizing: border-box; white-space: nowrap; }
|
|
45
|
+
/* Two pixels of padding and a line of twenty: with the border, a cell is one `--row` high — the same for the header's
|
|
46
|
+
* cells as for the numbers, or the header would be a band of another rhythm. */
|
|
47
|
+
#grid .cells > span {
|
|
48
|
+
padding: 2px 4px;
|
|
49
|
+
line-height: 20px;
|
|
50
|
+
border-bottom: 1px solid var(--grid-line);
|
|
51
|
+
}
|
|
52
|
+
#grid .num { text-align: right; }
|
|
43
53
|
/* The left edge of a group — the total, and every file — is carried by the group's first metric. One class instead of
|
|
44
54
|
* the four rules of "which metric is switched on": the window is built out of the files that are on, so which column
|
|
45
55
|
* stands first in a group is the page's own answer rather than something the styling has to work out. */
|
|
46
|
-
.cells > .g, .hmetrics > .g, .hgroups > .gh { border-left: 1px solid var(--grid-
|
|
56
|
+
#grid .cells > .g, #grid .hmetrics > .g, #grid .hgroups > .gh { border-left: 1px solid var(--grid-line); }
|
|
47
57
|
|
|
48
58
|
/* The header: one row of groups (the total and the files) over one row of metrics, stuck to the top edge of the shell,
|
|
49
59
|
* and the commit column stuck to its left edge. Both carry an opaque background (Canvas), or the numbers would show
|
|
50
|
-
* through them while the reader scrolls.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.hgroups {
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
.hgroups > span
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
* through them while the reader scrolls. The two lines are placed by `top` and are one row high each, so the header
|
|
61
|
+
* aligns with the table below it rather than standing a size of its own. */
|
|
62
|
+
#grid .head { position: sticky; top: 0; left: 0; z-index: 3; height: var(--head); background: Canvas; }
|
|
63
|
+
#grid .hgroups, #grid .hmetrics { position: absolute; display: grid; grid-auto-flow: column; grid-auto-columns: var(--col); }
|
|
64
|
+
#grid .hgroups { top: 0; height: var(--row); }
|
|
65
|
+
#grid .hmetrics { top: var(--row); height: var(--row); }
|
|
66
|
+
#grid .hgroups > span, #grid .hmetrics > span {
|
|
67
|
+
padding: 2px 4px;
|
|
68
|
+
line-height: 20px;
|
|
69
|
+
border-bottom: 1px solid var(--grid-line);
|
|
70
|
+
}
|
|
71
|
+
/* A file's name stands over its group of metrics and is centred on it: the group is a column of numbers, and a caption
|
|
72
|
+
* that began at its left edge would carry the eye to the wrong place. */
|
|
73
|
+
#grid .hgroups > span { text-align: center; overflow: hidden; text-overflow: ellipsis; }
|
|
74
|
+
#grid .hmetrics > span { text-align: center; color: CanvasText; }
|
|
61
75
|
|
|
62
|
-
/* The commit column: a sticky cell of every row, so the commit a number belongs to is in sight while the reader
|
|
63
|
-
*
|
|
64
|
-
* tooltip. */
|
|
65
|
-
.c-commit {
|
|
76
|
+
/* The commit column: a sticky cell of every row, so the commit a number belongs to is in sight while the reader scrolls
|
|
77
|
+
* sideways. Its width is fixed and is the script's `APP_COMMIT` as well — the numbers are placed from the left edge of
|
|
78
|
+
* the grid *beside* the column, and a caption longer than it is clipped (the whole subject stands in the tooltip). */
|
|
79
|
+
#grid .c-commit {
|
|
66
80
|
position: sticky;
|
|
67
81
|
left: 0;
|
|
68
82
|
z-index: 2;
|
|
@@ -79,24 +93,24 @@
|
|
|
79
93
|
white-space: nowrap;
|
|
80
94
|
border-bottom: 1px solid var(--grid-line);
|
|
81
95
|
}
|
|
82
|
-
.c-commit a { color: inherit; }
|
|
83
|
-
.head .c-commit { z-index: 6; height: var(--head); line-height: var(--head);
|
|
96
|
+
#grid .c-commit a { color: inherit; }
|
|
97
|
+
#grid .head .c-commit { z-index: 6; height: var(--head); line-height: var(--head); }
|
|
84
98
|
/* This block fills the commit column: the cell clips what does not fit, and the subject is what gives way (the flex
|
|
85
99
|
* item of `flex: 1 1 auto`), while the date and the journal mark keep their own width. */
|
|
86
|
-
.clip { display: flex; align-items: baseline; gap: 6px; width: 100%; }
|
|
87
|
-
.when { flex: none; opacity: .7; }
|
|
88
|
-
.subj { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; display: block; }
|
|
89
|
-
.subj.plain { opacity: .7; }
|
|
90
|
-
.sect { flex: none; opacity: .7; font-size: 11px; text-decoration: none; border-bottom: 1px dotted currentColor; }
|
|
100
|
+
#grid .clip { display: flex; align-items: baseline; gap: 6px; width: 100%; }
|
|
101
|
+
#grid .when { flex: none; opacity: .7; }
|
|
102
|
+
#grid .subj { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; display: block; }
|
|
103
|
+
#grid .subj.plain { opacity: .7; }
|
|
104
|
+
#grid .sect { flex: none; opacity: .7; font-size: 11px; text-decoration: none; border-bottom: 1px dotted currentColor; }
|
|
91
105
|
/* Growth green, fall red — by agreement with whoever asked for the report (growth is "more logic" rather than alarm). */
|
|
92
|
-
.up { color: #1e8449; }
|
|
93
|
-
.down { color: #c0392b; }
|
|
94
|
-
.miss { opacity: .5; }
|
|
95
|
-
/* The top row holds the current sizes: it is also what explains what the deltas below refer to
|
|
96
|
-
|
|
97
|
-
.row.now > .c-commit
|
|
106
|
+
#grid .up { color: #1e8449; }
|
|
107
|
+
#grid .down { color: #c0392b; }
|
|
108
|
+
#grid .miss { opacity: .5; }
|
|
109
|
+
/* The top row holds the current sizes: it is also what explains what the deltas below refer to — by its weight rather
|
|
110
|
+
* than by a heavier line, which would be a second border among the rows. */
|
|
111
|
+
#grid .row.now > .c-commit { font-weight: 600; }
|
|
98
112
|
/* A row's highlight is laid over rather than swapped in: the sticky column has to stay opaque, or numbers show through
|
|
99
113
|
* it while the reader scrolls sideways. */
|
|
100
|
-
.row:hover > .c-commit, .row:hover > .cells > span {
|
|
114
|
+
#grid .row:hover > .c-commit, #grid .row:hover > .cells > span {
|
|
101
115
|
background-image: linear-gradient(rgba(127, 127, 127, .08), rgba(127, 127, 127, .08));
|
|
102
116
|
}
|