tosijs-ui 1.9.6 → 1.9.7

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,94 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.7
4
+
5
+ **`<tosi-table>` can group rows.** The motivating shape is invoice lines: rows that belong
6
+ together, striped as a unit, with the values they all repeat shown once.
7
+
8
+ ```js
9
+ table.rowGroupId = (row) => `${row.invoice}/${row.buyer}`
10
+ table.nonRepeatingGroupedRowCells = ['invoice', 'buyer'] // shown once per group
11
+ table.visibleGroupedRowIds = ['INV-1001/Acme'] // stays visible past the filter
12
+ ```
13
+
14
+ Set `nonRepeatingGroupedRowCells` on its own and the grouping is **inferred** from exactly
15
+ those columns, so the common case is one line. Grouped rows get `table-cluster-even` /
16
+ `table-cluster-odd`, alternating **per group** — a five-line invoice is one stripe, not
17
+ five.
18
+
19
+ `visibleGroupedRowIds` keeps whole groups visible regardless of the filter, so a search
20
+ matching one line of an invoice can open the whole invoice without the filter needing to
21
+ know anything about grouping. It is additive to the filter's own result, so a filter that
22
+ ranks as well as selects keeps its ranking.
23
+
24
+ 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.
31
+ `table.isFirstInGroup(row)` answers the same question in JavaScript.
32
+
33
+ Ungrouped tables are unaffected — all three properties default to off.
34
+
35
+ **The inline doc-test lane was under-reporting, and staying green about it.** If you use
36
+ `tosijs-ui/site` and have doc pages whose examples do async work, **your test counts may
37
+ have been wrong.** A test iframe reported "page done" as soon as its _first_ example
38
+ settled, so any example still awaiting a slow `js` block — a `fetch`, typically — never ran
39
+ its tests, and they were omitted rather than failed. On this repo's own table page that was
40
+ 8 tests silently becoming 1, with the suite reporting "passed" either way.
41
+
42
+ Completion is now decided by which examples have `test` **source**, which is known before
43
+ anything executes, so an example that has not started yet is waited on instead of
44
+ overlooked. An example that never finishes now fails by name with an excerpt of its source.
45
+ The rule itself is exported as `unsettledExamples()` / `isSettled()` from
46
+ `src/doc-system/test-completion.ts`, pure and unit-tested — a bug whose symptom is a
47
+ _missing_ test cannot be caught by the browser lane it breaks.
48
+
49
+ Check your own counts against what your pages actually contain; a number that goes **up**
50
+ after upgrading is this bug, not a new one.
51
+
52
+ **The dev server no longer 404s while it rebuilds** — the actual cause behind the LAN
53
+ stalls in #63, traced by tosijs-3d.
54
+
55
+ `buildSite` moves the output directory aside (`mv docs docs.last-good`) and repopulates it
56
+ from empty, so for the length of every build **every served path is simply absent**. Two
57
+ failures follow:
58
+
59
+ - a request that _starts_ in that window gets a plain 404 — the page loads and never
60
+ hydrates
61
+ - a request already _in flight_ has its file vanish underneath it, stops producing data,
62
+ and idles until something closes it — the "loads then stalls" symptom
63
+
64
+ It only ever bit over the LAN because a loopback transfer finishes in ~22 ms and almost
65
+ never overlaps the hole, while a multi-MB transfer to a phone or a second machine is
66
+ easily still running two seconds later. It also explains why reloading never helped while
67
+ you were actively editing: every save reopened the window.
68
+
69
+ The dev server now falls back to the last-good tree for any path missing from the live
70
+ one. That copy exists for exactly the window's duration — created by the stash at the
71
+ start of a build, removed on success — so the fallback is self-scoping, and no stale
72
+ content can be served outside a rebuild.
73
+
74
+ Measured across real rebuilds: **2 failed requests before, 0 after**.
75
+
76
+ **Formatting is gated.** `bun format` was named in no gate, so drift only accumulated — 24
77
+ unformatted files at 1.9.0, 40 by now — and the cost always landed on whoever next ran it,
78
+ as three dozen unrelated files in their feature diff. New `bun run format-check`
79
+ (`prettier --check .`), run in CI. Adopters get the script; nothing about the published
80
+ package changes.
81
+
82
+ > **On versioning.** This is a patch that adds public API, which the project's own rule
83
+ > previously called a minor. The rule has changed: minors are for **breaking changes and
84
+ > feature rollouts**, while additive, non-breaking extensions ship as patches. The contract
85
+ > you rely on is unchanged and is the whole point — **a patch never breaks you.**
86
+
87
+ > The `idleTimeout` raise and asset compression in 1.9.6 both help, but neither closes this
88
+ > window — compression shortens the exposure, and `idleTimeout` only decides how long a
89
+ > stalled request waits before failing. If you are on 1.9.6 and still seeing LAN stalls,
90
+ > this is the release that fixes them.
91
+
3
92
  ## 1.9.6
