toga-ai 1.0.595 → 1.0.596

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.
@@ -55,6 +55,13 @@ Constants in `PrimaryTable.tsx`: `DEFAULT_MIN_COLUMN_WIDTH = 80`, `ABSOLUTE_MIN_
55
55
 
56
56
  1. **Measure** — on first paint, measure header + widest body cell content per column
57
57
  (`measureCellContentWidth`), store in `frozenWidths`; per-column floors in `contentFloors`.
58
+ The **full header width is a HARD floor for every column** (`headerFloors` state): it is not
59
+ capped by max-width, not overridden by a consumer `minColumnWidth`, and is applied even to
60
+ explicitly-pinned (`columnWidths`) columns — the earlier `return def.size` bypass was why
61
+ pinned columns stayed clipped. Header width is measured **deterministically** = label text
62
+ `scrollWidth` + a fixed ~34px reserve per sort/filter control, **not** by measuring the
63
+ icon-font/SVG control elements (which can report ~0 width pre-paint on the freeze pass,
64
+ causing both header abbreviation and icon overlap).
58
65
  2. **Freeze** — apply `table-layout: fixed` + a `<colgroup>` of frozen pixel widths so columns
59
66
  don't reflow while virtualizing. Resets when the leaf column set changes. Empty tables stay
60
67
  on auto-layout.
@@ -78,6 +85,10 @@ Sticky columns use a `--sticky-offset` CSS variable summed from neighboring colu
78
85
  ## Gotchas
79
86
 
80
87
  - Frozen widths measure only **mounted** rows; virtualized tables size from the first paint set.
88
+ - **Header labels must never abbreviate or overlap.** The full header width is a hard floor
89
+ (above `maxColumnWidth`, `minColumnWidth`, and pinned `columnWidths`). Measure header width
90
+ deterministically (label `scrollWidth` + fixed per-control reserve), never by measuring the
91
+ async-loading sort/filter icon elements — they can report ~0 width before they paint.
81
92
  - `headerSpan` adds a separate top row (`colSpan`); only the leaf header row maps to colgroup.
82
93
  - Resize is non-persistent (local state, resets when columns change).
83
94
  - **A visible column can look "missing" under `shrinkColumns`.** `shrinkColumns` (modal tables)
@@ -87,6 +98,11 @@ Sticky columns use a `--sticky-offset` CSS variable summed from neighboring colu
87
98
  not the column set.
88
99
 
89
100
  ## Change history
101
+ - 2026-08-17 — Fixed clipped/abbreviated headers: full header width is now a hard floor for every
102
+ column (not capped by max-width, not overridden by `minColumnWidth`, applied even to pinned
103
+ `columnWidths` — the old `return def.size` bypass left pinned columns clipped). Added a
104
+ `headerFloors` state; header width is now measured deterministically (label `scrollWidth` +
105
+ fixed ~34px per sort/filter control) instead of measuring async-loading icon elements. (apeterson)
90
106
  - 2026-08-17 — Noted that under `shrinkColumns` a genuinely visible column can render at a sliver width and look "missing" — check `isVisible` and the measured/frozen width, not the column set (apeterson).
91
107
  - 2026-06-23 — Documented the three Primary Table entry points, measure-and-freeze sizing, virtualization, and sticky columns (apeterson).
92
108
  - 2026-06-22 — Added `shrinkColumns` / `minColumnWidth` shrink-to-fit for modal tables; freeze logic uses natural content width (apeterson).
@@ -88,13 +88,18 @@ isHovered, skin, isDisabled }`. They are inserted as columns via `buildActionCol
88
88
 
89
89
  Per-column header controls, each a popover trigger with `data-active` / `data-open`:
90
90
 
91
- - **`HeaderFilterSearch`** — text filter, 5 modes (startsWith/endsWith/exactly/includes/
92
- excludes) chosen from a **mode dropdown that excludes the currently selected mode**.
93
- **Tag-based**: confirmed values become removable tags (`activeTags`); `isActive =
94
- activeTags.length > 0`. The mode dropdown is portalled to `document.body` with fixed
95
- positioning recomputed on scroll. The search icon (solid when active, regular otherwise)
96
- is hidden while the input is focused; both the trigger button and the icon span carry
97
- `data-active`. The clear button must not widen the input row (fixed-width menu).
91
+ - **`HeaderFilterSearch`** — text filter, **multi-chip with per-chip mode**. Each confirmed
92
+ search becomes its own `TextFilterTag = { value, mode }` (mode Includes / Starts with /
93
+ Ends with / Exactly / Excludes). The mode dropdown sets the mode for the **next** chip
94
+ added; existing chips keep the mode they were created with. All chips on a column are
95
+ combined with **AND**. `onFilterChange` emits the full tags array (`{ tags }`); the
96
+ component rehydrates every chip from props and renders each as "Mode: value". `isActive =
97
+ tags.length > 0`. The mode dropdown is portalled to `document.body` with fixed positioning
98
+ recomputed on scroll. The search icon (solid when active, regular otherwise) is hidden
99
+ while the input is focused; both the trigger button and the icon span carry `data-active`.
100
+ The clear button must not widen the input row (fixed-width menu).
101
+ (Previously a column allowed only one term / one mode — a second search replaced the first
102
+ and switching mode re-applied it to everything.)
98
103
  - **`HeaderFilterRange`** — numeric, modes exactly/moreThan/lessThan + a range toggle
99
104
  (min/max); emits `{ key, value }[]` (`eq`/`min`/`max`/`between`). Reads its persisted
100
105
  operator/values back from a `filterParams` prop (not `filterValue`, which collapses to the
@@ -134,6 +139,10 @@ Per-column header controls, each a popover trigger with `data-active` / `data-op
134
139
  ## Gotchas
