tosijs-ui 1.14.0 → 1.14.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/CHANGELOG.md +186 -0
- package/dist/data-table.d.ts +2 -0
- package/dist/data-table.js +97 -33
- package/dist/doc-browser-entry.d.ts +2 -0
- package/dist/doc-browser-entry.js +28 -0
- package/dist/doc-browser.d.ts +14 -0
- package/dist/doc-browser.js +16 -2
- package/dist/doc-system/routing.d.ts +16 -0
- package/dist/doc-system/routing.js +20 -0
- package/dist/doc-system/site/epub.js +1 -7
- package/dist/doc-system/site/generate-site.d.ts +8 -0
- package/dist/doc-system/site/generate-site.js +2 -6
- package/dist/doc-system/site/host-preset.d.ts +40 -0
- package/dist/doc-system/site/host-preset.js +60 -0
- package/dist/doc-system/site/make-llms-txt.d.ts +8 -1
- package/dist/doc-system/site/make-llms-txt.js +2 -2
- package/dist/doc-system/site/orchestrator.js +55 -4
- package/dist/doc-system/site/site-config.d.ts +25 -5
- package/dist/iife.js +20 -19
- package/dist/iife.js.map +9 -9
- package/dist/mapbox.js +47 -4
- package/dist/menu.d.ts +22 -0
- package/dist/menu.js +57 -3
- package/dist/select.d.ts +1 -0
- package/dist/select.js +7 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +3 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,191 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.14.2
|
|
4
|
+
|
|
5
|
+
A single-fix patch, cut from `v1.14.1` rather than from `main`, so it carries none of the
|
|
6
|
+
in-progress 1.15.0 work.
|
|
7
|
+
|
|
8
|
+
### `import 'tosijs-ui/doc-browser'` now actually defines `<tosi-doc-system>` (#158, #159)
|
|
9
|
+
|
|
10
|
+
**If you are building a doc site with `tosijs-ui/site`, upgrade.** The one import every
|
|
11
|
+
adoption doc tells you to write did not register the doc system.
|
|
12
|
+
|
|
13
|
+
`doc-browser.ts` is a library module — types, helpers, `createDocBrowser()` — while the element
|
|
14
|
+
lives in `doc-system/doc-system.ts`, which imports *from* doc-browser. The dependency arrow ran
|
|
15
|
+
the wrong way, so that import could never have reached the registrar however it was written.
|
|
16
|
+
|
|
17
|
+
What let it survive several releases is that it was a **partial** success. The leaf components
|
|
18
|
+
(`tosi-sidenav`, `tosi-example`, …) registered fine, so your bundle looked healthy, your own
|
|
19
|
+
elements worked, pages served 200 — and `<tosi-doc-system>` sat in the markup inert. The
|
|
20
|
+
expensive half was quieter: no doc system means no `window.__docTestResults`, so an entire
|
|
21
|
+
inline doc-test corpus **silently stopped existing**. Not a red suite; a suite reporting
|
|
22
|
+
nothing. Two projects hit this independently, one of them reaching a release candidate first.
|
|
23
|
+
|
|
24
|
+
`tosijs-ui/doc-browser` now resolves to an entry that imports the registrar and re-exports the
|
|
25
|
+
whole documented API. **No code changes** — the line you already wrote starts working.
|
|
26
|
+
|
|
27
|
+
A more precise spelling, a named `tosijs-ui/doc-system` export, is coming in a later release.
|
|
28
|
+
**This one keeps working**; it is an addition, not a deprecation.
|
|
29
|
+
|
|
30
|
+
Two sentences were separately false and are corrected: `import 'tosijs-ui'` does **not** also
|
|
31
|
+
register the doc system. The root barrel deliberately excludes that cluster (#133 — it was 77%
|
|
32
|
+
of the barrel's weight, landing on every app that imported a button), so the root import gives
|
|
33
|
+
you buttons and an undefined doc system. That text shipped in the emitted `.d.ts`, the adoption
|
|
34
|
+
page and the adopter reference.
|
|
35
|
+
|
|
36
|
+
A test now imports the built entry in a subprocess and fails if the element is undefined.
|
|
37
|
+
Verified by mutation: strip the side-effect import and it goes red.
|
|
38
|
+
|
|
39
|
+
## 1.14.1
|
|
40
|
+
|
|
41
|
+
### The doc-test gate can tell a dropped page from a passing one
|
|
42
|
+
|
|
43
|
+
`__docTestResults` reported `{ passed, failed, pages }`, so a consumer gate could assert that
|
|
44
|
+
tests ran and that none failed — but not that **all** of them ran. A runner that silently lost
|
|
45
|
+
half the corpus satisfied every check.
|
|
46
|
+
|
|
47
|
+
That is not hypothetical. The false green fixed in 1.14.0 was a page-**selection** defect: a
|
|
48
|
+
substring match counted 17 pages where 16 had tests, and ran a prose page's examples in a
|
|
49
|
+
background iframe. Fixing the selection did not add a guard against the next one.
|
|
50
|
+
|
|
51
|
+
The resolved object now carries `pagesWithTests` and `pagesTested`, and
|
|
52
|
+
`tests/doc-tests.pw.ts` asserts three things where it previously asserted one: the corpus
|
|
53
|
+
contains test pages at all, every such page reported, and some assertions executed. A corpus
|
|
54
|
+
with genuinely no tests resolves with honest zeros, so "no tests exist" stays distinguishable
|
|
55
|
+
from "tests exist and ran".
|
|
56
|
+
|
|
57
|
+
Framed by tosijs-platform in #142, from a suite that reported **140 pass, 0 fail while twelve
|
|
58
|
+
cases had never executed** — skip-guarded behind emulators that were not running, asserting
|
|
59
|
+
`expect(true).toBe(true)`. When the emulators were finally started it found three production
|
|
60
|
+
bugs in ten minutes. The tests were fine; the reporting was a lie, and a comfortable one, which
|
|
61
|
+
is why it survived. **"We didn't look" and "we looked and it's fine" must not produce the same
|
|
62
|
+
output** — a reported total is that same lie one level up.
|
|
63
|
+
|
|
64
|
+
### New page: "Adopting the doc system"
|
|
65
|
+
|
|
66
|
+
Three of the four onboarding defects this release fixes (#144, #145, #146) would have been
|
|
67
|
+
prevented by a page that did not exist — a narrow path to read *before* the first build, as
|
|
68
|
+
opposed to a configuration reference nobody reads until something is already wrong.
|
|
69
|
+
|
|
70
|
+
It is organised around the thing that makes this system hard to adopt: **its failure mode is a
|
|
71
|
+
site that serves perfectly and shows nothing.** Not a crash, not a 404 — pages return 200, the
|
|
72
|
+
markup is in the HTML, the adopter's own elements register, and the page is inert. So the page
|
|
73
|
+
leads with a "how to tell it actually worked" checklist (is `tosi-doc-system` defined, is the
|
|
74
|
+
chrome there, read one emitted `<head>`, do the examples run) and then explains the four silent
|
|
75
|
+
failures behind those checks.
|
|
76
|
+
|
|
77
|
+
Includes the complete `bundleEntry` file rather than naming it, since "your entry should import
|
|
78
|
+
what your pages need" was the sentence that cost #145's reporter the most — it reads as *your*
|
|
79
|
+
content, not as the doc system that renders the entire site.
|
|
80
|
+
|
|
81
|
+
### `<tosi-table>` filters and sorts the whole table, not the first `maxVisibleRows` (#147)
|
|
82
|
+
|
|
83
|
+
The window was applied **first**, so both the filter and the sort saw only the first N rows.
|
|
84
|
+
Two consequences, both reported from production at 300k rows:
|
|
85
|
+
|
|
86
|
+
- **A matching row past the cap could never be found.** A newly-created company appended to a
|
|
87
|
+
12,000-row list, with the cap at 10,000, did not exist as far as search was concerned.
|
|
88
|
+
- **Sorting did not sort the table.** You got the first N rows in *data* order, arranged
|
|
89
|
+
nicely — not the top N by the sort.
|
|
90
|
+
|
|
91
|
+
This was known and the answer had been a warning: the comment in the render path read *"every
|
|
92
|
+
count, filter and sort then ran on the truncated set, so the numbers were self-consistent and
|
|
93
|
+
wrong"* (#82). A warning does not help someone whose search returns nothing, and it named row
|
|
94
|
+
counts rather than the consequence — so "showing 10,000 of 12,000 rows" gave no clue why a
|
|
95
|
+
search failed.
|
|
96
|
+
|
|
97
|
+
The order is now **filter → force → sort → cluster → window**. `maxVisibleRows` is a *layout*
|
|
98
|
+
ceiling — the browser's maximum element height — so it belongs at the end, against what will be
|
|
99
|
+
drawn, rather than at the start against the data.
|
|
100
|
+
|
|
101
|
+
**Cost, measured on 300k rows**, since this moves the sort from "at most `maxVisibleRows`" to
|
|
102
|
+
"every match": the common case (no sort set) is **0.9ms, down from 2.9ms**; a filter narrowing
|
|
103
|
+
to ~1% is 2.0ms; a broad sort over all 300k is **108ms**. That last one is the price of sorting
|
|
104
|
+
the real table, and it is paid when you click a column header — but `pinColumns()`, a schema
|
|
105
|
+
change and a column resize all trigger a render without changing row order, so the pipeline is
|
|
106
|
+
memoized against the identities that determine it (array, filter, sort, groupId, forced ids).
|
|
107
|
+
A render that changes none of them reuses the result.
|
|
108
|
+
|
|
109
|
+
The truncation warning now counts against the **filtered** set — "10,000 of 300,000" is
|
|
110
|
+
alarming and wrong once a filter has narrowed things to 40 — and says that the rows shown are
|
|
111
|
+
the right ones, there are simply more than the browser can lay out.
|
|
112
|
+
|
|
113
|
+
### `menuClass` — theme one component's menus without restyling the page (#148)
|
|
114
|
+
|
|
115
|
+
Menu metrics are all theme variables, and setting them on your component **did not work**.
|
|
116
|
+
The popup is built per-invocation and mounted in a body-level `<tosi-float>`, so it is not a
|
|
117
|
+
descendant of whatever opened it, and custom properties have nothing to inherit from. The only
|
|
118
|
+
selector that reached it was `:root` — which restyles every menu on the page, including the doc
|
|
119
|
+
system's own when your component is documented on a `tosijs-ui/site` site. The reporter had not
|
|
120
|
+
shipped their denser menubar for exactly that reason: a component reaching out to re-theme the
|
|
121
|
+
page around it is the wrong direction.
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
popMenu({ target, menuItems, menuClass: 'my-editor-menu' })
|
|
125
|
+
<tosi-menu menu-class="my-editor-menu">
|
|
126
|
+
<tosi-select menu-class="my-listbox"> // its listbox mounts the same way
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The class lands beside `xin-menu tosi-menu`, takes a space-separated list, and **propagates to
|
|
130
|
+
submenus** — which are separate popups, so without that the theming would apply at the top
|
|
131
|
+
level and silently revert one level down. `menuWidth` was already a per-menu attribute, so
|
|
132
|
+
per-menu styling is not new here; this is the same idea for the rest of the knobs.
|
|
133
|
+
|
|
134
|
+
Chosen over the two alternatives in the report: copying the trigger's computed variables onto
|
|
135
|
+
the popup needs no change from adopters but is considerably more magic, and stamping a
|
|
136
|
+
`data-menu-owner` back-reference solves selection without solving theming.
|
|
137
|
+
|
|
138
|
+
### Adoption blockers found by tosijs-editor onboarding (#144, #145, #146)
|
|
139
|
+
|
|
140
|
+
Three defects and a credential, all found by one project adopting `tosijs-ui/site` for the
|
|
141
|
+
first time. Every one of them failed silently.
|
|
142
|
+
|
|
143
|
+
**A Mapbox token in `dist/` blocked adopters' pushes (#145).** Two `<tosi-map>` doc examples
|
|
144
|
+
carried a literal `pk.` token, which compiled into `dist/mapbox.js`, inlined into `iife.js`,
|
|
145
|
+
and landed in every adopter's sourcemap. It matches Mapbox's published secret pattern, so
|
|
146
|
+
**GitHub push protection blocked adopters the first time they committed their built site** — a
|
|
147
|
+
wall with someone else's name on it.
|
|
148
|
+
|
|
149
|
+
To be clear about what this is and is not: Mapbox `pk.` tokens are **public by design** —
|
|
150
|
+
Mapbox's own docs tell you to put them in client-side JavaScript — so the scanner match is a
|
|
151
|
+
false positive and there was never a secret here. The demos still use the token; it is now
|
|
152
|
+
assigned programmatically and base64'd, which is **not** a security measure and would be a
|
|
153
|
+
poor one. It exists solely so the literal string stops matching a regex. `atob()` protects
|
|
154
|
+
nothing from anyone, which is precisely why it suits a value that needs no protection.
|
|
155
|
+
|
|
156
|
+
`src/no-secrets.test.ts` fails the build on any credential-shaped string in `src/`, and says
|
|
157
|
+
plainly what it cannot see: an encoded value, deliberately. It catches accident, not intent.
|
|
158
|
+
|
|
159
|
+
**`bundleEntry` REPLACES the default bundle rather than extending it (#145).** An entry that
|
|
160
|
+
imports only your own library produces a site where every page renders its prerendered markup
|
|
161
|
+
and nothing else — no header, no nav, no menu, no live examples — with no console error, no
|
|
162
|
+
build warning and no 404. `<tosi-doc-system>` sits in the HTML, inert, because nothing defined
|
|
163
|
+
it, while your own elements register fine and make the bundle look healthy. The jsdoc now says
|
|
164
|
+
this in the imperative with the two-line fix, and the build **scans the emitted bundle and
|
|
165
|
+
warns** when `tosi-doc-system` is absent (and `tosi-example`, when the corpus has executable
|
|
166
|
+
fences). Grepping is sound here for once: `customElements.define` needs the literal tag, so
|
|
167
|
+
minification cannot erase it.
|
|
168
|
+
|
|
169
|
+
**`baseUrl` meant two different things (#144).** `generate-site` treated it as the origin and
|
|
170
|
+
added `basePath` on top; `make-llms-txt` treated it as origin-plus-path and ignored `basePath`
|
|
171
|
+
entirely. On a GitHub project page no configuration satisfied both — matching the canonical
|
|
172
|
+
URLs doubled the prefix in `llms.txt`, matching `llms.txt` dropped it from every canonical and
|
|
173
|
+
sitemap entry. Underneath were **two copies of `withBase`** (`generate-site`, and `epub`, whose
|
|
174
|
+
comment read "mirrors generate-site's withBase") plus this third consumer with none: one rule,
|
|
175
|
+
three implementations, one of them empty. `withBase` now lives once in `routing.ts`, `llms.txt`
|
|
176
|
+
applies it, `baseUrl` is documented as origin-only, and the build warns on the combination that
|
|
177
|
+
is always wrong.
|
|
178
|
+
|
|
179
|
+
**Which fence languages execute was undocumented (#146).** Six run — `js`, `ts`, `tjs`,
|
|
180
|
+
`html`, `css`, `test` — and the only place the distinction appeared named a subset, so the
|
|
181
|
+
mental model everyone formed was "js/ts/tjs/test run, everything else is display". A ` ```css `
|
|
182
|
+
fence showing how to style the component in your app is injected as a page-wide `<style>`; a
|
|
183
|
+
lone ` ```html ` fence showing the markup something compiles to renders as a broken-looking
|
|
184
|
+
example. Neither fails the build. Now documented as a table with display-only alternatives
|
|
185
|
+
(`xml` for markup), along with the grouping rule — adjacent fences form one example, prose
|
|
186
|
+
between them starts a new one — which is why moving a `test` block under its own heading
|
|
187
|
+
silently detaches it from the demo it was written against.
|
|
188
|
+
|
|
3
189
|
## 1.14.0
|
|
4
190
|
|
|
5
191
|
### Quote style no longer decides whether a live example runs (#141)
|
package/dist/data-table.d.ts
CHANGED
|
@@ -238,6 +238,8 @@ export declare class TosiTable extends WebComponent {
|
|
|
238
238
|
private _sort?;
|
|
239
239
|
private _rowGroupId;
|
|
240
240
|
private _visibleGroupedRowIds;
|
|
241
|
+
private _visibleMemo;
|
|
242
|
+
private computeVisibleRows;
|
|
241
243
|
private _nonRepeatingGroupedRowCells;
|
|
242
244
|
private _pinnedTopRows?;
|
|
243
245
|
private _pinnedBottomRows?;
|
package/dist/data-table.js
CHANGED
|
@@ -1453,6 +1453,76 @@ export class TosiTable extends WebComponent {
|
|
|
1453
1453
|
_sort;
|
|
1454
1454
|
_rowGroupId = null;
|
|
1455
1455
|
_visibleGroupedRowIds = null;
|
|
1456
|
+
/*
|
|
1457
|
+
Memo for the filter → force → sort → cluster pipeline (#147).
|
|
1458
|
+
|
|
1459
|
+
Correcting the order moved the sort from "at most `maxVisibleRows` rows" to "every matching
|
|
1460
|
+
row", which is the price of sorting the actual table. Measured on 300k rows: a broad sort is
|
|
1461
|
+
**108ms**, against 2.9ms for the old (wrong) window-first order. Paying that once when the
|
|
1462
|
+
user clicks a column header is fine. Paying it on `pinColumns()`, a schema change or a
|
|
1463
|
+
column resize — all of which call `queueRender` and none of which change the row order — is
|
|
1464
|
+
not, and would read as the O(1)-in-rows claim quietly breaking.
|
|
1465
|
+
|
|
1466
|
+
So the result is cached against the identities that actually determine it. Everything else
|
|
1467
|
+
that triggers a render reuses it. With no sort set (the common case) the whole pipeline is
|
|
1468
|
+
0.9ms and this never earns its keep — it exists for the case where the fix would otherwise
|
|
1469
|
+
regress a real table.
|
|
1470
|
+
*/
|
|
1471
|
+
_visibleMemo = null;
|
|
1472
|
+
computeVisibleRows(baseData, groupId, sort) {
|
|
1473
|
+
const memo = this._visibleMemo;
|
|
1474
|
+
if (memo &&
|
|
1475
|
+
memo.array === this._array &&
|
|
1476
|
+
memo.filter === this._filter &&
|
|
1477
|
+
memo.sort === sort &&
|
|
1478
|
+
memo.groupId === groupId &&
|
|
1479
|
+
memo.forcedIds === this._visibleGroupedRowIds) {
|
|
1480
|
+
return memo.result;
|
|
1481
|
+
}
|
|
1482
|
+
let rows = this.filter(baseData);
|
|
1483
|
+
/*
|
|
1484
|
+
Forcing runs BEFORE the sort, so a row re-admitted by `visibleGroupedRowIds` is ordered
|
|
1485
|
+
like any other row rather than being tacked on the end. It re-admits from the full
|
|
1486
|
+
pre-filter set, which is what `baseData` now is.
|
|
1487
|
+
*/
|
|
1488
|
+
if (groupId) {
|
|
1489
|
+
rows = withForcedGroups(rows, baseData, groupId, this._visibleGroupedRowIds);
|
|
1490
|
+
}
|
|
1491
|
+
/*
|
|
1492
|
+
`filter` defaults to `passThru`, which returns the SAME array — sorting it in place would
|
|
1493
|
+
reorder the caller's data. Copy only when we are about to mutate.
|
|
1494
|
+
*/
|
|
1495
|
+
if (sort)
|
|
1496
|
+
rows = rows.slice().sort(sort);
|
|
1497
|
+
// Clustering LAST — the spec is "grouped, then sorted within the grouping", so any other
|
|
1498
|
+
// sort is applied first and survives inside each group.
|
|
1499
|
+
if (groupId)
|
|
1500
|
+
rows = clusterByGroup(rows, groupId);
|
|
1501
|
+
// The window, last: it is a layout ceiling on what gets DRAWN.
|
|
1502
|
+
const limit = this.maxVisibleRows;
|
|
1503
|
+
if (rows.length > limit) {
|
|
1504
|
+
if (!this._warnedTruncation) {
|
|
1505
|
+
this._warnedTruncation = true;
|
|
1506
|
+
console.warn(`<tosi-table> is showing ${limit.toLocaleString()} of ${rows.length.toLocaleString()} matching rows.\n` +
|
|
1507
|
+
` maxVisibleRows is ${limit.toLocaleString()}${this._maxVisibleRows === undefined
|
|
1508
|
+
? ` — derived from this browser's maximum element height (${probeMaxElementHeight().toLocaleString() || 'unknown'}px) at rowHeight ${this.rowHeight}.`
|
|
1509
|
+
: ' — set explicitly on this element.'}\n` +
|
|
1510
|
+
` Filtering and sorting run over the FULL set, so the rows shown are the right\n` +
|
|
1511
|
+
` ones — there are simply more of them than the browser can lay out. Raise it\n` +
|
|
1512
|
+
` with \`table.maxVisibleRows = n\`, or narrow the filter.`);
|
|
1513
|
+
}
|
|
1514
|
+
rows = rows.slice(0, limit);
|
|
1515
|
+
}
|
|
1516
|
+
this._visibleMemo = {
|
|
1517
|
+
array: this._array,
|
|
1518
|
+
filter: this._filter,
|
|
1519
|
+
sort,
|
|
1520
|
+
groupId,
|
|
1521
|
+
forcedIds: this._visibleGroupedRowIds,
|
|
1522
|
+
result: rows,
|
|
1523
|
+
};
|
|
1524
|
+
return rows;
|
|
1525
|
+
}
|
|
1456
1526
|
_nonRepeatingGroupedRowCells = null;
|
|
1457
1527
|
// Optional explicit arrays of pinned items. When set, they are managed
|
|
1458
1528
|
// separately from `array` and override the `pinnedTop` / `pinnedBottom`
|
|
@@ -3073,25 +3143,13 @@ export class TosiTable extends WebComponent {
|
|
|
3073
3143
|
const pinnedTopData = this.effectivePinnedTopData;
|
|
3074
3144
|
const pinnedBottomData = this.effectivePinnedBottomData;
|
|
3075
3145
|
const baseData = this.effectiveBaseData;
|
|
3076
|
-
const limit = this.maxVisibleRows;
|
|
3077
|
-
const cap = Math.min(baseData.length, limit);
|
|
3078
3146
|
/*
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
is how a warning becomes something people filter.
|
|
3147
|
+
The truncation warning moved into `computeVisibleRows`, where the window is now applied
|
|
3148
|
+
(#147). It has to be counted against the FILTERED set: "showing 10,000 of 300,000 rows"
|
|
3149
|
+
is alarming and wrong once the filter has already narrowed things to 40. Kept as a
|
|
3150
|
+
once-per-table notice — one on every render of every table is how a warning becomes
|
|
3151
|
+
something people filter (#82).
|
|
3085
3152
|
*/
|
|
3086
|
-
if (cap < baseData.length && !this._warnedTruncation) {
|
|
3087
|
-
this._warnedTruncation = true;
|
|
3088
|
-
console.warn(`<tosi-table> is showing ${cap.toLocaleString()} of ${baseData.length.toLocaleString()} rows.\n` +
|
|
3089
|
-
` maxVisibleRows is ${limit.toLocaleString()}${this._maxVisibleRows === undefined
|
|
3090
|
-
? ` — derived from this browser's maximum element height (${probeMaxElementHeight().toLocaleString() || 'unknown'}px) at rowHeight ${this.rowHeight}.`
|
|
3091
|
-
: ' — set explicitly on this element.'}\n` +
|
|
3092
|
-
` Raise it with \`table.maxVisibleRows = n\`. Beyond the layout ceiling the\n` +
|
|
3093
|
-
` spacer stops growing, so the far end simply cannot be scrolled to.`);
|
|
3094
|
-
}
|
|
3095
3153
|
/*
|
|
3096
3154
|
A big table with no `rowHeight` is a misconfiguration, not a use case (#84).
|
|
3097
3155
|
|
|
@@ -3120,29 +3178,35 @@ export class TosiTable extends WebComponent {
|
|
|
3120
3178
|
` depending on row count at all — virtual tables are O(1) in rows.\n` +
|
|
3121
3179
|
` rowHeight 0 is for SMALL tables and for variable row heights.`);
|
|
3122
3180
|
}
|
|
3123
|
-
const scope = baseData.slice(0, cap);
|
|
3124
3181
|
// Fresh per render — see `_groupIdMemo`. Must happen before `groupIdFn` is read.
|
|
3125
3182
|
this._groupIdMemo = new WeakMap();
|
|
3126
3183
|
const groupId = this.groupIdFn;
|
|
3127
|
-
|
|
3184
|
+
const { sort } = this;
|
|
3128
3185
|
/*
|
|
3129
|
-
|
|
3130
|
-
|
|
3186
|
+
ORDER: filter → force → sort → cluster → WINDOW (#147).
|
|
3187
|
+
|
|
3188
|
+
`maxVisibleRows` used to be applied FIRST, so both the filter and the sort saw only the
|
|
3189
|
+
first N rows. Two consequences a user would call bugs, and both were reported from
|
|
3190
|
+
production at 300k rows:
|
|
3191
|
+
|
|
3192
|
+
- a matching row past the cap could never be found. Search for a company appended to
|
|
3193
|
+
the end of a 12,000-row list, with the cap at 10,000, and it does not exist.
|
|
3194
|
+
- sorting did not sort the table. You got the first N rows in DATA order, arranged
|
|
3195
|
+
nicely — not the top N by the sort.
|
|
3196
|
+
|
|
3197
|
+
The comment on the truncation warning above already said this ("every count, filter and
|
|
3198
|
+
sort then ran on the truncated set, so the numbers were self-consistent and wrong"), and
|
|
3199
|
+
#82's answer was to warn. A warning does not help someone whose search returns nothing.
|
|
3200
|
+
|
|
3201
|
+
The window is a LAYOUT ceiling — the browser's maximum element height — so it belongs at
|
|
3202
|
+
the end, against what will actually be shown, not at the start against the data.
|
|
3131
3203
|
*/
|
|
3204
|
+
const visibleData = this.computeVisibleRows(baseData, groupId, sort);
|
|
3132
3205
|
if (groupId) {
|
|
3133
|
-
visibleData = withForcedGroups(visibleData, scope, groupId, this._visibleGroupedRowIds);
|
|
3134
|
-
}
|
|
3135
|
-
const { sort } = this;
|
|
3136
|
-
if (sort)
|
|
3137
|
-
visibleData.sort(sort);
|
|
3138
|
-
// Clustering LAST — the spec is "grouped, then sorted within the grouping", so any
|
|
3139
|
-
// other sort is applied first and survives inside each group.
|
|
3140
|
-
if (groupId) {
|
|
3141
|
-
visibleData = clusterByGroup(visibleData, groupId);
|
|
3142
3206
|
this._grouping = groupRenderMeta(visibleData, groupId);
|
|
3143
|
-
// Counted from
|
|
3144
|
-
//
|
|
3145
|
-
this._rowGroupCounts = groupCounts(
|
|
3207
|
+
// Counted from the full PRE-FILTER set against the rendered set, and assigned BEFORE
|
|
3208
|
+
// the rows below are stamped, so a cell renderer can read it as it renders.
|
|
3209
|
+
this._rowGroupCounts = groupCounts(baseData, visibleData, groupId);
|
|
3146
3210
|
}
|
|
3147
3211
|
else {
|
|
3148
3212
|
this._grouping = null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The entry behind `tosijs-ui/doc-browser`.
|
|
3
|
+
|
|
4
|
+
This exists because that import — the one EVERY adoption doc tells you to write, and the one
|
|
5
|
+
the build's own remediation warning prints when it detects a missing doc system — did not
|
|
6
|
+
define `<tosi-doc-system>`.
|
|
7
|
+
|
|
8
|
+
`doc-browser.ts` is a library module: types, helpers, and the `createDocBrowser()` factory. The
|
|
9
|
+
custom element lives in `doc-system/doc-system.ts`, which imports FROM `doc-browser.ts`. The
|
|
10
|
+
dependency arrow runs the wrong way, so the documented import could never reach the registrar
|
|
11
|
+
however it was written.
|
|
12
|
+
|
|
13
|
+
The result was precisely the failure mode the adoption page warns about, and it is disguised by
|
|
14
|
+
being a PARTIAL success: `tosi-sidenav`, `tosi-example` and the other leaf components do
|
|
15
|
+
register, so the bundle looks healthy, pages serve 200, and `<tosi-doc-system>` sits in the
|
|
16
|
+
markup inert. The expensive half is quieter — no doc system means no `window.__docTestResults`,
|
|
17
|
+
so an adopter's entire inline doc-test corpus silently stops existing. The suite does not fail;
|
|
18
|
+
it reports nothing. Two projects hit this independently (#158, #159), one of them reaching a
|
|
19
|
+
release candidate before anyone noticed.
|
|
20
|
+
|
|
21
|
+
A SEPARATE FILE, not a side-effect import inside `doc-browser.ts`: that would be a genuine
|
|
22
|
+
import cycle (doc-system → doc-browser → doc-system). It would probably work, because
|
|
23
|
+
`doc-system.ts` uses `createDocBrowser` only inside methods — but "probably works, depending on
|
|
24
|
+
evaluation order after your bundler reorders it" is not a thing to put under the one import
|
|
25
|
+
every adopter is told to write. Nothing here depends on evaluation order.
|
|
26
|
+
*/
|
|
27
|
+
import './doc-system/doc-system.js';
|
|
28
|
+
export * from './doc-browser.js';
|
package/dist/doc-browser.d.ts
CHANGED
|
@@ -10,6 +10,20 @@ export interface DocTestResults {
|
|
|
10
10
|
passed: number;
|
|
11
11
|
failed: number;
|
|
12
12
|
pages: Record<string, PageTestResults>;
|
|
13
|
+
/**
|
|
14
|
+
* How many pages the runner INTENDED to run, and how many reported back.
|
|
15
|
+
*
|
|
16
|
+
* `passed + failed > 0` only proves the runner started. It cannot tell a full corpus from
|
|
17
|
+
* a corpus that silently lost half its pages — and a page-selection defect that ran 16 of
|
|
18
|
+
* 17 pages is exactly the false green fixed in 1.14.0, so this is a live failure mode
|
|
19
|
+
* rather than a hypothetical one. A consumer gate should assert
|
|
20
|
+
* `pagesTested === pagesWithTests && pagesWithTests > 0`.
|
|
21
|
+
*
|
|
22
|
+
* Framed by tosijs-platform in #142: "we didn't look" and "we looked and it's fine" must
|
|
23
|
+
* not produce the same output. Reporting a total is how a skipped run stays comfortable.
|
|
24
|
+
*/
|
|
25
|
+
pagesWithTests: number;
|
|
26
|
+
pagesTested: number;
|
|
13
27
|
}
|
|
14
28
|
declare global {
|
|
15
29
|
interface Window {
|
package/dist/doc-browser.js
CHANGED
|
@@ -488,6 +488,8 @@ export function createDocBrowser(options) {
|
|
|
488
488
|
passed: 0,
|
|
489
489
|
failed: 0,
|
|
490
490
|
pages: pageTestResults,
|
|
491
|
+
pagesWithTests,
|
|
492
|
+
pagesTested,
|
|
491
493
|
};
|
|
492
494
|
for (const pageResults of Object.values(pageTestResults)) {
|
|
493
495
|
allResults.passed += pageResults.totalPassed;
|
|
@@ -1723,7 +1725,13 @@ export function createDocBrowser(options) {
|
|
|
1723
1725
|
}
|
|
1724
1726
|
if (pagesWithTests === 0) {
|
|
1725
1727
|
if (testResultsResolve) {
|
|
1726
|
-
testResultsResolve({
|
|
1728
|
+
testResultsResolve({
|
|
1729
|
+
passed: 0,
|
|
1730
|
+
failed: 0,
|
|
1731
|
+
pages: {},
|
|
1732
|
+
pagesWithTests: 0,
|
|
1733
|
+
pagesTested: 0,
|
|
1734
|
+
});
|
|
1727
1735
|
testResultsResolve = undefined;
|
|
1728
1736
|
}
|
|
1729
1737
|
return;
|
|
@@ -1826,7 +1834,13 @@ export function createDocBrowser(options) {
|
|
|
1826
1834
|
setTimeout(() => markPageTested(currentDoc.filename), 2000);
|
|
1827
1835
|
}
|
|
1828
1836
|
else if (testResultsResolve) {
|
|
1829
|
-
testResultsResolve({
|
|
1837
|
+
testResultsResolve({
|
|
1838
|
+
passed: 0,
|
|
1839
|
+
failed: 0,
|
|
1840
|
+
pages: {},
|
|
1841
|
+
pagesWithTests: 0,
|
|
1842
|
+
pagesTested: 0,
|
|
1843
|
+
});
|
|
1830
1844
|
testResultsResolve = undefined;
|
|
1831
1845
|
}
|
|
1832
1846
|
}
|
|
@@ -48,3 +48,19 @@ export declare function resolveParent(parentValue: string, docs: Array<{
|
|
|
48
48
|
filename: string;
|
|
49
49
|
title?: string;
|
|
50
50
|
}>, slugMap: Record<string, string>): string;
|
|
51
|
+
/**
|
|
52
|
+
* Prefix a root-relative path with the site's `basePath`.
|
|
53
|
+
*
|
|
54
|
+
* THE ONE COPY. There were two identical implementations (`generate-site.ts`, `epub.ts` —
|
|
55
|
+
* the latter's comment said "mirrors generate-site's withBase") and a third consumer,
|
|
56
|
+
* `make-llms-txt.ts`, that had none at all. So `baseUrl` meant the ORIGIN to the page
|
|
57
|
+
* generator and ORIGIN-PLUS-PATH to llms.txt, and on a GitHub project page no configuration
|
|
58
|
+
* satisfied both: matching the canonical URLs doubled the prefix in llms.txt, and matching
|
|
59
|
+
* llms.txt dropped it from every canonical and sitemap entry (tosijs-ui#144).
|
|
60
|
+
*
|
|
61
|
+
* Nothing failed — `basePath` affects metadata only, so the site works and you find it by
|
|
62
|
+
* reading the emitted `<head>`. The reporter's shipped to Pages before they noticed.
|
|
63
|
+
*
|
|
64
|
+
* `baseUrl` is the ORIGIN ONLY. Absolute URLs and an empty/`/` basePath pass through.
|
|
65
|
+
*/
|
|
66
|
+
export declare function withBase(basePath: string | undefined, p: string): string;
|
|
@@ -128,3 +128,23 @@ export function resolveParent(parentValue, docs, slugMap) {
|
|
|
128
128
|
return d.filename;
|
|
129
129
|
return '';
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Prefix a root-relative path with the site's `basePath`.
|
|
133
|
+
*
|
|
134
|
+
* THE ONE COPY. There were two identical implementations (`generate-site.ts`, `epub.ts` —
|
|
135
|
+
* the latter's comment said "mirrors generate-site's withBase") and a third consumer,
|
|
136
|
+
* `make-llms-txt.ts`, that had none at all. So `baseUrl` meant the ORIGIN to the page
|
|
137
|
+
* generator and ORIGIN-PLUS-PATH to llms.txt, and on a GitHub project page no configuration
|
|
138
|
+
* satisfied both: matching the canonical URLs doubled the prefix in llms.txt, and matching
|
|
139
|
+
* llms.txt dropped it from every canonical and sitemap entry (tosijs-ui#144).
|
|
140
|
+
*
|
|
141
|
+
* Nothing failed — `basePath` affects metadata only, so the site works and you find it by
|
|
142
|
+
* reading the emitted `<head>`. The reporter's shipped to Pages before they noticed.
|
|
143
|
+
*
|
|
144
|
+
* `baseUrl` is the ORIGIN ONLY. Absolute URLs and an empty/`/` basePath pass through.
|
|
145
|
+
*/
|
|
146
|
+
export function withBase(basePath, p) {
|
|
147
|
+
if (!p || !basePath || basePath === '/' || /^(https?:)?\/\//.test(p))
|
|
148
|
+
return p;
|
|
149
|
+
return basePath.replace(/\/$/, '') + (p.startsWith('/') ? p : '/' + p);
|
|
150
|
+
}
|
|
@@ -17,7 +17,7 @@ Build-time only (Bun APIs + the `zip` CLI); never import from browser code.
|
|
|
17
17
|
import * as fs from 'fs';
|
|
18
18
|
import * as path from 'path';
|
|
19
19
|
import { renderDocMarkdown } from '../render.js';
|
|
20
|
-
import { buildSlugMap, pathForSlug, slugForPath } from '../routing.js';
|
|
20
|
+
import { buildSlugMap, pathForSlug, slugForPath, withBase } from '../routing.js';
|
|
21
21
|
import { buildNavTree } from '../nav-tree.js';
|
|
22
22
|
import { partitionByBook, DEFAULT_BOOK } from '../book-target.js';
|
|
23
23
|
import { epubVolumeIdentity } from './epub-volumes.js';
|
|
@@ -159,12 +159,6 @@ function collectExamplePres(el, out) {
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
-
/** Prefix a root-relative path with basePath (mirrors generate-site's withBase). */
|
|
163
|
-
function withBase(basePath, p) {
|
|
164
|
-
if (!p || !basePath || basePath === '/' || /^(https?:)?\/\//.test(p))
|
|
165
|
-
return p;
|
|
166
|
-
return basePath.replace(/\/$/, '') + (p.startsWith('/') ? p : '/' + p);
|
|
167
|
-
}
|
|
168
162
|
/**
|
|
169
163
|
* Rewrite in-book cross-links so they resolve INSIDE the EPUB (#15). renderDocMarkdown
|
|
170
164
|
* emits site paths — `/slug/` (wikilinks + the auto-generated section TOCs) and legacy
|
|
@@ -74,6 +74,14 @@ export interface GenerateSiteConfig {
|
|
|
74
74
|
/** extra lines injected into every <head> (favicon, analytics, etc.) */
|
|
75
75
|
headExtra?: string;
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Prefix a root-relative path with basePath. No-op for '/', empty, protocol-
|
|
79
|
+
* relative (`//…`), or absolute (`https://…`) URLs.
|
|
80
|
+
*
|
|
81
|
+
* Used ONLY for *metadata* URLs now (canonical, og:url, og:image, sitemap) — they
|
|
82
|
+
* legitimately need the real absolute origin+path. *Functional* URLs go through
|
|
83
|
+
* `relativeUrl` instead, so the artifact is mount-agnostic. See `relativeUrl`.
|
|
84
|
+
*/
|
|
77
85
|
/**
|
|
78
86
|
* Depth of a page below the site root: 0 for the root index (served at the mount
|
|
79
87
|
* root), 1 for every `/slug/` page. The generator only ever emits a flat `/slug/`
|
|
@@ -11,7 +11,7 @@ Build-time only (uses Bun.write). Shares slug + markdown rendering with the runt
|
|
|
11
11
|
component (src/doc-system/*) so static and hydrated output agree.
|
|
12
12
|
*/
|
|
13
13
|
import { pageTitle } from '../doc-title.js';
|
|
14
|
-
import { buildSlugMap, pathForSlug, rewriteDocLinks } from '../routing.js';
|
|
14
|
+
import { buildSlugMap, pathForSlug, rewriteDocLinks, withBase, } from '../routing.js';
|
|
15
15
|
import { buildNavTree, navOpenPath } from '../nav-tree.js';
|
|
16
16
|
import { renderDocMarkdown, docDescription, } from '../render.js';
|
|
17
17
|
const escapeAttr = (s) => s
|
|
@@ -83,11 +83,7 @@ function absUrl(baseUrl, pathOrUrl) {
|
|
|
83
83
|
* legitimately need the real absolute origin+path. *Functional* URLs go through
|
|
84
84
|
* `relativeUrl` instead, so the artifact is mount-agnostic. See `relativeUrl`.
|
|
85
85
|
*/
|
|
86
|
-
|
|
87
|
-
if (!p || !basePath || basePath === '/' || /^(https?:)?\/\//.test(p))
|
|
88
|
-
return p;
|
|
89
|
-
return basePath.replace(/\/$/, '') + (p.startsWith('/') ? p : '/' + p);
|
|
90
|
-
}
|
|
86
|
+
// `withBase` now lives in ../routing.ts — see the note there (#144).
|
|
91
87
|
/**
|
|
92
88
|
* Depth of a page below the site root: 0 for the root index (served at the mount
|
|
93
89
|
* root), 1 for every `/slug/` page. The generator only ever emits a flat `/slug/`
|
|
@@ -11,3 +11,43 @@ export interface HostConfigMismatch {
|
|
|
11
11
|
* multi-site config that serves the built directory from any of its targets is fine.
|
|
12
12
|
*/
|
|
13
13
|
export declare function firebasePublicMismatch(firebaseJson: string, outputDir: string): HostConfigMismatch | null;
|
|
14
|
+
/**
|
|
15
|
+
* `baseUrl` is the ORIGIN; `basePath` is the mount path. Supplying the path in BOTH is
|
|
16
|
+
* always wrong and always silent: `generate-site` emits `baseUrl + withBase(basePath, …)`,
|
|
17
|
+
* so every canonical URL, `og:url` and sitemap entry carries the prefix twice.
|
|
18
|
+
*
|
|
19
|
+
* Nothing fails — `basePath` affects metadata only (#25), so the site builds, serves and
|
|
20
|
+
* navigates correctly with assets resolving relative. You find it by reading the emitted
|
|
21
|
+
* `<head>`, or from Search Console. The reporter's shipped to GitHub Pages first (#144).
|
|
22
|
+
*
|
|
23
|
+
* Warns rather than throws: the emitted site is usable, and refusing to build over a
|
|
24
|
+
* metadata defect would be disproportionate. But it names the exact fix, because "these two
|
|
25
|
+
* fields overlap" is not something a reader can act on.
|
|
26
|
+
*/
|
|
27
|
+
export declare function basePathDoubling(config: {
|
|
28
|
+
baseUrl?: string;
|
|
29
|
+
basePath?: string;
|
|
30
|
+
}): string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Does a custom `bundleEntry` bundle actually contain the doc system?
|
|
33
|
+
*
|
|
34
|
+
* `bundleEntry` REPLACES tosijs-ui's bundle rather than extending it, so an entry that
|
|
35
|
+
* imports only the adopter's own library produces a site where every page renders its
|
|
36
|
+
* prerendered markup and nothing else: no header, no nav, no menu, no live examples. Nothing
|
|
37
|
+
* fails — `docs.json`, `hydrate.js` and the HTML all serve 200, `<tosi-doc-system>` is
|
|
38
|
+
* present in the markup, and the adopter's own `customElements.get('their-element')` returns
|
|
39
|
+
* true, so the bundle looks healthy. It is inert because nothing defined `tosi-doc-system`
|
|
40
|
+
* (tosijs-ui#145; cost the reporter more than any other onboarding problem).
|
|
41
|
+
*
|
|
42
|
+
* Checked by looking for the tag names in the built output, which survives minification
|
|
43
|
+
* because `customElements.define` needs the literal string. This is the one case where
|
|
44
|
+
* grepping a bundle is sound — we are looking for a STRING the runtime must contain, not for
|
|
45
|
+
* a package path that minification erases.
|
|
46
|
+
*
|
|
47
|
+
* `liveExample` is reported separately: a corpus with no executable fences legitimately does
|
|
48
|
+
* not need it, so the caller decides whether its absence is worth mentioning.
|
|
49
|
+
*/
|
|
50
|
+
export declare function bundleRegistrations(bundleSource: string): {
|
|
51
|
+
docSystem: boolean;
|
|
52
|
+
liveExample: boolean;
|
|
53
|
+
};
|