4
93
 
5
94
  **`<tosi-table>` sorts correctly** (#62, reported by snowfox). Two independent defects, one
@@ -19,11 +108,11 @@ arrived as text, anything id-shaped. Sorting now compares naturally:
19
108
  - blank cells sort **last in both directions** — a descending sort that opens on a
20
109
  screenful of empty cells is never what was clicked for
21
110
 
22
- The old comparator also never returned `0`, so two *equal* values each claimed to be
111
+ The old comparator also never returned `0`, so two _equal_ values each claimed to be
23
112
  greater than the other. `Array.sort` is entitled to turn an inconsistent comparator into
24
113
  arbitrary output, not merely wrong output.
25
114
 
26
- **New `ColumnOptions.sortValue`** — sort by what the cell *shows*, when that differs from
115
+ **New `ColumnOptions.sortValue`** — sort by what the cell _shows_, when that differs from
27
116
  what the row stores. A column with a custom `dataCell` renders whatever it likes while the
28
117
  sort keyed on `prop`, so the rows reordered by an invisible value:
29
118
 
@@ -72,7 +161,7 @@ same comparison is wanted outside tables.
72
161
  tosijs#20 (the `parts` proxy crossing into nested components) and #21 — and that reason
73
162
  is now recorded in `CLAUDE.md` rather than only in an old changelog entry.
74
163
 
75
- `makeSorter` deliberately keeps its existing comparison: switching it would change *case*
164
+ `makeSorter` deliberately keeps its existing comparison: switching it would change _case_
76
165
  ordering (`'Zed'` before `'alice'` today) in a general-purpose utility where that has not
77
166
  been reported as a problem. Coerce numeric strings in the valuator, or pass values through
78
167
  `naturalCompare` yourself.
@@ -90,18 +179,18 @@ An annotation is a **suffix identifier**, so only a `#` that looks like one is t
90
179
  one — immediately preceded by a non-space, followed by letters/digits/`_`/`-`, at the end
91
180
  of the string:
92
181
 
93
- | string | read as |
94
- | --- | --- |
95
- | `Okay#confirm` | annotation |
96
- | `C# Tutorial` | literal — space after |
97
- | `Issue #42` | literal — space before |
98
- | `#hashtag` | literal — nothing before |
99
- | `C#` | literal — no annotation after |
182
+ | string | read as |
183
+ | -------------- | ----------------------------- |
184
+ | `Okay#confirm` | annotation |
185
+ | `C# Tutorial` | literal — space after |
186
+ | `Issue #42` | literal — space before |
187
+ | `#hashtag` | literal — nothing before |
188
+ | `C#` | literal — no annotation after |
100
189
 
101
190
  No author action and no data migration; every documented annotation form is unchanged.
102
191
  Escape with `\#` for a literal that genuinely looks like an annotation (`tag\#42`).
103
192
 
104
- **The worse half:** annotations were stripped from the *translated value* too, so a
193
+ **The worse half:** annotations were stripped from the _translated value_ too, so a
105
194
  translation containing `#` was truncated **even when the source string had none** —
106
195
  `'Sharp'` → `'Dièse #1'` came back as `'Dièse '`. A translator writing an ordinary string
107
196
  had it silently cut, with nothing in the source to hint why. Translations are no longer
@@ -121,9 +210,9 @@ it resolves to.
121
210
 
122
211
  It spawned its own channel via `bunx haltija@^1.6.1` and ignored the project's dependency
123
212
  entirely — and because **bunx caches the resolution**, a range that resolves forward never
124
- *re*-resolves once its cache key exists. So an adopter who bumped `haltija` to `^1.11.2`
213
+ _re_-resolves once its cache key exists. So an adopter who bumped `haltija` to `^1.11.2`
125
214
  for a fix, restarted, and still didn't have the fix was running a cached 1.11.0: new
126
- enough to look current, old enough to lack it. `hj where` reports the *spawned* server, so
215
+ enough to look current, old enough to lack it. `hj where` reports the _spawned_ server, so
127
216
  the version indicator agreed with them.
128
217
 
129
218
  - Your `node_modules/.bin/haltija` is preferred when present. `HALTIJA_VERSION` still
@@ -156,7 +245,7 @@ upstream as [oven-sh/bun#36788](https://github.com/oven-sh/bun/issues/36788).
156
245
  ### ePub volumes are now linkable (#46)
157
246
 
158
247
  The build wrote an ePub per volume and linked to none of them, so a reader had no route
159
- to a book that existed. The filename is *derived* (`<project>-<volume>.epub`), so
248
+ to a book that existed. The filename is _derived_ (`<project>-<volume>.epub`), so
160
249
  hand-written links rot silently when a volume is renamed.
161
250
 
162
251
  Three ways to surface them, and the ePub build now names its output through the same
@@ -178,7 +267,7 @@ at.
178
267
  walled you with "that invite link has been used". The stale token is simply irrelevant
179
268
  to someone who already holds a session.
180
269
  - **`tosijs-tunnel --close` confirms identity before signalling.** It SIGTERMed every
181
- `pgrep -f` match, and `pgrep -f` matches an argv *substring* — a shell echoing the
270
+ `pgrep -f` match, and `pgrep -f` matches an argv _substring_ — a shell echoing the
182
271
  command or an editor holding a log path both matched. It now checks the process really
183
272
  is `ssh`, says what it signalled, and names what it skipped. It also interpolated `host`
184
273
  into a REGEX unescaped, so `me@a.b` matched `me@axb` and a host containing `+`, `(` or
@@ -199,15 +288,15 @@ at.
199
288
  `hj map` (haltija 1.11) over the doc site, which reports a measured contrast ratio per
200
289
  node — none of these were visible as wrong to the eye.
201
290
 
202
- | pair | was | now |
203
- | --- | --- | --- |
204
- | test widget, running | white on `#fa0` — **1.9:1** | `#2b1a00` on `#fa0` — 8.8:1 |
205
- | test widget, passed | white on `#0a0` — **3.1:1** | white on `#008a00` — 4.5:1 |
206
- | `--tosi-accent` as text | `#EE257B` — **4.05:1** on white, 4.3:1 on the page | `#d92270` — 4.6:1 |
207
- | `--tosi-accent` as a fill | white on `#EE257B` — **4.05:1** | white on `#d92270` — 4.8:1 |
291
+ | pair | was | now |
292
+ | ------------------------- | -------------------------------------------------- | --------------------------- |
293
+ | test widget, running | white on `#fa0` — **1.9:1** | `#2b1a00` on `#fa0` — 8.8:1 |
294
+ | test widget, passed | white on `#0a0` — **3.1:1** | white on `#008a00` — 4.5:1 |
295
+ | `--tosi-accent` as text | `#EE257B` — **4.05:1** on white, 4.3:1 on the page | `#d92270` — 4.6:1 |
296
+ | `--tosi-accent` as a fill | white on `#EE257B` — **4.05:1** | white on `#d92270` — 4.8:1 |
208
297
 
209
298
  **The brand pink is 9% darker** (`#EE257B` → `#d92270`). It is imperceptible side by side
210
- and it was the only value that clears AA in *both* directions — as text on the page, and
299
+ and it was the only value that clears AA in _both_ directions — as text on the page, and
211
300
  as a fill carrying white text. Override `--tosi-accent` if you want the original.
212
301
 
213
302
  Two more, found by sweeping wider — both in **example code we publish**, which matters
@@ -227,8 +316,8 @@ Verified across 8 pages in both themes on haltija 1.11.2: **0 failures, 0 uncert
227
316
 
228
317
  **The browser-test lane now runs its own private haltija.** It used to reuse any running
229
318
  instance and otherwise spawn with `-f`, which reclaims haltija's shared default port and
230
- kills whatever held it. Both halves were wrong: adopting meant inheriting the *desktop
231
- app's* window, whose visibility depends on whatever else is on screen — and `hj` rightly
319
+ kills whatever held it. Both halves were wrong: adopting meant inheriting the _desktop
320
+ app's_ window, whose visibility depends on whatever else is on screen — and `hj` rightly
232
321
  refuses to drive a hidden tab, since a backgrounded tab throttles `requestAnimationFrame`
233
322
  and would produce plausible-but-wrong results. The lane failed roughly two runs in three
234
323
  with no programmatic way out. It now takes an isolated instance on an ephemeral port:
@@ -260,11 +349,11 @@ Two runtime requirements are unchanged, and are now documented rather than impli
260
349
  are honest constraints, not bugs, but the failure messages named symptoms rather than
261
350
  causes:
262
351
 
263
- | entry point | runtime |
264
- | --- | --- |
265
- | `tosijs-ui/site` | **bun** — it shells out, builds and spawns |
266
- | `tosijs-ui`, `tosijs-ui/<component>` | a **browser** or a bundler targeting one |
267
- | `tosijs-ui/icon-svg` | anything — deliberately DOM-free |
352
+ | entry point | runtime |
353
+ | ------------------------------------ | ------------------------------------------ |
354
+ | `tosijs-ui/site` | **bun** — it shells out, builds and spawns |
355
+ | `tosijs-ui`, `tosijs-ui/<component>` | a **browser** or a bundler targeting one |
356
+ | `tosijs-ui/icon-svg` | anything — deliberately DOM-free |
268
357
 
269
358
  `engines` now declares bun, and `doc-site-system.md` has a "Runtimes — what runs where"
270
359
  section.
@@ -1,5 +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
4
  import { ValueRendererType } from './value-renderer.js';
4
5
  export interface ColumnOptions {
5
6
  name?: string;
@@ -94,6 +95,12 @@ export declare class TosiTable extends WebComponent {
94
95
  zIndex: string;
95
96
  background: string;
96
97
  };
98
+ ':host .tr:not(.table-cluster-first) .cluster-repeat': {
99
+ visibility: string;
100
+ };
101
+ ':host .tr.table-cluster-odd': {
102
+ _tosiTableBg: string;
103
+ };
97
104
  ':host .tr[aria-selected] .td': {
98
105
  background: string;
99
106
  };
@@ -169,6 +176,9 @@ export declare class TosiTable extends WebComponent {
169
176
  private _columns;
170
177
  private _filter;
171
178
  private _sort?;
179
+ private _rowGroupId;
180
+ private _visibleGroupedRowIds;
181
+ private _nonRepeatingGroupedRowCells;
172
182
  private _pinnedTopRows?;
173
183
  private _pinnedBottomRows?;
174
184
  get pinnedTopRows(): any[] | undefined;
@@ -183,6 +193,15 @@ export declare class TosiTable extends WebComponent {
183
193
  set array(newArray: any[]);
184
194
  get filter(): ArrayFilter;
185
195
  set filter(filterFunc: ArrayFilter);
196
+ get rowGroupId(): RowGroupIdFn | null;
197
+ set rowGroupId(fn: RowGroupIdFn | null);
198
+ get visibleGroupedRowIds(): string[] | null;
199
+ set visibleGroupedRowIds(ids: string[] | null);
200
+ get nonRepeatingGroupedRowCells(): string[] | null;
201
+ set nonRepeatingGroupedRowCells(props: string[] | null);
202
+ /** The grouping function in force, or null when the table is ungrouped. */
203
+ private get groupIdFn();
204
+ private _grouping;
186
205
  get sort(): SortCallback | undefined;
187
206
  set sort(sortFunc: SortCallback | undefined);
188
207
  get columns(): ColumnOptions[];
@@ -205,6 +224,15 @@ export declare class TosiTable extends WebComponent {
205
224
  private cellStyle;
206
225
  private applyGridCellAttrs;
207
226
  private buildCell;
227
+ private tagClusterParity;
228
+ /**
229
+ * Is this row the first of its group? Always true when the table is ungrouped.
230
+ *
231
+ * Exposed for custom `dataCell` columns, which render themselves and so have to decide
232
+ * for themselves whether to show a value the rest of the group repeats.
233
+ */
234
+ isFirstInGroup(row: any): boolean;
235
+ private isRepeatedGroupRow;
208
236
  private buildRow;
209
237
  private buildHeaderCell;
210
238
  private buildHeader;