@vernikr/size-report 2.8.1 → 2.8.3
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 +52 -18
- package/package.json +2 -2
- package/src/page/app.css +3 -8
- package/src/page/app.js +23 -29
- package/src/page/panel.js +34 -45
- package/src/page/state.js +48 -63
- package/src/page/table.js +106 -49
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ shows.
|
|
|
10
10
|
|
|
11
11
|
## Status
|
|
12
12
|
|
|
13
|
-
**Release 2.8.
|
|
13
|
+
**Release 2.8.3 (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,38 @@ 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.3 is about what a sideways scroll of the report costs, and its figures are measured rather than retold. **A step
|
|
23
|
+
sideways moves the window instead of building it:** a row is made once and lives through the move, keeping its node and
|
|
24
|
+
its numbers — the window's columns are the range of that row's numbers, so the columns that entered it are made and the
|
|
25
|
+
ones that left are dropped, the header's line of names and its line of metrics by the same rule. A switch of the
|
|
26
|
+
reader's own and the shell's resize are the one thing that cannot be a move — a column that is not built cannot be
|
|
27
|
+
shown — and they build the window from nothing as they did. **Measured on this repository's own report in live Chrome**
|
|
28
|
+
(`probes/step-12-window.mjs`, two rounds, 1440 × 900): the page's own handling of one step **10.4 → 0.6 ms** (worst
|
|
29
|
+
19.2 → 3.6) and the layout the step forces **14.4 → 5.9 ms**; over a sweep of forty steps the browser's own accounting
|
|
30
|
+
gives layout **0.605 → 0.246 s**, style recalculation 0.169 → 0.056 s and task time **1.281 → 0.350 s**. The checks go
|
|
31
|
+
86 → **87** in the fast profile and 191 → **192** in the full one: `test/page-grid.test.js` holds the rule by node
|
|
32
|
+
identity — a page that built the same numbers again would read the same — and by the numbers of the window built at
|
|
33
|
+
that place in one leap. The figures and the reasoning stand in `worklog/0211-sideways-step.md`.
|
|
34
|
+
|
|
35
|
+
2.8.2 is about the page's own sources and the checks that guard them, and its figures are measured rather than retold.
|
|
36
|
+
**The four page suites that went quiet with the `<table>` are alive again against the window:** `test/page-view.test.js`,
|
|
37
|
+
`page-cols`, `page-tree` and `page-choice` read the grid and the panel they guard rather than markup that no longer
|
|
38
|
+
exists, and what they promise is what they hold — the pasted program with no second calculation, the sparse block, the
|
|
39
|
+
recount by the choice, the order of the columns, the tree's folds and its memory, the link's refusals. **Their readers
|
|
40
|
+
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
|
|
41
|
+
window built, the numbers of a row and its captions, a folder's and a category's switch, the place the shell stands at),
|
|
42
|
+
and the copies the suites carried left with it. **And the page's chapters shed the duplication the same request
|
|
43
|
+
found:** the choice's record and the folded tree share one reading and one writing of the browser's memory
|
|
44
|
+
(`appLoad`/`appKeep`), a folder's two figures come from one walk of its subtree (`appSub`), the three walks over the
|
|
45
|
+
metrics and the files of a record became the panels' one question asked twice (`appReach`), and the three paths of a
|
|
46
|
+
click became one drawing (`appChanged`) — while two rules that never took effect went with them (a dead `.cap` of the
|
|
47
|
+
styling, an undefined colour under the folding sign). Measured in bytes for the same history: the chapters **−745 B**
|
|
48
|
+
(37 lines), the suites **−18 462 B** (251 lines), the harness **+4 328 B** — the tree loses **14 879 B and 220 lines**,
|
|
49
|
+
and the artifact **85 730 → 84 991 B**, its program and its styling being exactly what the chapters are. The checks go
|
|
50
|
+
89 → **86** in the fast profile and 194 → **191** in the full one: what a reader of the page does is one place now
|
|
51
|
+
rather than five, and a check that read the table it no longer has is not a check. The figures and the reasoning stand
|
|
52
|
+
in `worklog/0210-page-suites-on-the-window.md`.
|
|
53
|
+
|
|
22
54
|
2.8.1 is the drawing of the page put right after its table became a window, and its figures are measured rather than
|
|
23
55
|
retold. **The panel and the table were sharing one class:** the table's rows were styled by a bare `.row` while the
|
|
24
56
|
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
|
|
@@ -49,11 +81,10 @@ stripe over a drawing is gone with the freeze it was drawn for**: a switch costs
|
|
|
49
81
|
for an indicator to indicate, and `src/page/work.js` left the tree with it. A virtualizer library was measured and not
|
|
50
82
|
taken — `@tanstack/virtual-core` is ~6.7 kB gzip and headless, `virtua`'s grid is experimental and `Clusterize.js`
|
|
51
83
|
knows rows and not columns — because each would be vendored into the artifact, which the report then measures as its own
|
|
52
|
-
bytes.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
fast profile and 186 → **193** in the full one.
|
|
84
|
+
bytes. The checks of the old table went quiet with it, each naming its reason, and 2.8.2 brought them back against the
|
|
85
|
+
window; `test/page-grid.test.js` is what guards the window itself. The figures and the reasons stand in
|
|
86
|
+
`worklog/0208-table-window.md`. The checks grow with the work: 81 → **88** in the fast profile and 186 → **193**
|
|
87
|
+
in the full one.
|
|
57
88
|
|
|
58
89
|
2.7.0 is about the page and what a reader does with it, and its figures are measured rather than retold.
|
|
59
90
|
**The report's address stays clean:** a switch writes the record into the browser's memory and nothing into the
|
|
@@ -253,8 +284,8 @@ commits and installs hooks; the reason for each expensive file is named line by
|
|
|
253
284
|
|
|
254
285
|
| Run | Command | Checks |
|
|
255
286
|
|---|---|---|
|
|
256
|
-
| Fast — every edit | `pnpm test` | **
|
|
257
|
-
| Full — release and CI | `pnpm test:all` | **
|
|
287
|
+
| Fast — every edit | `pnpm test` | **87 of 192** |
|
|
288
|
+
| Full — release and CI | `pnpm test:all` | **192** |
|
|
258
289
|
|
|
259
290
|
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
|
|
260
291
|
expensive work cannot quietly move into the fast one. Two declarations guard that:
|
|
@@ -389,7 +420,10 @@ right edge of the pinned commit column rather than under it, and every line of t
|
|
|
389
420
|
cell, along a group's left edge — is one pixel of one colour: a heavier line would promise a hierarchy to work out
|
|
390
421
|
rather than read. The geometry is four figures — a column, a row, the header, the pinned column — written in the styling
|
|
391
422
|
and in the script that counts the window's ordinals in them, and `test/page-grid.test.js` reads both and holds them
|
|
392
|
-
together.
|
|
423
|
+
together. **A step sideways moves that window rather than building it:** the rows live through the move and only the
|
|
424
|
+
columns that entered are made, at 10.4 → **0.6 ms** for the page's own handling of the step, 14.4 → **5.9 ms** for the
|
|
425
|
+
layout it forces and 1.281 → **0.350 s** of task time over a sweep of forty steps
|
|
426
|
+
(`probes/step-12-window.mjs`).
|
|
393
427
|
|
|
394
428
|
**A click builds the window again, and that is cheap now.** A file switched off is simply not among the columns that
|
|
395
429
|
are built, and a metric switched off not among the metrics: there is nothing to hide and nothing to carry, and the
|
|
@@ -568,7 +602,7 @@ acceptance for each.
|
|
|
568
602
|
| `tools/parity-freeze.js` | Takes the parity reference (`pnpm run parity`): with the frozen copy, at the project revision from the manifest — `--json`, the config, the artifact's hash, the tool's hash |
|
|
569
603
|
| `tools/make-fixture.js` | Assembles the synthetic fixture (`pnpm run fixture`): a deterministic history with traps plus the reference numbers |
|
|
570
604
|
| `tools/synthetic/` | The subjects of that assembly, one per matter: `repo.js` — how git is spoken to (pinned time, author, settings), `content.js` — what the files hold, `history.js` — which commits come of it, `note.js` — the fixture's note with the list of traps |
|
|
571
|
-
| `probes/` | The scripted measurements behind `plans/2026-09-17-page-perf/`: one file per step, run by hand against live Chrome at the debug port — the fixed layout, where `content-visibility` acts at all,
|
|
605
|
+
| `probes/` | The scripted measurements behind `plans/2026-09-17-page-perf/`: one file per step, run by hand against live Chrome at the debug port — the fixed layout, where `content-visibility` acts at all, the price of a switch and of a step sideways — (`probes/README.md`). Outside the sensors' paths on purpose: they measure the product rather than being part of it, and a suite cannot see layout, paint or a browser's own skipping |
|
|
572
606
|
| `tools/parity-live.js` | Compares the engine with the live project on a clone: the numbers and the self-contained report at the path the consumer's settings give (`pnpm run parity:live`) |
|
|
573
607
|
| `tools/pack-check.js` | Assembles the tarball and checks that everything works from it: all sources arrived, the numbers and the report as from the repository (`pnpm run pack:check`) |
|
|
574
608
|
| `tools/check-standards.js` | Checks that both references reproduce: a re-take goes nowhere and is compared with what is committed (our files byte for byte, the bundle by content), and that the live-history bundle carries `HEAD` (`pnpm run check:standards`) |
|
|
@@ -604,7 +638,7 @@ acceptance for each.
|
|
|
604
638
|
| `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 |
|
|
605
639
|
| `src/page/dom.js` | The page's nodes: the small helpers of markup (`appEl`, `appBox`) — one set for the panel and the table alike |
|
|
606
640
|
| `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 |
|
|
607
|
-
| `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 |
|
|
641
|
+
| `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), moved by a scroll rather than built again (a row lives through a step sideways and only the columns that entered it are made), 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 |
|
|
608
642
|
| `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 |
|
|
609
643
|
| `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` |
|
|
610
644
|
| `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 |
|
|
@@ -637,7 +671,7 @@ acceptance for each.
|
|
|
637
671
|
| `test/api.test.js` | The package's public API: the list of names is frozen, and splitting the engine may not change it |
|
|
638
672
|
| `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`) |
|
|
639
673
|
| `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 |
|
|
640
|
-
| `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
|
|
674
|
+
| `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 |
|
|
641
675
|
| `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 |
|
|
642
676
|
| `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 |
|
|
643
677
|
| `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 |
|
|
@@ -654,10 +688,10 @@ acceptance for each.
|
|
|
654
688
|
| `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 |
|
|
655
689
|
| `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 |
|
|
656
690
|
| `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 |
|
|
657
|
-
| `test/page-view.test.js` | The assembled page: pasted with no copy of the calculation, self-contained, the
|
|
658
|
-
| `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
|
|
659
|
-
| `test/page-choice.test.js` | The reader's choice
|
|
660
|
-
| `test/page-cols.test.js` | The
|
|
691
|
+
| `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 |
|
|
692
|
+
| `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 |
|
|
693
|
+
| `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 |
|
|
694
|
+
| `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 |
|
|
661
695
|
| `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 |
|
|
662
696
|
| `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 |
|
|
663
697
|
| `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 |
|
|
@@ -737,7 +771,7 @@ The same release can be taken by a reference to the repository — installation
|
|
|
737
771
|
registry, but stays tied to a revision:
|
|
738
772
|
|
|
739
773
|
```bash
|
|
740
|
-
pnpm add -D github:vernikr/size-report#v2.8.
|
|
774
|
+
pnpm add -D github:vernikr/size-report#v2.8.3
|
|
741
775
|
```
|
|
742
776
|
|
|
743
777
|
With no network (or nothing to fetch from codeload) — the tarball: `pnpm pack` in the package clone, then
|
|
@@ -749,7 +783,7 @@ the branch moves on the installation fails with `Could not resolve <sha> to a co
|
|
|
749
783
|
observation rather than reasoning: the short pin `6530237` installed while `main` stood on it and stopped
|
|
750
784
|
working at the very next commit, while the same sha in full installed. A branch name (`#main`) and a tag
|
|
751
785
|
are both accepted, but a branch is a moving target and a tag is constant: this release stands on the tag
|
|
752
|
-
`v2.8.
|
|
786
|
+
`v2.8.3`, which is also the one in the example (forty characters work as well, but they have to be copied
|
|
753
787
|
out of the history by eye).
|
|
754
788
|
|
|
755
789
|
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.3",
|
|
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.8.
|
|
68
|
+
"@vernikr/size-report": "2.8.2",
|
|
69
69
|
"c8": "10",
|
|
70
70
|
"dependency-cruiser": "17",
|
|
71
71
|
"eslint": "^9.18.0",
|
package/src/page/app.css
CHANGED
|
@@ -45,7 +45,7 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
45
45
|
}
|
|
46
46
|
.panel fieldset { margin: 0; padding: 0; border: 0; }
|
|
47
47
|
.panel fieldset + fieldset { margin-top: 10px; }
|
|
48
|
-
.panel legend
|
|
48
|
+
.panel legend {
|
|
49
49
|
padding: 0;
|
|
50
50
|
color: var(--muted);
|
|
51
51
|
font-size: 11px;
|
|
@@ -53,7 +53,6 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
53
53
|
text-transform: uppercase;
|
|
54
54
|
letter-spacing: .07em;
|
|
55
55
|
}
|
|
56
|
-
.panel .cap { display: block; margin-bottom: 4px; }
|
|
57
56
|
.panel .row { display: flex; flex-wrap: wrap; gap: 3px 6px; align-items: center; }
|
|
58
57
|
/* The way of counting is visible text under the switches: the token dictionary and the way of compression come from the
|
|
59
58
|
* settings of the run rather than from a checkbox, so pointing a mouse is not enough — the reader has to see what produced
|
|
@@ -91,7 +90,7 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
91
90
|
cursor: pointer;
|
|
92
91
|
user-select: none;
|
|
93
92
|
}
|
|
94
|
-
.panel .tree li > .fold:hover { color:
|
|
93
|
+
.panel .tree li > .fold:hover { color: CanvasText; }
|
|
95
94
|
|
|
96
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
|
|
97
96
|
* input) and assistive technology all reach it. */
|
|
@@ -131,11 +130,7 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
|
|
|
131
130
|
/* Numbers are denser than the page's text: there are more of them, they are shorter, and they are read by their digits. */
|
|
132
131
|
#grid { font-size: 12.5px; }
|
|
133
132
|
|
|
134
|
-
/*
|
|
135
|
-
* over the browser's relayout of the whole table, and the table is built as a window now — a switch costs a few
|
|
136
|
-
* milliseconds (`src/page/table.js`), so an indicator would be a promise the page does not keep.
|
|
137
|
-
*
|
|
138
|
-
* 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
|
|
139
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
|
|
140
135
|
* of its own. */
|
|
141
136
|
.notice {
|
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/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/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
|
@@ -31,6 +31,14 @@ import { appData, appUi, appView } from './state.js';
|
|
|
31
31
|
* table, against 6.5 ms for building the whole window again). The header sticks to the shell's top and the commit
|
|
32
32
|
* column to its left (both `position: sticky`), so the row and the column a number belongs to are always in sight.
|
|
33
33
|
*
|
|
34
|
+
* **A step down the table and a step sideways cost the same kind of work.** A row is made once and then stands at its
|
|
35
|
+
* own place in the scrolled content while the window moves over it: the window's columns are the range of that row's
|
|
36
|
+
* numbers, so a step sideways makes the columns that entered and drops the ones that left (`appStrip`) — a cell per
|
|
37
|
+
* row, the header's two lines included. A row that left the window vertically is dropped and made again when the
|
|
38
|
+
* reader comes back to it, which is what makes a jump down the table cost the same as a step. Building the rows again
|
|
39
|
+
* on every sideways scroll — the first version of this step — is what `probes/step-12-window.mjs` measures against the
|
|
40
|
+
* splice (`worklog/0211-sideways-step.md`).
|
|
41
|
+
*
|
|
34
42
|
* The choice is applied by building the window again: the columns of a switched-off file are simply not among the
|
|
35
43
|
* columns that are built, so there is nothing to hide and nothing to recount — the totals are the sum over the files
|
|
36
44
|
* that are on, counted by `rowModel` for the rows the window holds (`src/derived.js`, one place for that
|
|
@@ -138,6 +146,27 @@ export function appState(metricsCount, filesCount) {
|
|
|
138
146
|
document.getElementById('shell').hidden = metricsCount === 0;
|
|
139
147
|
}
|
|
140
148
|
|
|
149
|
+
/* A strip of pieces of one range, moved to another: the pieces the two ranges share are kept, and only the ends are
|
|
150
|
+
* made and dropped. The unit of a strip is its child — a row's numbers are one column to a child, the header's line of
|
|
151
|
+
* names one group, its line of metrics one column — so the same movement serves all three, and a group of metrics
|
|
152
|
+
* cannot be split by the window (a column of numbers would lose its caption). A window that jumped clear of the old
|
|
153
|
+
* range — the reader dragged the scrollbar — has nothing to keep and the strip is made again; the empty range
|
|
154
|
+
* `[c, c - 1]` is how a strip that holds nothing yet says so. */
|
|
155
|
+
function appStrip(strip, was, now, make) {
|
|
156
|
+
const a = Math.max(was[0], now[0]);
|
|
157
|
+
const b = Math.min(was[1], now[1]);
|
|
158
|
+
if (a > b) {
|
|
159
|
+
strip.textContent = '';
|
|
160
|
+
for (let c = now[0]; c <= now[1]; c++) strip.appendChild(make(c));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
for (let n = a - was[0]; n > 0; n--) strip.firstChild.remove();
|
|
164
|
+
for (let n = was[1] - b; n > 0; n--) strip.lastChild.remove();
|
|
165
|
+
const edge = strip.firstChild;
|
|
166
|
+
for (let c = a - 1; c >= now[0]; c--) strip.insertBefore(make(c), edge);
|
|
167
|
+
for (let c = b + 1; c <= now[1]; c++) strip.appendChild(make(c));
|
|
168
|
+
}
|
|
169
|
+
|
|
141
170
|
/* One cell of the window: the column decides what it holds. The group is the column's ordinal divided by the number of
|
|
142
171
|
* metrics — `0` is the total over the files, `g ≥ 1` is the g-th column of the order — and it is the same arithmetic
|
|
143
172
|
* the header is placed by, so a column and its caption cannot drift apart. The model lists the *enabled* files in the
|
|
@@ -153,9 +182,13 @@ function appCell(model, c, now, cache) {
|
|
|
153
182
|
return appNum(now ? valueParts(cell) : cellParts(cell.value, cell.delta, '−'), cls);
|
|
154
183
|
}
|
|
155
184
|
|
|
156
|
-
/* A row of the window: the caption of the commit and
|
|
157
|
-
* state at HEAD (`nowModel`) rather than the last commit's cells: an absolute number stands in the
|
|
158
|
-
* the deltas below it add up to it.
|
|
185
|
+
/* A row of the window: the caption of the commit and an empty strip of numbers for the window to fill (`appCells`).
|
|
186
|
+
* The top row is the state at HEAD (`nowModel`) rather than the last commit's cells: an absolute number stands in the
|
|
187
|
+
* table once, and the deltas below it add up to it.
|
|
188
|
+
*
|
|
189
|
+
* The model is kept in the entry beside the node because the numbers of a row depend on the commit and the choice
|
|
190
|
+
* rather than on the place the window stands at: a step sideways asks for a cell's arithmetic, never for `rowModel`
|
|
191
|
+
* again. The strip is born empty — the range `[c0, c0 - 1]` — and is filled by the first `appCells`. */
|
|
159
192
|
function appRow(cache, r, span) {
|
|
160
193
|
const last = appData.rows.length;
|
|
161
194
|
const now = r === 0;
|
|
@@ -163,28 +196,36 @@ function appRow(cache, r, span) {
|
|
|
163
196
|
const model = now
|
|
164
197
|
? nowModel(appData.now, cache.keys, appView.files)
|
|
165
198
|
: rowModel(appData.rows[i].values, i === 0 ? null : appData.rows[i - 1].values, cache.keys, appView.files);
|
|
166
|
-
const
|
|
167
|
-
|
|
199
|
+
const el = appEl('div', 'row' + (now ? ' now' : ''));
|
|
200
|
+
el.style.top = (APP_HEAD + r * APP_ROW) + 'px';
|
|
168
201
|
const commit = appEl('div', 'c-commit');
|
|
169
202
|
if (now) commit.textContent = appUi.now;
|
|
170
203
|
else commit.appendChild(appCommit(appData.rows[i]));
|
|
171
|
-
|
|
204
|
+
el.appendChild(commit);
|
|
172
205
|
const cells = appEl('div', 'cells');
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
206
|
+
el.appendChild(cells);
|
|
207
|
+
return { el: el, cells: cells, model: model, now: now, cols: [span.c0, span.c0 - 1] };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* A row's numbers at the window's columns: the columns the two windows share keep their cells, and a column that
|
|
211
|
+
* entered is made. The numbers stand beside the pinned commit column rather than under it — the column lies over the
|
|
212
|
+
* content — so the strip is placed by the window's first column, and the cells of it follow their strip. A step down
|
|
213
|
+
* the table asks for the columns it already holds, and the row is not touched at all. */
|
|
214
|
+
function appCells(entry, cache, span) {
|
|
215
|
+
const cols = [span.c0, span.c1];
|
|
216
|
+
if (entry.cols[0] === cols[0] && entry.cols[1] === cols[1]) return;
|
|
217
|
+
entry.cells.style.left = (APP_COMMIT + cols[0] * APP_COL) + 'px';
|
|
218
|
+
appStrip(entry.cells, entry.cols, cols, (c) => appCell(entry.model, c, entry.now, cache));
|
|
219
|
+
entry.cols = cols;
|
|
179
220
|
}
|
|
180
221
|
|
|
181
222
|
/* A built row into the window: the map is what says which rows are in the markup, so a row goes into it where it is
|
|
182
223
|
* made — otherwise a window that moved would build its rows beside the ones that are still there. */
|
|
183
224
|
function appPlace(cache, r, span) {
|
|
184
|
-
const
|
|
185
|
-
cache.rows.set(r,
|
|
186
|
-
cache
|
|
187
|
-
|
|
225
|
+
const entry = appRow(cache, r, span);
|
|
226
|
+
cache.rows.set(r, entry);
|
|
227
|
+
appCells(entry, cache, span);
|
|
228
|
+
cache.grid.appendChild(entry.el);
|
|
188
229
|
}
|
|
189
230
|
|
|
190
231
|
/* A caption of the header: a file's name or a metric's. The room is a fixed number of columns, so a name that does
|
|
@@ -197,30 +238,39 @@ function appCaption(text, cls, span) {
|
|
|
197
238
|
return el;
|
|
198
239
|
}
|
|
199
240
|
|
|
200
|
-
/* The header: the
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
function
|
|
204
|
-
const
|
|
205
|
-
const g0 = Math.floor(span.c0 / count);
|
|
206
|
-
const g1 = Math.floor(span.c1 / count);
|
|
207
|
-
const left = (APP_COMMIT + g0 * count * APP_COL) + 'px';
|
|
208
|
-
const head = appEl('div', 'head');
|
|
209
|
-
head.appendChild(appEl('div', 'c-commit', appUi.commit));
|
|
241
|
+
/* The header's frame: the commit column and the two empty strips that `appStrip` fills — the names of the groups over
|
|
242
|
+
* the columns, their metrics under them. An empty strip says `[0, -1]`, which is what makes the first `appHead` fill
|
|
243
|
+
* it rather than move it. */
|
|
244
|
+
function appHeadNew() {
|
|
245
|
+
const el = appEl('div', 'head');
|
|
210
246
|
const groups = appEl('div', 'hgroups');
|
|
211
|
-
groups.style.left = left;
|
|
212
247
|
const metrics = appEl('div', 'hmetrics');
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
248
|
+
el.appendChild(appEl('div', 'c-commit', appUi.commit));
|
|
249
|
+
el.appendChild(groups);
|
|
250
|
+
el.appendChild(metrics);
|
|
251
|
+
return { el: el, groups: groups, metrics: metrics, g: [0, -1] };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* The header: the total and the enabled files over their columns, and one caption per metric column under them. The two
|
|
255
|
+
* strips move by the same `appStrip` as a row's numbers, and the unit of the metrics line is a column, so its strip is
|
|
256
|
+
* asked for whole groups — the metrics of a column stand under their file even when the window cuts the last group.
|
|
257
|
+
* A window that did not leave a group (a step down the table) leaves the header alone. */
|
|
258
|
+
function appHead(cache, span) {
|
|
259
|
+
const count = cache.keys.length;
|
|
260
|
+
const g0 = count === 0 ? 0 : Math.floor(span.c0 / count);
|
|
261
|
+
const g1 = count === 0 ? -1 : Math.floor(span.c1 / count);
|
|
262
|
+
if (cache.head === null) cache.head = appHeadNew();
|
|
263
|
+
const h = cache.head;
|
|
264
|
+
if (h.g[0] === g0 && h.g[1] === g1) return h.el;
|
|
265
|
+
const was = h.g;
|
|
266
|
+
h.g = [g0, g1];
|
|
267
|
+
h.groups.style.left = (APP_COMMIT + g0 * count * APP_COL) + 'px';
|
|
268
|
+
h.metrics.style.left = h.groups.style.left;
|
|
269
|
+
appStrip(h.groups, was, h.g, (g) => appCaption(
|
|
270
|
+
g === 0 ? appUi.total : appData.files[cache.list[g - 1]].label, 'gh', count));
|
|
271
|
+
appStrip(h.metrics, [was[0] * count, was[1] * count + count - 1], [g0 * count, g1 * count + count - 1],
|
|
272
|
+
(c) => appCaption(cache.labels[c % count], c % count === 0 ? 'g' : '', 1));
|
|
273
|
+
return h.el;
|
|
224
274
|
}
|
|
225
275
|
|
|
226
276
|
/* The window the shell shows: the rows and the columns, in the ordinals of the whole grid, and the size of that
|
|
@@ -259,26 +309,32 @@ export function appSpan(cache) {
|
|
|
259
309
|
};
|
|
260
310
|
}
|
|
261
311
|
|
|
262
|
-
/* The window drawn.
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
312
|
+
/* The window drawn. Moving over the grid costs what left it and what entered it, and nothing else: the rows in the
|
|
313
|
+
* markup are the window's rows (what left it is dropped, what entered it is made) and each of them holds the columns
|
|
314
|
+
* of the window (`appCells`), so a step in either direction is paid by the rows that moved rather than by the table.
|
|
315
|
+
*
|
|
316
|
+
* The reader's own choice (`redraw`) is the one thing that cannot be a move: the columns of a switched-off file are
|
|
317
|
+
* simply not among the columns that are built, so there is nothing to hide and nothing to recount — the totals are the
|
|
318
|
+
* sum over the files that are on (`rowModel`), and the window is built from nothing. The shell's own size is the same
|
|
319
|
+
* kind of change: it decides how much of the grid is in sight, which is not a place to move to. */
|
|
266
320
|
export function appWindow(cache, redraw) {
|
|
267
321
|
const span = appSpan(cache);
|
|
268
322
|
const was = cache.win;
|
|
269
323
|
cache.win = span;
|
|
270
|
-
if (redraw === true || was === null
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
cache.grid.
|
|
324
|
+
if (redraw === true || was === null) {
|
|
325
|
+
/* The grid holds the header and the rows of the window and nothing else, so the whole of it is thrown away rather
|
|
326
|
+
* than tracked piece by piece — and the rows that are made below say in the map that they are there. */
|
|
327
|
+
cache.grid.textContent = '';
|
|
274
328
|
cache.rows.clear();
|
|
275
|
-
|
|
329
|
+
cache.head = null;
|
|
330
|
+
cache.grid.appendChild(appHead(cache, span));
|
|
276
331
|
for (let r = span.r0; r <= span.r1; r++) appPlace(cache, r, span);
|
|
277
332
|
return;
|
|
278
333
|
}
|
|
334
|
+
appHead(cache, span);
|
|
279
335
|
[...cache.rows.keys()].forEach((r) => {
|
|
280
|
-
if (r >= span.r0 && r <= span.r1) return;
|
|
281
|
-
cache.rows.get(r).remove();
|
|
336
|
+
if (r >= span.r0 && r <= span.r1) { appCells(cache.rows.get(r), cache, span); return; }
|
|
337
|
+
cache.rows.get(r).el.remove();
|
|
282
338
|
cache.rows.delete(r);
|
|
283
339
|
});
|
|
284
340
|
for (let r = span.r0; r <= span.r1; r++) {
|
|
@@ -299,6 +355,7 @@ export function appTable(grid) {
|
|
|
299
355
|
rank: [],
|
|
300
356
|
slot: [],
|
|
301
357
|
rows: new Map(),
|
|
358
|
+
head: null,
|
|
302
359
|
win: null
|
|
303
360
|
};
|
|
304
361
|
cache.shell.addEventListener('scroll', () => appWindow(cache));
|