tosijs-ui 1.9.7 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,147 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.10.0
4
+
5
+ Mostly a **silent-failure** release: several things that were quietly not working, or quietly
6
+ not being checked, now say so. If you use `tosijs-ui/site`, three of these have probably cost
7
+ you time already.
8
+
9
+ ### `<tosi-table>`
10
+
11
+ **Scroll position survives a re-render.** Sorting, filtering, or toggling
12
+ `visibleGroupedRowIds` no longer throws you back to the top. The table anchors on the
13
+ **topmost visible row**, not on `scrollTop`, and restores that row to the same offset — the
14
+ distinction matters as soon as the row count changes, because the same pixel offset then
15
+ shows entirely different rows. Set `preserveScroll = false` for the case where a render means
16
+ "here is a different dataset" and starting at the top is right.
17
+
18
+ ### The tunnel: links now work on a second device
19
+
20
+ A magic link is redeemable **repeatedly for 15 minutes** instead of dying on first use. The
21
+ old behaviour collided with the feature's own purpose: glance at a link and close the tab and
22
+ you need a new one; open it on a laptop then reach for your phone and it is dead — in a
23
+ workspace built for reading your uncommitted tree _on a phone_. One project had already
24
+ replaced it with a never-expiring link of their own, which is the tell.
25
+
26
+ The bound moved from _uses_ to _time_, not away. Set the level per project:
27
+
28
+ ```typescript
29
+ tunnel: { linkPolicy: 'single-use', linkTtlMinutes: 2 } // ratchet up
30
+ ```
31
+
32
+ An expired link is refused under either policy — widening reuse never widens lifetime.
33
+
34
+ Every user-facing string — including the line the CLI prints at the moment you share a link
35
+ — is derived from the policy in force, rather than asserting "single-use" regardless.
36
+
37
+ ### Silent failures, now loud
38
+
39
+ - **`docPaths` are watched.** A root-level doc was served and rendered but never watched:
40
+ edit, save, refresh, stale page, no rebuild, no message — indistinguishable from a browser
41
+ cache. (#49)
42
+ - **Two builders on one output tree are refused,** naming the holder's pid and port. Both
43
+ wipe-then-repopulate `docs/`, so running a standalone build against a live dev server meant
44
+ each deleted what the other was writing; it killed a dev server with no error anywhere and
45
+ left `docs/` a fraction populated. A standalone `buildSite()` refuses; a second **dev
46
+ server** only warns and continues, because `bun playwright test` and `bun run test-browser`
47
+ each start their own and a hard failure there would break the lanes this protects. The lock
48
+ is stale-safe (a dead holder's lock is debris), re-entrant, released on Ctrl-C, and fails
49
+ open. (#51)
50
+ - **A truncated `/*#` doc block warns, by name.** Block comments cannot nest, so a `*/`
51
+ inside a doc demo ends the doc there — previously surfacing as parse errors pointing at
52
+ _prose_, never at the delimiter responsible. (#70)
53
+ - **Inline doc tests no longer under-report.** A page could report "done" before its examples
54
+ had run, so their tests were omitted rather than failed — silently, with the suite green.
55
+ **If you use `tosijs-ui/site` with async examples, your counts may have been wrong;** a
56
+ number that goes UP after upgrading is this fix.
57
+
58
+ ### Build output
59
+
60
+ **The hydration bundle now builds into your site output, not `dist/`.** It was written into
61
+ the library tree with only the `.js` copied out, stranding the sourcemap in a directory you
62
+ publish and commit but never serve — 65 MiB across 216 packed blobs in one adopter, ~35% of
63
+ that repo's packed blob store, for a file nothing could load. Set `bundleOutDir` if your
64
+ bundle is itself a published artifact. If you have that history:
65
+ `git rm --cached dist/iife.js dist/iife.js.map` and gitignore them. (#69)
66
+
67
+ ### Also in this release
68
+
69
+ - **`checkExamples` accepts `{ contextKeys: [...] }`** as well as a boolean, so a library
70
+ that documents ITSELF — whose examples import its own package name — keeps the guard on
71
+ instead of disabling it. The keys are **added to** the `tosijs` / `tosijs-ui` defaults, not
72
+ substituted for them. (#71)
73
+ - **`preview.tunnel.linkPolicy`** (`'window'` | `'single-use'`) and
74
+ **`preview.tunnel.linkTtlMinutes`** (default 15) set the link security level.
75
+ - **`SiteConfig.preview.host` is optional.** The documented practice supplies it from
76
+ `PREVIEW_HOST`, so a correct config used to fail typecheck. (#72)
77
+ - **`marked` peer is `^16.4.2 || ^17.0.0 || ^18.0.0`** — it was two majors behind `latest`, so
78
+ a consumer on a current marked got a peer violation on a fresh install. (#60)
79
+ - **The live-example docs now state that `test()` bodies run concurrently** within a fence,
80
+ and give the rule that follows: steps that depend on each other belong in one `test()`.
81
+ (#43)
82
+ - **`llms.txt` carries the browser-control affordance** when a project sets `haltijaDev`, so
83
+ an agent learns it can drive the running page instead of inferring behaviour from source.
84
+ (#18)
85
+ - **Host bootstrap in `doc-site-system.md` now gives the actual commands** rather than
86
+ describing the danger and leaving the dangerous step as an exercise.
87
+ - **`svg2DataUrl`: two real fixes, and one that is NOT done.** It wrote `strokeWidth` /
88
+ `strokeLinecap` / `strokeLinejoin` — names SVG ignores — on any element carrying inline
89
+ style, and explicit `fill` / `stroke` / `strokeWidth` arguments were clobbered by the style
90
+ pass. Both fixed.
91
+
92
+ **[#68](https://github.com/tonioloewald/tosijs-ui/issues/68) is NOT closed by this.** Most
93
+ icons carry their styling on the **root** `<svg>`, which this does not touch, so a
94
+ `stroked` icon — about 280 of ~287 — still serializes with no stroke at all and renders as
95
+ a solid black wedge. Verified after the fix: `svg2DataUrl(icons.chevronRight())` emits no
96
+ `stroke` and no `stroke-width`. Prefer inline `<svg>` over a data URL for stroked icons
97
+ until this lands.
98
+
99
+ - **`acquireBuildLock` / `lockDecision`** in `build-lock.ts` — the
100
+ output-tree lock behind the concurrent-build refusal. **Internal**: not exported from
101
+ `tosijs-ui/site`, so do not build on it yet. Say so if you want it public.
102
+ - New **`tosijs-caddy-install`** bin — installs the Caddy snippets the deploy/tunnel bins
103
+ need, **refuses** to install a template with placeholders left in it, keeps the outgoing
104
+ config as `Caddyfile.bak`, and is dry-run by default. That refusal was the reason this
105
+ existed as a private script; shipping the template without it was the wrong half to keep.
106
+ - The unit lane now covers `bin/` — `bunfig.toml`'s test root was `./src`, so tests anywhere
107
+ else were silently never collected.
108
+
109
+ > **Versioning note.** This is a patch that adds public API. Minors are for breaking changes
110
+ > and feature rollouts; additive, non-breaking extensions ship as patches. The contract you
111
+ > rely on is unchanged: **a patch never breaks you.**
112
+
113
+ ## 1.9.8
114
+
115
+ **Grouped tables can now say how much of a group is showing.** The piece missing from 1.9.7's
116
+ row grouping: a cell that reports on its group ("showing 2 of 7") or offers a **show-all**
117
+ toggle needs to compare what is rendered against what exists — and that is the one comparison
118
+ a cell renderer cannot make for itself. The filter has already run by then, so a consumer
119
+ sees the survivors with nothing to measure them against. The table sees both sides.
120
+
121
+ ```js
122
+ const id = table.groupIdFor(row)
123
+ const { visible, total } = table.rowGroupCounts.get(id) ?? { visible: 0, total: 0 }
124
+ cell.textContent = visible < total ? `showing ${visible} of ${total}` : `${total} lines`
125
+
126
+ // the toggle is just a set of ids handed back to the table
127
+ expanded.has(id) ? expanded.delete(id) : expanded.add(id)
128
+ table.visibleGroupedRowIds = [...expanded]
129
+ ```
130
+
131
+ - **`table.rowGroupCounts`** — a `Map` from group id to `{ visible, total }`, rendered rows
132
+ against rows before filtering. Recomputed each render and readable while cells render.
133
+ - **`table.groupIdFor(row)`** — a row's group id, or `null` when ungrouped. This matters when
134
+ the grouping was **inferred** from `nonRepeatingGroupedRowCells`: you never wrote that
135
+ function, so its ids are otherwise unreproducible and the map would be keyed by strings you
136
+ could not construct.
137
+
138
+ Groups the filter removed **entirely** stay in the map with `visible: 0`, so "nothing here
139
+ matched" stays distinguishable from "no such group" — the first being exactly when a show-all
140
+ toggle is worth offering. `rowGroupCounts` is always a `Map` (empty when ungrouped), so
141
+ `.get()` needs no null check. Pinned rows sit outside grouping and are not counted.
142
+
143
+ Additive only; ungrouped tables are unaffected.
144
+
3
145
  ## 1.9.7
4
146
 
5
147
  **`<tosi-table>` can group rows.** The motivating shape is invoice lines: rows that belong
@@ -22,14 +164,23 @@ know anything about grouping. It is additive to the filter's own result, so a fi
22
164
  ranks as well as selects keeps its ranking.
23
165
 
24
166
  Two details worth knowing. **Clusters keep your sort**: groups appear in first-appearance
25
- order, so a group lands wherever its best-sorted row landed. Sorting the clusters by their
26
- id instead would have quietly thrown away the sort the user just clicked. And repeated
27
- cells are **hidden, not emptied** — `.tr:not(.table-cluster-first) .cluster-repeat
28
- { visibility: hidden }`. If you write your own rule for a custom `dataCell`, use
29
- `visibility`, never `display: none`: every cell is an item of the row's grid, so removing
30
- one pulls each later cell a column left and the row renders under the wrong headers.
167
+ order, so a group lands wherever its best-sorted row landed. Sorting the clusters by their id
168
+ instead would have quietly thrown away the sort the user just clicked.
169
+
170
+ And repeated cells are **hidden, not emptied** — transparent text plus `display: none` on
171
+ element children, via `.tr:not(.table-cluster-first) .cluster-repeat`. Deliberately not
172
+ `visibility: hidden`, which also stops the cell painting its **background**: a pinned
173
+ column's background is the only thing masking the columns scrolling underneath it, so
174
+ repeated cells there became windows onto the scrolled content. And never `display: none` on
175
+ the cell itself — every cell is an item of the row's grid, so removing one pulls each later
176
+ cell a column left and the row renders under the wrong headers.
31
177
  `table.isFirstInGroup(row)` answers the same question in JavaScript.
32
178
 
179
+ Group ids are **memoized per render** in a `WeakMap` — the id is computed several times for
180
+ one row in a single pass (clustering, parity, first-of-group, counts, then again per stamped
181
+ row), and the inferred form does a `JSON.stringify` each time. The map is replaced on every
182
+ render rather than kept, so a cached id can never outlive the values it came from.
183
+
33
184
  Ungrouped tables are unaffected — all three properties default to off.
34
185
 
35
186
  **The inline doc-test lane was under-reporting, and staying green about it.** If you use
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env bun
2
+ /*
3
+ Install the Caddy snippets `tosijs-deploy` / `tosijs-tunnel` depend on.
4
+
5
+ tosijs-caddy-install # DRY RUN — shows the diff, changes nothing
6
+ tosijs-caddy-install --go # substitute, validate, install, reload
7
+ tosijs-caddy-install --host=me@1.2.3.4 --go
8
+ tosijs-caddy-install --template=./my-Caddyfile --go
9
+
10
+ Both deploy bins write a fragment ending in `import preview_site` / `import tunnel_site`,
11
+ and those snippets have to exist on the box first — otherwise `caddy validate` fails on
12
+ every deploy forever, per project, and nothing routes.
13
+
14
+ This existed only as an inline `package.json` script in the tosijs-ui repo, so adopters got
15
+ the template and the warning but not the tool. That is the wrong half to keep private: the
16
+ template is inert text, while the VALUE here is the refusal to install it with placeholders
17
+ intact. Installing `{{ACME_EMAIL}}` and `__PREVIEW_TOKEN__` verbatim gives you a preview host
18
+ whose invite gate is a literal string published in a public repo, issuing certificates under
19
+ someone else's Let's Encrypt account. A hand-rolled `sed` pipeline has no such guard.
20
+
21
+ Three deliberate properties:
22
+
23
+ DRY RUN BY DEFAULT. This replaces `/etc/caddy/Caddyfile` wholesale. If the box serves
24
+ anything else through Caddy, that is destructive — so the dry run prints a diff against
25
+ the live file and you get to look at it before `--go`.
26
+
27
+ THE SECRET NEVER LEAVES THE BOX. Substitution happens remotely, reading
28
+ `/etc/caddy/preview.env`. `PREVIEW_TOKEN` is never transmitted, never appears in argv on
29
+ this machine, and is redacted out of the diff before it is printed.
30
+
31
+ VALIDATE BEFORE SWAP. `caddy fmt` then `caddy validate` run against a scratch file; the
32
+ live config is replaced by an atomic `mv` only once it is known to parse. A rejected
33
+ config leaves the running site untouched.
34
+ */
35
+
36
+ import { existsSync, readFileSync } from 'fs'
37
+ import { resolveSiteConfig } from './resolve-site-config'
38
+
39
+ const siteConfig = await resolveSiteConfig()
40
+
41
+ const args = process.argv.slice(2)
42
+ const flag = (name: string): string | undefined => {
43
+ const hit = args.find((a) => a.startsWith(`--${name}=`))
44
+ return hit ? hit.slice(name.length + 3) : undefined
45
+ }
46
+ const has = (name: string): boolean => args.includes(`--${name}`)
47
+
48
+ /*
49
+ Reject anything we do not understand, BEFORE reaching the network.
50
+
51
+ An unrecognised flag used to be ignored, so `--status` — which this bin has never had — read
52
+ as "no flags at all" and the bin went straight on to ssh into the configured host. A tool
53
+ that contacts someone's server must not treat an unknown instruction as consent to proceed.
54
+ */
55
+ const KNOWN = ['--go', '--help']
56
+ const unknown = args.filter(
57
+ (a) => !KNOWN.includes(a) && !/^--(host|template)=/.test(a)
58
+ )
59
+ if (unknown.length || has('help')) {
60
+ const bad = unknown.length
61
+ console.error(
62
+ (bad ? `\nUnknown option(s): ${unknown.join(' ')}\n` : '') +
63
+ `\ntosijs-caddy-install — install the Caddy snippets the deploy/tunnel bins need\n\n` +
64
+ ` tosijs-caddy-install DRY RUN — shows the diff, changes nothing\n` +
65
+ ` tosijs-caddy-install --go substitute, validate, install, reload\n` +
66
+ ` --host=user@box override the configured preview host\n` +
67
+ ` --template=./path/to/Caddyfile use your own template\n`
68
+ )
69
+ process.exit(bad ? 1 : 0)
70
+ }
71
+
72
+ const host =
73
+ flag('host') ??
74
+ process.env.PREVIEW_HOST ??
75
+ // Legacy alias. This repo's own `deploy:index` script still asks for PREVIEW_SSH,
76
+ // so someone who exported the only variable it ever mentioned would otherwise be
77
+ // told "No preview host" with nothing pointing at why.
78
+ process.env.PREVIEW_SSH ??
79
+ siteConfig.preview?.host
80
+ const go = has('go')
81
+
82
+ if (!host) {
83
+ console.error(
84
+ `\nNo preview host.\n\n` +
85
+ ` Set one once in your site config:\n` +
86
+ ` preview: { host: 'user@example.com' }\n\n` +
87
+ ` ...or pass it per-run:\n` +
88
+ ` tosijs-caddy-install --host=user@example.com --go\n` +
89
+ ` PREVIEW_HOST=user@example.com tosijs-caddy-install\n`
90
+ )
91
+ process.exit(1)
92
+ }
93
+
94
+ /*
95
+ The template ships inside the package, so the default is resolved relative to THIS file
96
+ rather than to the cwd — an adopter runs the bin from their own project, where `deploy/` is
97
+ their business and not ours.
98
+ */
99
+ const templatePath =
100
+ flag('template') ?? new URL('../deploy/Caddyfile', import.meta.url).pathname
101
+
102
+ if (!existsSync(templatePath)) {
103
+ console.error(
104
+ `\nNo Caddyfile template at ${templatePath}\n\n` +
105
+ ` The package ships one at node_modules/tosijs-ui/deploy/Caddyfile.\n` +
106
+ ` Pass your own with --template=./path/to/Caddyfile\n`
107
+ )
108
+ process.exit(1)
109
+ }
110
+
111
+ const template = readFileSync(templatePath, 'utf8')
112
+
113
+ // The template is delivered inside a quoted heredoc, so it is copied verbatim with no shell
114
+ // expansion. A template containing the delimiter would truncate the file silently — refuse
115
+ // rather than install something that is not what was read.
116
+ const EOF_MARKER = 'TOSIJS_CADDY_TEMPLATE_EOF'
117
+ if (template.split('\n').some((line) => line.trim() === EOF_MARKER)) {
118
+ console.error(
119
+ `\nTemplate contains the line \`${EOF_MARKER}\`, which is used as the transfer\n` +
120
+ `delimiter. Rename that line in ${templatePath}.\n`
121
+ )
122
+ process.exit(1)
123
+ }
124
+
125
+ const remoteScript = `set -euo pipefail
126
+ umask 077
127
+ cat > /etc/caddy/Caddyfile.tpl <<'${EOF_MARKER}'
128
+ ${template}
129
+ ${EOF_MARKER}
130
+
131
+ if [ ! -f /etc/caddy/preview.env ]; then
132
+ echo "No /etc/caddy/preview.env on this box." >&2
133
+ echo "Create it with PREVIEW_TOKEN=, ACME_EMAIL= and PREVIEW_DOMAIN=." >&2
134
+ exit 1
135
+ fi
136
+ set -a; . /etc/caddy/preview.env; set +a
137
+ : "\${PREVIEW_TOKEN:?set PREVIEW_TOKEN in /etc/caddy/preview.env}"
138
+ : "\${ACME_EMAIL:?set ACME_EMAIL in /etc/caddy/preview.env}"
139
+ : "\${PREVIEW_DOMAIN:?set PREVIEW_DOMAIN in /etc/caddy/preview.env}"
140
+
141
+ sed -e "s|__PREVIEW_TOKEN__|\$PREVIEW_TOKEN|g" \\
142
+ -e "s|{{ACME_EMAIL}}|\$ACME_EMAIL|g" \\
143
+ -e "s|{{PREVIEW_DOMAIN}}|\$PREVIEW_DOMAIN|g" \\
144
+ /etc/caddy/Caddyfile.tpl > /etc/caddy/Caddyfile.new
145
+
146
+ if grep -qE '\\{\\{|__PREVIEW_TOKEN__' /etc/caddy/Caddyfile.new; then
147
+ echo "Unsubstituted placeholders remain — refusing to install:" >&2
148
+ grep -nE '\\{\\{|__PREVIEW_TOKEN__' /etc/caddy/Caddyfile.new >&2
149
+ rm -f /etc/caddy/Caddyfile.new
150
+ exit 1
151
+ fi
152
+
153
+ caddy fmt --overwrite /etc/caddy/Caddyfile.new
154
+ caddy validate --config /etc/caddy/Caddyfile.new
155
+
156
+ if [ "${go ? '1' : '0'}" = "1" ]; then
157
+ # Keep the outgoing config. This replaces a file that may be serving OTHER sites, and
158
+ # "validated" only means it parses — it does not mean it is the config you wanted.
159
+ if [ -f /etc/caddy/Caddyfile ]; then
160
+ cp -p /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak
161
+ fi
162
+ mv /etc/caddy/Caddyfile.new /etc/caddy/Caddyfile
163
+ systemctl reload caddy
164
+ echo "Installed /etc/caddy/Caddyfile and reloaded caddy."
165
+ echo "Previous config saved as /etc/caddy/Caddyfile.bak"
166
+ else
167
+ echo "--- would change /etc/caddy/Caddyfile (token redacted) ---"
168
+ if [ -f /etc/caddy/Caddyfile ]; then
169
+ diff -u /etc/caddy/Caddyfile /etc/caddy/Caddyfile.new \\
170
+ | sed "s|\$PREVIEW_TOKEN|***REDACTED***|g" || true
171
+ else
172
+ echo "(no existing /etc/caddy/Caddyfile — this would create it)"
173
+ fi
174
+ rm -f /etc/caddy/Caddyfile.new
175
+ echo "--- dry run: nothing changed. Re-run with --go to install. ---"
176
+ fi`
177
+
178
+ console.log(
179
+ `${
180
+ go ? 'Installing' : 'Checking'
181
+ } Caddy config on ${host} from ${templatePath}\n`
182
+ )
183
+
184
+ // The script goes over stdin rather than as an argv string: it contains the template
185
+ // verbatim, and quoting an arbitrary file through a shell argument is a bug waiting to
186
+ // happen.
187
+ const proc = Bun.spawn(['ssh', host, 'bash', '-s'], {
188
+ stdin: new TextEncoder().encode(remoteScript),
189
+ stdout: 'inherit',
190
+ stderr: 'inherit',
191
+ })
192
+ const exitCode = await proc.exited
193
+
194
+ if (exitCode !== 0) {
195
+ console.error(`\nFailed (exit ${exitCode}). Nothing was installed.\n`)
196
+ process.exit(exitCode)
197
+ }
198
+
199
+ if (!go) {
200
+ console.log(
201
+ `\nNext: re-run with --go once the diff above looks right.\n` +
202
+ `Then \`tosijs-deploy --go\` and \`tosijs-tunnel\` will validate.\n`
203
+ )
204
+ }
@@ -51,7 +51,14 @@ const preview = siteConfig.preview
51
51
  // Precedence: explicit flag > env > site config. The config is the "just works"
52
52
  // default; the flag exists so you can push a one-off somewhere else without editing
53
53
  // (and committing) a config file.
54
- const host = flag('host') ?? process.env.PREVIEW_HOST ?? preview?.host
54
+ const host =
55
+ flag('host') ??
56
+ process.env.PREVIEW_HOST ??
57
+ // Legacy alias. This repo's own `deploy:index` script still asks for PREVIEW_SSH,
58
+ // so someone who exported the only variable it ever mentioned would otherwise be
59
+ // told "No preview host" with nothing pointing at why.
60
+ process.env.PREVIEW_SSH ??
61
+ preview?.host
55
62
  const remotePath =
56
63
  flag('path') ??
57
64
  process.env.PREVIEW_PATH ??
package/bin/tunnel.ts CHANGED
@@ -5,7 +5,7 @@ Expose THIS machine's dev server at an authenticated public URL.
5
5
  tosijs-tunnel # open the tunnel (foreground; Ctrl-C closes it)
6
6
  tosijs-tunnel --status # is one already up?
7
7
  tosijs-tunnel --close # close any tunnel this project opened
8
- tosijs-tunnel --link # ask the dev server to print a fresh single-use edit link
8
+ tosijs-tunnel --link # ask the dev server to print a fresh edit link
9
9
 
10
10
  (In this repo `bun run tunnel` is a shortcut for the same thing.)
11
11
 
@@ -49,7 +49,14 @@ const flag = (n: string) => {
49
49
  }
50
50
 
51
51
  const preview = siteConfig.preview
52
- const host = flag('host') ?? process.env.PREVIEW_HOST ?? preview?.host
52
+ const host =
53
+ flag('host') ??
54
+ process.env.PREVIEW_HOST ??
55
+ // Legacy alias. This repo's own `deploy:index` script still asks for PREVIEW_SSH,
56
+ // so someone who exported the only variable it ever mentioned would otherwise be
57
+ // told "No preview host" with nothing pointing at why.
58
+ process.env.PREVIEW_SSH ??
59
+ preview?.host
53
60
  const localPort = Number(
54
61
  flag('port') ?? resolveDevPort(siteConfig, process.env)
55
62
  )
@@ -1,6 +1,6 @@
1
1
  import { Component as WebComponent, ElementCreator } from 'tosijs';
2
2
  import { SortCallback } from './make-sorter.js';
3
- import { RowGroupIdFn } from './row-grouping.js';
3
+ import { RowGroupIdFn, GroupCount } from './row-grouping.js';
4
4
  import { ValueRendererType } from './value-renderer.js';
5
5
  export interface ColumnOptions {
6
6
  name?: string;
@@ -96,7 +96,11 @@ export declare class TosiTable extends WebComponent {
96
96
  background: string;
97
97
  };
98
98
  ':host .tr:not(.table-cluster-first) .cluster-repeat': {
99
- visibility: string;
99
+ color: string;
100
+ userSelect: string;
101
+ };
102
+ ':host .tr:not(.table-cluster-first) .cluster-repeat > *': {
103
+ display: string;
100
104
  };
101
105
  ':host .tr.table-cluster-odd': {
102
106
  _tosiTableBg: string;
@@ -155,6 +159,7 @@ export declare class TosiTable extends WebComponent {
155
159
  nohide: boolean;
156
160
  noreorder: boolean;
157
161
  localized: boolean;
162
+ nopreservescroll: boolean;
158
163
  };
159
164
  selectionChanged: SelectCallback;
160
165
  rowRendered: ((item: any, cells: HTMLElement[]) => void) | null;
@@ -200,8 +205,48 @@ export declare class TosiTable extends WebComponent {
200
205
  get nonRepeatingGroupedRowCells(): string[] | null;
201
206
  set nonRepeatingGroupedRowCells(props: string[] | null);
202
207
  /** The grouping function in force, or null when the table is ungrouped. */
208
+ private _groupIdMemo;
203
209
  private get groupIdFn();
210
+ /**
211
+ * This row's group id, or null when the table is ungrouped.
212
+ *
213
+ * Public because the grouping may be INFERRED from `nonRepeatingGroupedRowCells`, in which
214
+ * case the consumer never wrote the function and cannot reproduce its ids — which would
215
+ * leave `rowGroupCounts` keyed by strings they have no way to construct.
216
+ */
217
+ groupIdFor(row: any): string | null;
218
+ /**
219
+ * Per-group `{ visible, total }` counts — rendered rows against rows before filtering.
220
+ *
221
+ * For cells that report or control their own group: "showing 2 of 7", or a toggle that
222
+ * adds the group to `visibleGroupedRowIds`. The table is the only thing that sees both
223
+ * sides of the filter, so this is the piece a custom cell renderer cannot derive itself.
224
+ *
225
+ * Recomputed each render, and always a Map — empty when ungrouped — so callers can `.get()`
226
+ * without a null check. Groups filtered away entirely are present with `visible: 0`;
227
+ * pinned rows sit outside grouping and are not counted.
228
+ */
229
+ get rowGroupCounts(): Map<string, GroupCount>;
204
230
  private _grouping;
231
+ private _rowGroupCounts;
232
+ /**
233
+ * Keep the reader in place across a re-render (default `true`).
234
+ *
235
+ * Set `false` when a render means "here is a different dataset" rather than "here is the
236
+ * same data, re-viewed" — then starting at the top is the correct answer.
237
+ *
238
+ * Backed by the `nopreservescroll` ATTRIBUTE so it is settable from markup like every
239
+ * other boolean here (`nosort`, `nohide`, `noreorder`). The inverted name is not
240
+ * gratuitous: this defaults to `true`, and a presence-only attribute can only ever turn
241
+ * something ON — so a `preservescroll` attribute could never express "off", which is the
242
+ * only thing anyone needs to say. Getting this wrong is unfixable after release without a
243
+ * rename, so it is settled here rather than later.
244
+ */
245
+ get preserveScroll(): boolean;
246
+ set preserveScroll(value: boolean);
247
+ /** In-flight scroll restore, so a newer one supersedes it and gestures can cancel it. */
248
+ private _scrollRestore;
249
+ private _scrollAnchor;
205
250
  get sort(): SortCallback | undefined;
206
251
  set sort(sortFunc: SortCallback | undefined);
207
252
  get columns(): ColumnOptions[];
@@ -248,6 +293,8 @@ export declare class TosiTable extends WebComponent {
248
293
  private updateSelection;
249
294
  private findCell;
250
295
  private _pendingFocus;
296
+ private captureScrollAnchor;
297
+ private restoreScrollAnchor;
251
298
  private handleScrollEnd;
252
299
  private focusCell;
253
300
  private handleKeyNav;