135
140
 
136
141
  - `MIN_COLUMN_WIDTH = 120` is the shrink floor only for columns with **no** explicit width.
142
+ - **Never truncate header labels.** `truncateCells` (`.truncateCells .headerCellLabel` in
143
+ `toga.module.css`) is for **body** values only; it must apply only `white-space: nowrap` to
144
+ the header (no `overflow:hidden`/`text-overflow:ellipsis`/wrapping) or headers show ellipses.
145
+ Headers must render in full on one line, never abbreviated, never overlapping.
137
146
  - The filter `includes` mode treats comma-separated values as OR.
138
147
  - `EditableCell` currency formats **on blur**, phone formats **on keystroke**.
139
148
  - Cell type config (`columnTypeConfig` prop) overrides the hardcoded `COLUMN_TYPE_CONFIG`.
@@ -151,24 +160,36 @@ Per-column header controls, each a popover trigger with `data-active` / `data-op
151
160
  never for data fetching. A visible column can still *look* missing when `shrinkColumns` freezes
152
161
  it to a tiny width because its measured header+body content is minimal (see
153
162
  [primary-table-templates](primary-table-templates.md)).
154
- - **Filter presentational components must seed their internal mode/value state from props.**
155
- The filter mode/operator survives the URL round-trip it is persisted as a URL key suffix
156
- (`_starts`/`_ends`/`_eq`/`_excludes` for text; `_min`/`_max`/`_eq`/`_between` for range),
157
- rehydrated into TanStack `columnFilters` by toga25-supply `useServerTableUrlState`, and
158
- carried through `PrimaryTableHeaderCell`. But `HeaderFilterSearch`/`HeaderFilterRange` are
159
- remounted fresh each time a filter popover opens, so if they don't seed local state from
160
- their props (`filterMode` / `filterParams`) they hard-default (`includes` for text,
161
- `exactly` for range) and reopening a filter *shows the wrong mode* even though the applied
162
- filter is correct. Fixed via a `useEffect` seeding `filterMode` from `filterModeProp` in
163
- `HeaderFilterSearch`, and a rehydration effect keyed on `JSON.stringify(filterParams)` in
164
- `HeaderFilterRange` that reconstructs mode + value(s) from the operator keys
165
- (`eq`→Exactly, `min`→MoreThan, `max`→LessThan, `between`→range min/max).
163
+ - **Filter presentational components must seed their internal state from props.**
164
+ Filter state survives the URL round-trip and is rehydrated into TanStack `columnFilters` by
165
+ toga25-supply `useServerTableUrlState`, carried through `PrimaryTableHeaderCell`. But
166
+ `HeaderFilterSearch`/`HeaderFilterRange` are remounted fresh each time a popover opens, so
167
+ they must seed local state from props or reopening *shows the wrong state*.
168
+ **Text (`HeaderFilterSearch`)** now persists as a single readable URL param
169
+ `<slug>_<col>_filter` = comma-separated `mode:value` pairs (values URL-encoded so `:`/`,`
170
+ inside a term can't corrupt the delimiters); `getDataTableData` parses it and pushes **one
171
+ AND WHERE condition per chip** (mode→op: includes→contains, startsWith→starts,
172
+ endsWith→ends, exactly→eq, excludes→excludes). The component rehydrates the full `tags`
173
+ array from props. Old base64/legacy single-mode `_starts`/`_ends`/… URLs don't parse under
174
+ the new scheme (graceful just re-apply the filter). The browser may percent-encode `:`/`,`
175
+ in the address bar but it round-trips.
176
+ **Range (`HeaderFilterRange`)** still persists as key suffixes (`_min`/`_max`/`_eq`/
177
+ `_between`); it reads mode + value(s) back from `filterParams` via a rehydration effect keyed
178
+ on `JSON.stringify(filterParams)` (`eq`→Exactly, `min`→MoreThan, `max`→LessThan,
179
+ `between`→range min/max).
166
180
  - **Table/surface meta is cached with `Infinity` staleTime/gcTime** (`useFetchTablePageMeta`), so
167
181
  after a DB metadata change (`TableViewFields`, `Core.RecordFields`) a **hard reload** is required
168
182
  to see it. And the app consumes toga-blox's built **`dist/`**, not `src/` — editing blox source
169
183
  requires `npm run build` before it takes effect at runtime.
170
184
 
171
185
  ## Change history
186
+ - 2026-08-17 — Built multi-chip text column filter: each `HeaderFilterSearch` search is now its
187
+ own `TextFilterTag {value, mode}`, the mode dropdown sets the mode for the next chip, existing
188
+ chips keep their mode, and all chips AND together. Persisted as a single readable URL param
189
+ `<slug>_<col>_filter` = `mode:value,...` (URL-encoded values); `getDataTableData` emits one AND
190
+ WHERE per chip. Also fixed: header labels were being ellipsis-truncated (and overlapping the
191
+ next column) — `truncateCells` is body-only, so `.truncateCells .headerCellLabel` is now just
192
+ `white-space: nowrap`. (apeterson)
172
193
  - 2026-08-17 — Fixed: column filter **mode** was not restored when a filter popover was reopened
173
194
  (text reverted to `includes`, range to `exactly`). Root cause: the URL round-trip and
174
195
  `PrimaryTableHeaderCell` preserved the mode, but `HeaderFilterSearch` (destructured
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.595",
3
+ "version": "1.0.596",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",