@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 CHANGED
@@ -10,7 +10,7 @@ shows.
10
10
 
11
11
  ## Status
12
12
 
13
- **Release 2.8.0 (2026-09-18).** The tool lives as a package of its own: the registry name is
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. **The checks of the old table are deactivated while it is rebuilt:** `test/page-view.test.js`, `page-cols`,
37
- `page-tree` and `page-choice` skip every check with its reason at the top of the file, and this document names them
38
- where they are the promise-holders of what they described; `test/page-grid.test.js` is what guards the window meanwhile.
39
- The figures and the reasoning stand in `worklog/0208-table-window.md`. The checks grow with the work: 81 → **88** in the
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` | **88 of 193** |
241
- | Full — release and CI | `pnpm test:all` | **193** |
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 all 186 with the same files, the fast one takes part
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 is cut with an
371
- ellipsis rather than wrapped (the header is one line high and the whole name stands in the tooltip). The geometry is
372
- three figures — a column, a row, the header written in the styling and in the script that counts the window's ordinals
373
- in them, and `test/page-grid.test.js` reads both and holds them together.
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 the last commit touched come first.** The report is rebuilt after every commit, and a
391
- reader's first question is what that edit brought. The mark comes from the history rather than from the
392
- numbers an edit that changed no size is an edit too and it is taken from the last commit that
393
- touched at least one column, counting back from the top: a commit that went past the columns, above all
394
- the report itself, which the hook commits, is skipped, or the mark would depend on the report's own
395
- commit, the same run would give different bytes and the hook would commit the report a second time.
396
- Inside each part the order stays as it comes from the settings (the sort is stable): the order of the
397
- columns is what the reader is used to, and his choice of files does not rearrange it
398
- (`test/page-grid.test.js`, the contract's `last` field).
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 two figures of the window — 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 |
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 panel's switches, 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 |
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, and every row of the window against the engine's own calculation |
637
- | `test/page-view.test.js` | The assembled page: pasted with no copy of the calculation, self-contained, the empty states, the styling, the switches, a click that makes no table and the carried totals against the engine's own sums **deactivated** while the table is rebuilt, every check skipped with its reason in the file |
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 — **deactivated** with the table, the checks skipped with their reason |
639
- | `test/page-choice.test.js` | The reader's choice and the work a click starts: 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 the drawing of a long switch in a task of its own behind the stripe — **deactivated**: the choice and the link are the same, while the counts and the stripe are not |
640
- | `test/page-cols.test.js` | The fixed layout the page carries: a column's width is counted from the model rather than measured in a laid-out cell, the clip keeps a caption inside its column, the sticky header and commit column keep their edges, and the table names its own width **deactivated** with the table: the columns are one fixed width now |
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.0
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.0`, which is also the one in the example (forty characters work as well, but they have to be copied
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.0",
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.7.0",
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
- --line: rgba(127, 127, 127, .32);
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, .panel .cap {
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: var(--ink); }
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
- /* There is no stripe of progress over the page any longer, and that is a measurement rather than an omission: it stood
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 needs no rule of its own here any more: its width is the measure `--clip` of the shared part
235
- * (`src/table.css`), and on a narrow screen that measure stands as it is. A shorter one would be `--clip: 150px`
236
- * here — the adaptation the shared part cannot make for itself. */
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, as before: `appPaint` draws the whole view (the first drawing, a
8
- * record from the browser's memory, a link in the address), while a click on a box does the same work for the choice
9
- * it made — and the difference between them is only how much of the view moved.
10
- *
11
- * What a click costs now: the window of rows and columns is built again — a few hundred cells, measured at 1–5 ms on
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 file switched by the reader: the view, the window (its column is simply not among the columns that are built),
46
- * the fields it shows in. The message about a link fades here: by this action the reader has read it. */
47
- export function appSwitch(i, on) {
48
- if (appView.files[i] === on) return;
49
- appView.files[i] = on;
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([i]);
48
+ if (touched.length > 0) appPanelState(touched);
53
49
  appWrite();
54
50
  appNotice('');
55
51
  }
56
52
 
57
- /* A group switched at once a folder or a category: the same work per file, then the fields of the files the choice
58
- * really reached (switching a folder on when a part of it was already on touches only the rest, and a field that did
59
- * not move is not written). The columns of the whole group leave or enter the window together. */
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
- appWindow(appCache, true);
64
- appCounts();
65
- appPanelState(touched);
66
- appWrite();
67
- appNotice('');
65
+ appChanged(touched);
68
66
  }
69
67
 
70
- /* One metric switched: the columns of that metric are not among the columns that are built any longer, so this is the
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
- appWindow(appCache, true);
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 is the list of skipped commits: it changes with the report's own
130
- * commit (one with nothing to say lands in the list), and the file would stop being a **fixed point** — a rebuild
131
- * after its own commit would yield different bytes and the hook would commit the report forever. The page has no use
132
- * for the list at all: it does not show it. It stays available to the reader `--data`, `--json` and `explain`
133
- * answer from the same run.
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', 'last', 'hist'];
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
- /* Every measured file of a subtree what a folder's switch controls: a file outside the report has nothing to
26
- * switch. */
27
- function appIndexes(node) {
28
- const out = node.files.slice();
29
- node.dirs.forEach((sub) => { out.push(...appIndexes(sub)); });
30
- return out;
31
- }
32
-
33
- /* How many files a subtree holds — including the ones that made it into no report. */
34
- function appCount(node) {
35
- let n = node.files.length + node.others.length;
36
- node.dirs.forEach((sub) => { n += appCount(sub); });
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 idx = appIndexes(sub);
52
- const total = appCount(sub);
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: appIndexes(node.dirs.get(name)).length > 0 }));
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
- leaves.filter((leaf) => leaf.entry !== null).forEach((leaf) => list.appendChild(appLeaf(leaf)));
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(box) {
241
- const boxes = appRowBoxes(box);
241
+ function appDirState(path) {
242
+ const boxes = appRowBoxes(appFields.dir[path]);
242
243
  const on = boxes.filter((b) => b.checked).length;
243
- const input = box.querySelector('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's field through the files below it: every folder on the path of a switched file shows the
271
- * share of what is left on. A folder without measured files has a field of its own too it is off and unavailable,
272
- * which is not the reader's state and must not be overwritten here. */
273
- function appDirsOf(indexes) {
274
- const seen = {};
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
- const key = appData.files[i].category;
289
- if (seen[key] === true || appFields.cat[key] === undefined) return;
290
- seen[key] = true;
291
- appCatState(key);
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
- appDirsOf(indexes);
303
- appCatsOf(indexes);
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(appFields.dir[path]));
301
+ Object.keys(appFields.dir).forEach((path) => appDirState(path));
313
302
  }
@@ -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`: the calculation (`rowModel`, `totalsOf`, `cellParts`,
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 for everything: it goes both into the memory and into the address, so there are no two formats
110
- * of one state. Only what is switched off is kept, by name: "switched on" and "no record" are the same state, which is
111
- * why turning every checkbox back on removes the record instead of leaving a trace indistinguishable from a choice. */
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
- return { v: 1, passport: appPassport(), metrics: metrics, files: files };
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
- /* The record goes into the browser's memory and nowhere else: it is written on the click itself, which is what survives
126
- * a closing, and the page's address keeps a clean tail the report is a local page whose address is copied as it is,
127
- * and a reader's choice belongs in the browser that made it rather than in the tab's title bar. What a link sent from an
128
- * earlier release holds is still read (`appLinkUse`), and it is not written into the reader's memory: what came in is
129
- * not his choice until he changes something. */
130
- export function appWrite() {
131
- if (appTransient) return;
132
- const rec = appRecord();
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 (empty) window.localStorage.removeItem(appKey);
136
- else window.localStorage.setItem(appKey, JSON.stringify(rec));
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 (!metricKeys[k]) n++; });
185
- Object.keys(rec.files || {}).forEach((k) => { if (!known[k]) n++; });
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
- /* Reading: only a record of ours of our format version and our passport. Another report's record lies under another key,
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
- let text = null;
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, and unfolding nothing is the state the page opens in: then the record is not
252
- * kept at all rather than being kept empty. `appFoldKey` is set with the model (`appBoot`), for the reason the key
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
- let text = null;
256
- try {
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 rec = { v: 1, passport: appPassport(), open: Object.assign({}, appView.open) };
277
- try {
278
- if (Object.keys(rec.open).length === 0) window.localStorage.removeItem(appFoldKey);
279
- else window.localStorage.setItem(appFoldKey, JSON.stringify(rec));
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 = 44;
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 the last commit touched come first the report is rebuilt after every
57
- * commit, and a reader's first question is what that edit brought. Inside each part the order is the settings', and
58
- * it depends on the files rather than on the choice: that is what lets a column be switched off without moving the
59
- * others. */
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
- const files = [];
62
- appData.files.forEach((_f, i) => files.push(i));
63
- files.sort((a, b) => (appData.last[a] === true ? 0 : 1) - (appData.last[b] === true ? 0 : 1));
64
- return files;
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
- cells.style.left = (span.c0 * APP_COL) + 'px';
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
- const head = cache.grid.querySelector('.head');
241
- if (head !== null) head.remove();
242
- cache.grid.insertBefore(appHead(cache, span), cache.grid.firstChild);
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
- [...cache.grid.querySelectorAll('.row')].forEach((row) => row.remove());
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
- * Three figures are the whole geometry a metric column, a row and the header and they are written twice, here and
5
- * in the script that counts the window's ordinals in them. Two copies of a number are two answers waiting to happen,
6
- * hence `test/page-grid.test.js` reads these three declarations and holds the script to them. The fourth, the commit
7
- * column's width, is the styling's alone: the script places the numbers from the left edge of the grid, and the caption
8
- * is pinned over them.
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
- * came out of the text would have to be measured over every cell of the column — the very cost this step removed.
13
- * A file's caption over its group is cut with an ellipsis rather than wrapped (`appCaption`), because the header is one
14
- * line high and the grid is built of rows of one height.
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: 44px;
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
- .cells > span { padding: 2px 4px; line-height: 20px; border-bottom: 1px solid var(--grid-line); }
42
- .num { text-align: right; }
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-edge); }
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
- .head { position: sticky; top: 0; left: 0; z-index: 3; height: var(--head); background: Canvas; }
52
- .hgroups, .hmetrics { position: absolute; display: grid; grid-auto-flow: column; grid-auto-columns: var(--col); }
53
- .hgroups { top: 0; height: 22px; }
54
- .hmetrics { top: 22px; height: 22px; }
55
- .hgroups > span, .hmetrics > span { padding: 0 4px; line-height: 20px; border-bottom: 2px solid var(--grid-edge); }
56
- .hgroups > span { text-align: left; }
57
- /* A file's name is one line of a fixed room: a name that does not fit is cut with an ellipsis (`appCaption` puts the
58
- * whole of it into the tooltip) rather than wrapped — the grid is rows of one height — or run over its neighbour. */
59
- .hgroups > span { overflow: hidden; text-overflow: ellipsis; }
60
- .hmetrics > span { text-align: center; color: CanvasText; }
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
- * scrolls sideways. Its width is fixed, and a caption longer than it is clipped — the whole subject stands in the
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); border-bottom-width: 2px; }
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
- .row.now > .c-commit { font-weight: 600; }
97
- .row.now > .c-commit, .row.now > .cells > span { border-bottom-width: 2px; }
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
  }