# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.11.0] - 2026-09-17

> Minor. A standards pass over `GRIDTOOLBAR` against doc 00 R10 ("style through
> the theme, not through invention"), doc 09 §8.11.1 (one focus treatment) and
> the listing-page specification's accessibility rules. No prop, event or schema
> change. **Three things change in the DOM** — read "Changed" before adopting.

### Fixed — keyboard focus was invisible on every button in the package

`.qo-btn.tmpl-button` and `.qo-btn.tmpl-button-icon` carried
`box-shadow: none !important` to strip Kendo's grey `:focus` halo (which fires
on a mouse click as well as on a keyboard landing). That `!important` also
outranked the theme's own `.qo-btn:focus-visible { box-shadow: $focus-ring }`,
so no button in the toolbar, its popups or its dialogs showed keyboard focus at
all — against both doc 09 §8.11.1 and the specification's "do not remove
keyboard focus indication". Every one of Kendo's button focus rules is (0,2,0)
and this package loads after Kendo, so `box-shadow: none` at the same
specificity is enough; the `!important` is gone, and the ring is restated on
`:focus-visible` at (0,3,0) through one `tmpl-button-focus` mixin. The
segmented control in Create New View drew a 2px outline of its own on
`:focus-visible`; it takes `$focus-ring` now, like everything else wearing
`qo-`.

The ring itself carries `!important`, and that is Kendo's doing: every
`fillMode="clear"` button — the dialog close icons, the row actions, the
criteria tools, the filter row's add and remove — gets
`.k-button-clear { box-shadow: none !important }` from Kendo, and only another
`!important` reaches past it (doc 09 §6, §9). Measured in the harness before
this: a focused close icon matched `:focus-visible` and drew nothing.

### Fixed — icon-only buttons had no accessible name

Every icon-only button in the toolbar and its popups now carries an
`aria-label` — the four tools (Quick Filter, Reset, Setting, Export), the
search field's magnifier and clear, the three dialog close icons, the filter
row's add and remove, the criteria reset and copy, the saved-view row actions,
the column chooser's remove, and the export history's download / failed /
in-process / delete. `title` was the only name they had, and the ICON widget's
tooltip wrapper takes it, so the accessibility tree showed a `button` with no
name next to a `generic "Quick Filter"`. The listing-page specification
requires `aria-label` on a button that contains only an icon. The column
chooser's invisible placeholder (the remove slot on a primary row) is
`aria-hidden` and out of the tab order.

**Adopt with `procode-lowcode-kendo-widget` 2.7.1.** The prop only reaches the
DOM once the ICON widget forwards it (2.7.1, where it also defaults to
`title`); on 2.7.0 it is dropped and the names stay missing, with no error.

### Changed — two clickable elements are buttons, and one caption is a label

- **Advanced search's Cancel is a `<button>`.** It was a `<span>` with
  `cursor: pointer` and a colour: clickable, but not focusable, not in the tab
  order, and the one action in that footer a keyboard could not reach. It is a
  BUTTON widget carrying `qo-btn qo-btn-link-danger tmpl-button` — ink, hover
  and the focus ring from the theme's link variant, the box from `tmpl-button`.
  `tmpl-cancel-action` stays on it as a hook and **styles nothing**; the
  `.tmpl-action-container div .tmpl-cancel-action` rule is gone. A UI test
  that located Cancel as a `span` has to look for a `button`.
- **Export format chips are `<button type="button">`s**, for the same reason —
  they were `<div onClick>`s. `.tmpl-export-option-card` keeps its name.
- **The dialog captions are the theme's form label.** "View Name", "Start
  With", "Copy From" and "Group By" carry `qo-label` (12px / 600 /
  `$text-darker`, the class every schema field's label wears in the consuming
  app) and the required ones `qo-label-required`, whose `::after` draws the
  marker in `$text-danger`. They were bare spans with a type restated locally
  (12px / 500 / `$text-dark`) beside a `<span style="color: red">*</span>`. The
  weight steps 500 → 600 and the ink one shade darker; nothing moves.
- **The export popup's two captions are the same label.** The scope caption
  ("Select Export Format") and "Previous Exports" carry `qo-label`; they ran
  11px and 13px of their own type, both `$text-secondary`. Their rules keep
  layout only (the section gap, the full-bleed rule above the history).
- **The quick filter's row labels are the same label.** Each row's caption
  carries `qo-label`; it ran 13px / 400 / `$text-secondary`. The rule keeps
  its layout — block, the `$space-xs` gap to the picker, and the leading trim,
  now derived from `$line-height-sm` (the step paired with the label's size)
  because `qo-label` leaves line-height at `normal`.

### Changed — every colour a token, every measure a rule

- **Export format chips are flat.** They were a bevel — `lighten()` /
  `darken()` of `$background-light` at rest, two alphas of
  `$background-primary` on hover, an inset highlight over both and a pressed
  inset under `:active` — five fills the theme has no name for, on the one
  element in the stack drawn in relief. At rest `$background-default` inside
  `$border-default`; on hover `$background-primary-soft` under a
  `$border-primary` edge with `$text-primary` ink — the same tokens the
  segmented control in Create New View uses for the same kind of choice. The
  `&--selected` rule went with it: nothing in the package ever emitted the
  class.
- **The advanced-search row error tint is `$background-danger-soft`**, the
  theme's danger tint (2.3.0), not `rgba($background-danger, 0.06)` mixed by
  hand from the solid.
- **The global search field's type is a rule, not an inline style.**
  `GlobalSearchView` set `inline: { fontSize: "12px" }` beside a `qo-font-md`
  (13px) class that Kendo's `.k-input` out-ordered — the class said one size
  and the inline style drew another, and neither could be restyled from a
  consumer stylesheet (doc 08 §8.5 listed it as such). Both are gone;
  `.tmpl-grid-toolbar-container .tmpl-global-search` states `$font-size-sm`,
  the size every other control in the row already renders at. Same rendered
  size; now one `className` override away.
- **The Between editor's layout is a rule.** `.tmpl-advanced-ranged-input`
  carried `display: flex; align-items: center; gap: 4px` inline on the wrapper
  and `flex: 1 1 0; min-width: 0` inline on each of its two widget roots. The
  SCSS states all of it (`gap: $space-xs`, `> * { flex: 1 1 0; min-width: 0 }`).
- **Two hidden placeholders use `qo-invisible`** — the theme's
  `visibility: hidden` — instead of an inline style: the sole filter row's
  remove button, and the column chooser's remove slot on a primary row.
- **`.tmpl-screen-view-actions` loses an `!important`** on its `gap`. Nothing
  contested it.
- **`$export-chip-height` is derived, not measured.** It was a 28px literal;
  the chip is a `<button>` on the package's button box now (`line-height:
  $btn-line`, `$btn-inset-y` above and below, the hairline) and the constant
  is that sum, 26px — so the scope list's three-row cap follows the box.
- **The create-view validation message loses its inline colour.**
  `style={{ color: "var(--qo-text-danger, red)" }}` sat on an element already
  carrying `.tmpl-error-message`, which the SCSS colours with `$text-danger`.

### Not changed, and why

`box-shadow: none !important` stays on `.tmpl-dropdown` and `.tmpl-input`: those
are Kendo fields, whose focus is a `$border-primary` edge from the kendo-widget
bridge (doc 09 §8.11.1), so no ring is lost, and the `!important` is the
documented tier for beating Kendo's own field rules (doc 09 §6, §9).
`.tmpl-filter-applied` and the error-row `border-color` keep theirs for the
same reason. The radial-gradient dot radii on the drag grip (1.4px / 1.5px)
are glyph geometry, not spacing, and stay.

## [2.10.0] - 2026-09-16

> Minor. The four titled popups — **Column Chooser**, **Select New Group(s)**,
> **Create New View** and its **Edit View** mode — now head themselves with an
> `<h3 class="qo-h3">` and state no type of their own. They were `<h2>`s with
> the type hand-set beside them in three separate SCSS blocks.
>
> **This changes the rendered element.** A selector, test step or override
> written against `.tmpl-chooser-header h2`, `.tmpl-group-filter-popup-header h2`
> or `.tmpl-screen-creater-popup-header h2` matches nothing after this release.

### Changed — the dialog title is a heading, and the theme owns its type

Each of the three headers carried its own copy of the same four declarations:

```scss
h2 {
  font-size: $font-size-xl;        // 18px
  line-height: $line-height-xl;
  font-weight: $font-weight-semibold;
  color: $text-darker;
  letter-spacing: $tracking-title; // -0.2px
}
```

at specificity (0,2,1) — the same shape of defect the button box had before
2.9.0: the package restating a ramp it does not own, in three places, so a theme
that moved its heading scale would leave these three behind. `procode-vs-theme`
2.6.0 gives headings a real scale (`qo-h1` … `qo-h6`, carrying size, leading and
weight together), which is the thing to name instead of copying.

**The element changes with the class, and that is the point.** A heading has an
outline level and an appearance, and they are two decisions — the same pair the
`H1` … `H6` widgets keep apart in `procode-kendo-widget` 2.7.0. These dialogs
open over a page that already has its own `<h1>`; their titles are sections of
it, so `<h3>` is the level, and `qo-h3` is the look.

**Ink is the one declaration left in each header.** The theme's heading classes
set no colour, and these popups are portalled out of `.qo-page`, so without it
the titles fall back to the document default — measured black `rgb(0,0,0)`
against the intended `#111827`.

Measured in a consuming app, all four popups (1536×800):

| | 2.9.0 | 2.10.0 |
|---|---|---|
| element | `<h2>` | `<h3 class="qo-h3">` |
| type | 18px / 22.5 / 600, tracking -0.2px | **16px / 20.8 / 600**, no tracking |
| ink | `#111827` | `#111827` (unchanged) |
| header height | ~58px | **52.4px** |

The shared dialog rhythm is untouched — the headers still take
`padding: $space-lg $space-lg $space-md` and are still floored by the 26px close
button, so the ~5.6px they lose is the title's own line box and nothing else.

### Changed — the View Name field reads as a field, not a form

The one free-text box in the package sized itself off Kendo: `.k-input-inner`
is `0.875rem` (14px), a step above everything else in the dialog and two above
the toolbar fields behind it, so the value the user typed was the largest text
on screen after the title. It now takes **11px** (`$font-size-xs`), and its
caption stays a step above it at 12px (`$font-size-sm`, unchanged) — the
pairing this dialog is drawn at.

| | 2.9.0 | 2.10.0 |
|---|---|---|
| `View Name` caption | 12px / 500 | 12px / 500 *(unchanged)* |
| the value inside the box | **14px** / 400 | **11px** / 400 |

Scoped to `.tmpl-screen-creater-popup .tmpl-dropdown-container .tmpl-input
input`, so it is this dialog’s field and not every consumer of `.tmpl-input`.
The package’s other inputs — global search, the pickers — are 12px and are
untouched.

### Removed — three dead rules and one dead variable

- The `p { … }` block in the Create New View and Select New Group(s) headers.
  It styled the subtitle those headers lost in 2.8.3 and has matched nothing
  since.
- The wrapping `<div>` around the title in both of those headers — it existed to
  group the title with that subtitle. Each header is now one flex child a side.
- `$tracking-title` (-0.2px). It tightened an 18px line and had exactly one
  consumer, the three blocks above. `$tracking-caps` is untouched.

### Classes

| Removed | Added |
|---|---|
| — | — |

No class is added or removed. `tmpl-chooser-header`,
`tmpl-group-filter-popup-header` and `tmpl-screen-creater-popup-header` all keep
their names and their layout; only the element they title with, and the type on
it, have changed.


## [2.9.0] - 2026-09-16

> Minor. The advanced-search **criteria expression** is now parsed and checked
> before it is accepted, and the box it is written in has been redesigned around
> that. An empty expression — the reported defect — was previously saved and
> sent as `{"pattern":"","filters":[…]}`, which the API answers `200` with the
> filter silently discarded while the toolbar keeps showing its applied badge.
> Free text was sent verbatim and came back `500`. Neither is reachable now.
>
> One internal helper is removed (`normalizeLogicOperators`) and one prop is
> renamed (`Filter`'s `hasError` → `errorMessage`); both are internal to the
> package. Two SCSS classes are removed and six are added — see the class table
> at the end.

### Fixed — an empty or malformed criteria expression could be applied

The criteria box was the only free-text control in advanced search and nothing
read it back. `onCriteriaApplied` took whatever the box held, ran a single regex
over the gaps between `)` and `(`, and wrote the result to
`advancedSearch.criteria`; Apply then copied that into `appliedQuery.criteria`.
There was no check of any kind — not that the expression was non-empty, not that
it parsed, not that it named the rows that exist.

Confirmed end to end against a live grid before and after the change:

| What is in the box | 2.8.3 | 2.9.0 |
|---|---|---|
| *(empty)* | saved; request sent with `"pattern":""`; API returns `200` with **every** row, filter ignored, toolbar badge still lit | `Enter a criteria expression — for example (1).` Apply refuses, popup stays open, no request |
| *(spaces only)* | as above | as above |
| `hello (9) world` | saved and sent verbatim → **HTTP 500** | `"hello" is not part of a criteria expression. Use row tokens like (1) joined by AND or OR.` |
| `(9)` with 1 row | saved; pattern names a row that is not in `filters` | `(9) is not a filter row — this filter has one row, (1).` |
| `(1)` with 3 rows | saved; rows 2 and 3 are sent in `filters` with nothing in the pattern applying them | `(2), (3) are missing from the criteria — every filter row must be used, or removed.` |
| `(1) AND (1)` | saved; the label→id substitution rewrites only the first `(1)`, so the pattern reaches the API still containing a label | `(1) is used more than once — name each filter row exactly once.` |
| `((1)` | saved | `Unclosed "(" in the criteria expression.` |
| `(1) AND` | saved | `The criteria expression is incomplete — it ends where a row was expected.` |
| `1` | saved | `Put brackets around row numbers — write (1), not 1.` |
| `(1) &&` | saved | `"&&" is not part of a criteria expression. …` |
| `(1) ANDD (2)` | silently became `(1) AND (2)` — defect 45 | `"ANDD" is not part of a criteria expression. …` |

The expression is now a small language with a parser rather than a regex
(`helper/AdvancedSearchFunctions.ts`):

```
expression := operand ( ("AND" | "OR") operand )*
operand    := "(" row ")" | "(" expression ")"
row        := an integer naming a filter row, 1-based, as idLabel spells it
```

Two leniencies are kept, and both are now *shown* rather than assumed: case is
free (`and` → `AND`), and two adjacent operands with nothing between them take
an implicit AND (`(1) (2)` → `(1) AND (2)`). The normalized expression is what
gets committed, and the box snaps to it on blur, so the user sees what the
leniency did. Everything else is an error naming the offending token.

`validateCriteriaExpression(input, rowCount)` returns
`{ valid, message, expression, rowOperators }` and is the single rule both the
live hint and Apply consult — the same discipline `operatorNeedsValue` already
imposed on the row check, for the same reason.

### Fixed — the criteria box could show text the model had never seen

The box was a `contentEditable` paragraph React rendered once and then did not
own. The user typed into the DOM; the value came back only via
`document.getElementById("tmpl-criteria-input-" + uiElementGroupId).innerText`,
and only when the ✓ beside it was clicked. Three failures followed from that:

- **The box could lie.** Type into it, then change any row's dropdown: React
  re-renders with an unchanged `criteria` string, so it never rewrites the text
  node, and the box goes on showing text that Apply will not use. Reproduced:
  box reading `JUNK I TYPED` while the model held `hello (9) world`.
- **Two toolbars sharing a `uiElementGroupId` read each other's box** — defect 46.
- Whatever the box held went into `appliedQuery` unchecked, which is the defect
  above.

It is an ordinary controlled `<input>` now. The draft is what the user sees and
what Apply uses; it commits to the model on every keystroke that parses, and
re-seeds from the model whenever a row edit rewrites the expression. Nothing
reads the DOM by id any more, and `onCriteriaChange` / `onApply` take the
expression as an argument.

### Fixed — Apply used the stored expression, not the one on screen

`onAdvancedSearchApply` read `advancedSearch.criteria`, so an edit the user had
not yet confirmed with the ✓ was silently dropped and the previous expression
applied instead. Apply now takes the expression it validated as an argument, and
lands rows, expression and `appliedQuery` in one write.

### Changed — the criteria expression has its own band, with a label and the syntax

It used to be an unlabelled ~150px box wedged between **Clear Filter** and
**Cancel**, with two 20px icons crowded into it — the one control in the panel
that takes free text was the smallest thing in it, said nothing about what it
wanted, and needed a ✓ clicked for its contents to count at all.

It is now its own band between the rows and the footer:

- a **MATCH CRITERIA** label, matching the FILTER / OPERATOR / VALUE captions
  over the rows;
- a full-width monospaced input — the one place in the package that shows an
  expression rather than prose, so `(1) AND (11)` lines its brackets up and a
  missing one is visible rather than merely absent;
- the syntax written underneath — *Join the rows above with AND / OR, and group
  with brackets — ((1) OR (2)) AND (3).* — replaced in place by the reason for a
  rejection, so the panel does not reflow when an expression goes wrong;
- a **Reset** action that rebuilds the expression from the rows, which is the
  way back from a mangled one that does not also throw the rows away (previously
  only **Clear Filter** could recover, and it wiped everything);
- the placeholder is the expression the rows would produce, so an emptied box
  shows what Reset would put back.

The **✓ is gone.** An expression that parses is committed as it is typed and
normalized on blur, the way every other field in the panel behaves; there is no
longer a confirm step to forget. **Copy** stays.

### Changed — a row that blocks Apply says which cell is missing

Apply flagged incomplete rows with a pink tint and nothing else, so a user was
told that something was wrong but not what. Each flagged row now carries one
sentence under its cells — *Choose a filter for this row.* / *Choose an operator
for this row.* / *Enter a value for this row.* — cleared as soon as that cell is
touched. Rows and expression are reported together on one click rather than one
after the other.

### Changed — AND/OR chips are derived from the expression, not from operator order

`onCriteriaApplied` paired the Nth `AND`/`OR` in the text with the Nth row.
That stops being true the moment an expression is grouped: in
`((1) OR (2)) AND (3)` the second operator belongs after row 2, not after row 1.
Each row's chip is now the first logic token that follows *its own* reference in
the parsed token stream, at whatever bracket depth that lands.

### Changed — one function builds the expression from the rows

Add, delete and the AND/OR toggle each rebuilt `criteria` inline, three times,
with three slightly different loops — one of which left a trailing separator
that only a `.trim()` hid. All three call `buildCriteriaFromRows` now, which is
also what **Reset** restores to.

### Removed — `normalizeLogicOperators`

The whole of the old input handling: one regex over the gaps between `)` and
`(` that uppercased `and`/`or` and rewrote **anything else** — including a typo,
including a word — to `AND`, with no error. Internal to the package; nothing
outside it imported the helper. `validateCriteriaExpression` replaces it.

### Behaviour changes that can break an existing screen

- **Every control in the package is 14px shorter.** Buttons stopped stating a
  height and now measure their own content; the toolbar row, both alert footers
  and every dialog footer go from 40px to 26px, and the non-button controls
  sharing those rows follow. A screen that reserved a fixed height for the
  toolbar, or an app rule positioned against a 40px control, needs re-checking.
  Two buttons move the other way: the filter row's add/remove (16px) and the
  saved-view row actions (20px) were below the 24px pointer-target floor and are
  now 26px.
- **`tmpl-button-sm` is gone**, replaced by `tmpl-button-icon`, and no
  vs-template button carries `qo-btn-md` / `-sm` / `-xs` / `-icon` any more. A
  selector or style override built on either has to move.
- **A saved view holding a malformed expression now shows an error on open** and
  Apply refuses until it is fixed or Reset. The filter it describes was already
  not being applied — the grid returned unfiltered rows, or the request 500'd —
  so this surfaces an existing failure rather than creating one. Reset rebuilds
  a working expression from the rows in one click.
- **An expression that does not name every row is rejected.** Rows the pattern
  omits were being sent in `filters` and ignored; delete the row or reference it.
- **Nothing is applied from an invalid expression**, so a screen that depended on
  an empty pattern returning every row will now not issue the request at all.
- The ✓ beside the criteria box no longer exists; anything targeting it in a test
  should target the input itself.

### Changed — every button in the package measures its own content

No button in vs-template states a height any more. The box is
`line-height + padding-top + padding-bottom + 2 × border`, declared once:

```scss
$btn-line: $size-sm;        // 16px — the one line box a button centres
$btn-inset-y: $space-xs;    // 4px
$btn-inset-x: $space-md;    // 12px
// 16 + 8 + 2 = 26px
$control-compact: $btn-line + (2 * $btn-inset-y) + (2 * $border-width-sm);
```

Two boxes come out of it, and they are deliberately different:

| | Height | What takes it |
|---|---|---|
| the button box | **26px**, never declared | every button in the package — popup actions, dialog footers, alert actions, the filter row's add/remove, the saved-view row actions, the criteria box's reset and copy |
| `$field-height` | **32px** | every value-setting control — text, number, date, date-range, dropdown, multiselect, the global search field, the saved-view picker, the column-chooser search |

**The toolbar row states its height, and is the only place in the package that
does.** Its eight controls — global search, Advanced Search, Quick Filter, Group
By, Reset, Settings, Export, the saved-view picker — are a single band and have
to agree to the pixel, and a derived height cannot deliver that: the arithmetic
starts from a font's line box. Measured on the real row, `line-height + padding +
border` gave **31.6px** for five of the tools and **33.6px** for two more,
against the search field's declared 32.

```scss
.tmpl-grid-toolbar-container > * { height: $field-height; }   // every tool, wrapper included
.qo-btn.tmpl-button-field       { height: $field-height; }    // and the button inside it
```

`box-sizing: border-box` is global, so a declared height **is** the measured
height — padding and border sit inside it, and the number in the SCSS is the
number in the inspector. It is said twice because each tool renders its own
wrapper, and what the eye reads as "the control" is that wrapper for two of the
eight and the button itself for the rest; stated on one alone, two tools stayed
1.6px taller than the other six.

Everything in that row goes **40px → 32px**; buttons elsewhere in the package go
**40px → 26px** and still state no height. That remains the pattern — nothing
outside this row has to line up to the pixel with a Kendo control.

### Removed — vs-template buttons no longer carry a theme size class

`qo-btn-md` / `-sm` / `-xs` / `-icon` each set `min-height`, side padding and
font-size at specificity (0,2,0), and `.tmpl-button` was (0,1,0) — it lost all
three. That was invisible only because the two happened to agree on 40px: the
package was reading its height off the theme's ramp while appearing to state it,
which is how theme 2.4.0 moving the ramp inflated the column-chooser rows from
~40px to ~61px and stretched the saved-view row icons from 20px to 40px.

Every call site now carries `tmpl-button` or `tmpl-button-icon` and no size
class, so `.qo-btn.tmpl-*` (0,2,0) beats the `.qo-btn` base (0,1,0) cleanly and
one rule decides the box. **The variant classes stay** — `qo-btn-primary`,
`qo-btn-outline-danger` and `qo-text-*` are ink, not size, and `qo-btn` itself
is load-bearing because the theme scopes its button ink as `.qo-btn.qo-text-*`
to beat Kendo's `.k-button-*`.

### Fixed — the toolbar row sized its own controls

`.tmpl-grid-toolbar-container` had no `align-items`, so it defaulted to
`stretch`: every tool slot grew to the row height and passed it down as
`height: 100%` on the button inside it, over a slot pinned to `flex: 0 0
$control-height`. A button that measures its own content would have been
overruled by its container, twice. The row is `align-items: center` now, each
slot is `flex: 0 0 auto`, and the three `height: 100%; width: 100%` rules on the
tool buttons are gone.

### Fixed — the date and number filter editors were never sized

`.tmpl-advanced-input` is the class the widget puts on its **outer** element, and
for every filter type that is a Kendo wrapper — `.k-input` for TEXT and NUMBER,
`.k-datepicker` for DATE, `.k-picker` for LIST — holding a borderless `<input>`.
A Kendo field draws its box on that wrapper, and the rule only ever sized the
input, so the DATE and NUMBER cells rendered at Kendo's own height while the
TEXT and LIST cells beside them in the same filter row took the row measure.

The height is on the wrapper now and the inner input fills it. Two editors are
excluded because they have to grow — the radio group wraps its options onto a
second line and the multiselect wraps its chips — and take the measure as a
floor instead.

### Changed — one row box for every popup, shared with the dropdown popups

Every selectable row in the package's popups is now the same box, taken from the
Settings menu, which was the one that was already right:

```scss
padding: $space-sm $space-md;   // 8 / 12
font-size: $font-size-md;       // 13px
line-height: $line-height-md;   // 1.33   →  ~33px, and no height is stated
```

All four are `procode-vs-theme` tokens, and `procode-kendo-widget` 2.7.0 states
the same four for `.prc-dropdown-list-item` — so this package's rows and every
`DROPDOWN` / `MULTISELECT` popup in the app measure the same without either
package holding a copy of the other's numbers.

**The saved-view row paid its inset twice.** `UserViewView`'s `itemRender`
clones Kendo's `<li>` with `prc-dropdown-list-item`, so the row box is already
paid there; `.tmpl-screen-view-item` added `padding: $space-1 $space-sm` on top
of it under a flat `height` that fought both, and the row measured **~48px**
against the ~33px every other row draws. It carries layout only now — flex, gap,
alignment — and nothing about its size.

Two inert floors went with it: `.prc-label-field` and
`.tmpl-chooser-setting-container` each declared `min-height: $field-height`
(32px) **below** the ~33px box they were floor to, so neither had ever applied.

### Fixed — the advanced-search multiselect was clipped at a flat height

The value cell for `In The List` / `Not In The List` is a MULTISELECT, and its
classes arrive through `rootStyle` — so they sit on the widget's **wrapper**,
not on the Kendo element. `.tmpl-dropdown` gave that wrapper a flat
`height: $field-height`, which clipped the chips as soon as they wrapped to a
second line.

It carries `tmpl-advanced-multiselect` now and takes a floor instead — the same
treatment the group-by dialog's multiselect already had. It needs a class of its
own because the exclusion cannot be written as `:not(.k-multiselect)`: that
class is on the element *inside* the wrapper.

Pairs with `procode-lowcode-kendo-widget` 2.7.0, which gives `.k-multiselect`
itself the control box every dropdown already had.

### Changed — the quick filter opens a gap between a label and its field

There was none: the label sat directly on the picker it names, so the pair read
as one block of text rather than as a caption and a control. `$space-xs` — the
smallest step, and clearly smaller than the `$space-md` that separates one row
from the next, or the label starts to look like it belongs to the field above.

The label's leading trim is now on **both** edges. It only pulled back the half
-leading above the glyphs, which was enough while every gap opened above a
label; a gap that closes below one needed the same treatment, or a declared 4px
rendered as ~7px.

### Added — rename a saved view, through the view dialog in edit mode

An **Edit** action on every saved-view row, beside Set Default and Delete. It
opens **the same dialog Create New View uses**, in edit mode — one window for
both things a view's name is set in, rather than a second editor that would have
to be kept in step with the first.

What edit mode changes, and nothing else:

| | Create | Edit |
|---|---|---|
| Title | Create New View | **Edit View** |
| Name field | empty | **prefilled**, focused |
| *Start With* + *Copy From* | shown | **hidden** |
| "Will be saved" preview | shown | **hidden** |
| Action | Create | **Save** |

**The source section is hidden on purpose.** "Current setup / Default setup /
Copy existing" decides where a *brand-new* view's configuration comes from; a
rename touches the name and nothing else, and offering the choice would suggest
renaming could also overwrite the view's saved state. In edit mode the dialog is
the name field and nothing more.

`renameScreenAction` is the same `PATCH /api/metadata` the Save button uses, with
`viewName` in place of `screenViewMetaData` — so a view's configuration cannot be
disturbed by renaming it. On success the name is patched into `allViews` (and
`selectedView`, when it is the one renamed) rather than re-fetched: reloading
would close the popup the user is still working in.

**System Default cannot be renamed** — it is derived rather than stored, so there
is no record to rename. Same guard Delete has always had. Renaming a view to the
name it already has closes silently; an error there would be inventing a problem.

**A duplicate name is an expected failure, not an exceptional one.** The server
owns uniqueness and answers 422; the action returns that as a message instead of
throwing, and the dialog stays open with the offending name still in the field so
it can be edited rather than retyped. Same message and same cause
`createScreenAction` already handled — the other way a duplicate is produced.

### Changed — one copy of the view-name rule

`validateViewName` and `VIEW_NAME_MAX_LENGTH` (25) moved to
`helpers/validateViewName.ts`. They were module-local to `CreateScreenPopup`, and
edit mode is the second path through the same field. Sharing the dialog means
sharing the rule by construction, but the helper makes that true even if the two
paths ever separate again.

### Fixed — a long view name pushed the row's actions off the edge

`.tmpl-screen-view-name` is `flex: 1` with `white-space: nowrap` and an
ellipsis, but it had no `min-width: 0`. A flex item defaults to
`min-width: auto`, which floors it at its **min-content** width — and for nowrap
text that is the whole string. So the name refused to shrink, the ellipsis never
engaged, and the row's actions were pushed past the popup's right edge instead
of sitting against it. The ellipsis was there all along; it could not fire
without that one line.

The row also states `justify-content: flex-end` now, so the actions sit right
even on a row whose name is short, empty, or ever stops being the flexing item.

### Changed — a row's two gaps are two measures

The actions are wrapped in `.tmpl-screen-view-actions`, which is what lets the
space between the name and the group differ from the space between the buttons:

| | Measure | Why |
|---|---|---|
| name ↔ actions | `$space-xs` (4px) | the same gap `.tmpl-grid-toolbar-container` uses between its own tools — these are different kinds of thing and want air between them |
| button ↔ button | `$optical-inset` (2px) | the four are one control group, each with its own hover surface, and should read as a unit |

The row was a single `$space-sm` (8px) gap doing both jobs, which read as a
loose group against a dense list. 2px is below the 4px ladder's smallest step on
purpose: `$optical-inset` is this file's sub-grid measure, and every use of it —
an icon's breathing room inside its own square, the unsaved dot's overhang — is
optical rather than structural. This is the same job.

### Fixed — the saved-view list grew under the pointer

The row's actions were hidden with `display: none`, so they took no space: a row
was as tall as its text line (~17px) at rest and as tall as a 26px button the
moment the pointer entered it. The list grew, and every row below the one being
pointed at moved out from under the cursor.

They are `visibility: hidden` now — reserved at rest, revealed on hover — so the
row measures the same either way. A negative block margin folds the 26px target
back into the row's own vertical inset, which is dead space, so the saved-view
row keeps the shared popup-row height instead of standing a step taller than
every other list in the stack for the sake of buttons that are only sometimes
visible. The hit area is still the full 26px; it overlaps padding rather than
adding to it.

### Fixed — the saved-view picker lost its bottom border

`.tmpl-screen-view-container` is the only tool in the toolbar row that draws a
border on its **outer** element: the picker inside is `fillMode="flat"` and
draws no box of its own, so that border *is* the control's visible edge.

`.tmpl-grid-toolbar-container > *` sizes that outer element, and `box-sizing:
border-box` puts the two hairlines inside the 32px — leaving a 30px content box.
The picker restated `height: $field-height` (32px), overhung by exactly 2px, and
painted over the bottom border. It read as the border having gone missing rather
than as an overflow, which is why it was worth a note.

The wrapper and the picker fill the container's content box now (`height: 100%`)
instead of restating the row height. The outer measurement is still 32px; the
border is whole again.

> **The general shape, for the next control added to this row:** the row states
> the height on the outer element, so anything *inside* that draws its own
> border must fill rather than restate. Only this one tool has an outer border
> today — every other slot is borderless and its button's 32px fits the 32px
> content box exactly.

### Fixed — icon buttons were 16px wide and 20px tall

Kendo derives an icon button's glyph box from its own custom properties —
`.k-button-md.k-icon-button .k-button-icon { min-width: calc(var(--kendo-font-size)
* var(--kendo-line-height)) }`, which resolves to 0.875rem × 1.4285714286 =
**20px**, at specificity (0,3,0). The package pinned `min-width` but not
`min-height`, so Kendo's floor won on height alone and every icon button drew
**32×36 in a 32px toolbar row** instead of 32×32.

All four box properties are set now, and so are the two Kendo derives from
(`--kendo-font-size: $btn-line; --kendo-line-height: 1`) — so its own calc lands
on the same number and the result no longer depends on which stylesheet the
consuming app imports last. Every icon button in the package is square: 32×32 in
the toolbar row, 26×26 elsewhere.

### Fixed — the global search was the one control in the row with no height

It never stated one; it took its height from the row, which stretched every item
to the tallest. Now that the row centres its items and each one arrives at
`$field-height` under its own arithmetic, a control that says nothing falls back
to Kendo's rem-derived height — leaving the search box the only thing in the
band at the wrong size. It takes `$field-height` explicitly.

### Changed — the alert action floor drops 110px → 80px

`$alert-action-min-width` was chosen against a 40px-tall action; under the 26px
box it drew a 4.2:1 bar. 80px puts the proportion back where it was. It is still
a floor, and still the only measure either alert action states.

### Classes

| Removed | Added |
|---|---|
| `tmpl-criteria-input-container` | `tmpl-criteria-editor` |
| `tmpl-criteria-input-icon` | `tmpl-criteria-editor-head` |
| `tmpl-button-sm` | `tmpl-criteria-editor-tools` |
| | `tmpl-criteria-label` |
| | `tmpl-criteria-hint` (+ `tmpl-criteria-hint-error`) |
| | `tmpl-filter-error-message` |
| | `tmpl-button-icon` |

`tmpl-criteria-input` is kept and now names an `<input>` rather than a
`contentEditable` `<p>`; it gains `tmpl-criteria-input-error`. `tmpl-button` is
kept but is now the package's one text-button box rather than "the toolbar
button", and is only styled through `.qo-btn.tmpl-button`.
`tmpl-button-sm` is replaced by `tmpl-button-icon`, which is a different size
(26px, not 16px) as well as a different name.


## [2.8.3] - 2026-09-15

> Patch. Corrections to the saved-view UI — the **Create View** button's size,
> the **Create New View** footer, and the **Copy From** picker — plus a pass that
> makes **Select a new group** and **Create New View** the same dialog. No schema
> or prop is added or removed; one class is added (`tmpl-view-copy-field`).

### Changed — the `+ Create View` button takes the theme's compact size

It carried `qo-btn-md`, so it drew at `$control-height` (40px) — the size the
toolbar row uses for the controls a screen is *operated* with. This one sits in
the saved-view dropdown's footer, under a list of 40px rows, where a full-height
primary button read as a second toolbar rather than the list's one action. It is
`qo-btn-sm` now: `$control-height-sm` (32px), `$space-3` of side padding and
`$font-size-sm`, all from the same ramp (`UserViewView.tsx:70`). Nothing else in
the footer moves.

### Removed — the Create New View dialog's Cancel button

The footer had **Cancel** and **Create**; the dialog already closes three other
ways, and two of them are within a few pixels of Cancel itself: the header's
close icon, and a click anywhere outside the dialog. A second neutral button
next to the one action the dialog exists for is weight, not safety — there is
nothing to lose by dismissing this dialog, since it writes nothing until Create.

The footer is now Create alone, still right-aligned by the same
`justify-content: flex-end`. `onViewToggle` is untouched and still wired to the
header icon and the outside-click handler, so every dismissal path that worked
before still works.

### Changed — the Copy From picker matches every other dropdown

`Copy existing`'s source picker passed `fillMode="flat"`, the only control in
this package's dialogs to do so. Kendo's flat fill draws no box — just a bottom
rule — so a required field sat directly under a fully bordered **View Name**
text box and the pair read as two different kinds of control. It now renders the
same bordered `.k-picker-solid` as the advanced-search, quick-filter and group-by
pickers (`CreateScreenPopup.tsx:336-355`); `.tmpl-dropdown` already sized it to
the same `$control-height` and `$border-radius-md` as `.tmpl-input`, so the two
fields now agree on all four edges.

It is also **capped at `$dialog-field-max-width` (260px)** rather than running the
full content column. It holds a view name, and a view name is capped at 25
characters at the dialog's own font size — so the column is roughly twice what
the widest value can use, and a picker that wide reads as a field expecting a
sentence rather than one of a handful of names. A cap, not a width: the field is
still `width: 100%` beneath it, so on a dialog narrowed by
`$dialog-viewport-max-width` it shrinks with everything else. The row carries its
own `tmpl-view-copy-field` class for this — `tmpl-dropdown-container` is the
shared row wrapper, and a `:nth-child` would move the day a field is added above
it.

> With `procode-lowcode-kendo-widget` 2.6.0 the picker and the text box above it
> also focus identically — a `$border-primary` edge, no halo. On older
> kendo-widget the picker focuses blue (this package's own dropdown rule) and the
> text box goes grey; nothing breaks, the two just still disagree.

### Changed — Select a new group and Create New View are one dialog

The two are the same object: a title, one field, one primary action. They
already shared the dialog rhythm block — the same header padding, the same
hairline, the same footer inset, the same `$shadow-xl` and `$border-radius-lg`,
the same `92vw`/`90vh` cap — but three things below that still disagreed, and
each one was visible.

**One width.** They ran `550px` and `480px`. Neither measure could state a
reason for its number, and the wider one was the worse off: it drew a 550px
window around a control that took 40% of it. Both are `$field-dialog-width`
(480px) now — a third name, declared once, that `$group-filter-dialog-width` and
`$view-creator-dialog-width` both resolve to, so each dialog still says which
measure it is asking for and the two cannot drift apart again.

**The header is a title and nothing else.** *Select a new group* carried a
subtitle — "This option would allow you to select from existing set of pre-built
groups" — restating the title in a longer sentence, under a field labelled
*Group By* holding a list of groups. It is gone, the header centres on its title
the way Create New View's does (it was `align-items: flex-start`, for the
paragraph), and the `h2` drops the `$space-xs` it reserved beneath itself.

**Both fields take one measure.** The group picker was `width: 40%` of its
container — the same idea as a cap, stated as a ratio, so it moved whenever the
dialog moved and came out at 220px on the old 550px one. It is
`$dialog-field-max-width` (260px) now, the measure the copy-source picker in
Create New View already used, renamed from `$view-copy-field-width` because two
dialogs share it.

The group picker also stops clipping. It is the one MULTISELECT among these
controls, so past three or four groups its chips wrap to a second line, and
`.tmpl-dropdown`'s flat `height: $control-height` cut them off — a selection the
user had made and could not see. `height: auto` with a `min-height` floor: level
with the 40px controls around it at rest, grows when it has to.

**Both primary actions carry `tmpl-button`.** *Save And Continue* had
`qo-btn qo-btn-primary qo-btn-md` without it, so it kept the Kendo shadow the
class exists to clear and took its height from `qo-btn-md`'s floor rather than
stating it. *Create* already carried it.

**And both close buttons are the theme's icon button.** Create New View was the
only one of the four dialogs in this package using `tmpl-button-sm` for its
close affordance — a hand-rolled `$size-sm` (16px) square — against
`qo-btn qo-btn-icon qo-btn-sm` (32px, straight off the theme's size ramp) in the
column chooser, export and group dialogs. It moves to the ramp, which doubles
its hit target.

## [2.8.2] - 2026-09-15

> Patch. Two spacing/treatment corrections in `GRIDTOOLBAR` and `BLOCK`. No
> schema, prop or class is added or removed.

### Changed — every `BLOCK` draws its vertical rule, not just the ones with a block before them

The rule hung off `.tmpl-block-container + .tmpl-block-container` — the usual
"n-1 separators for n items" pattern, which reads correctly only in a single
unbroken row. These blocks sit in `prc-flx-container prc-wrap`, so as soon as
the row wraps, the first block of the second line is still an adjacent
*sibling* in the DOM while being visually first in its line: it drew a leading
rule against nothing, while the genuinely first block had none. Two blocks in
the same visual position, styled differently, with the difference decided by
DOM order rather than by layout.

CSS cannot see where a flex line breaks, so the choice is a rule on every block
or a rule that is wrong on some of them. It is now on every block: the
treatment no longer depends on position at all, and a `BLOCK` becomes a
self-contained unit that can be reordered, wrapped, or rendered alone without
its appearance changing.

**Adoption:** the first block of each row gains a `$border-light` left rule and
the `$space-md` indent every other block already had, so its content shifts
right by 12px and aligns with the blocks beside it.

### Changed — the `GRIDTOOLBAR` gap between tools is `$space-xs`

`.tmpl-grid-toolbar-container` spaced its controls with `$space-sm` (8px). Each
tool slot is already a full `$control-height` square with its own hover
surface, so the wider step read as a loose row against the dense grid below it.
Now `$space-xs` (4px).

## [2.8.1] - 2026-09-14

> Patch, one declaration. **A `BLOCK` no longer reserves space below itself** —
> read this before adopting, because it changes the vertical rhythm of every
> screen that stacks blocks.

### Fixed — `BLOCK` spaced itself twice

`.tmpl-block-container` carried both `padding: $space-md` and
`margin-bottom: $space-md`, so two stacked blocks were separated by the lower
block's padding *and* the upper block's margin, while the gap above the first
block and below the last was a single `$space-md`. The spacing between blocks
therefore read as one step larger than the spacing around the group, and a row
of blocks sat unevenly inside its container.

The margin is gone; the padding, and the `& + .tmpl-block-container` border rule
that draws the divider between adjacent blocks, are untouched
(`src/Widgets/Block/Styles/index.scss`).

**What changes on screen:** every `BLOCK` loses `$space-md` of space beneath it.
Blocks that relied on that margin to separate them from a *non-block* sibling —
a button row, a grid — now sit flush against it. Where that matters, the gap
belongs on the container, as a theme `qo-gap-*` / `qo-mb-*` utility on the
element that owns the layout, not back on the widget.

## [2.8.0] - 2026-09-14

> Minor. Finishes the 2.7.0 consistency pass across the four widget partials it
> did not reach, puts elevation on the theme's ramp, and takes every remaining
> hardcoded value in the package's SCSS either onto a `procode-vs-theme` token
> or into a named, documented local measure. Visual only — no API, prop or
> class-name change — but several of the fixes change how an existing screen
> renders. See "Behaviour changes" below.
>
> After this pass, a search for a `px` literal in a rule body across the five
> partials returns nothing but the two shadow-geometry lines that are commented
> as deliberately off-ramp.

### Fixed — the four toolbar icon tools overhung their slots

2.7.0 made Export, Settings, Reset and Quick Filter square at `$control-height`
by putting them on `qo-btn-icon`, the theme class for exactly that. Their flex
slots were left at the 32px basis from before, with `flex-shrink: 0` — so each
40px button sat in a 32px box and overhung it by 8px, which is precisely the
toolbar's own `gap: $space-sm`. The four tools rendered edge to edge with no gap
between them, and the group measured 32px narrower than it drew.

All four slots are now `flex: 0 0 $control-height`, stated as the token rather
than as a literal so they cannot fall behind the ramp again.

Two related size mismatches in the same file: the saved-view row's delete and
set-default icons declared `flex: 0 0 $size-sm` (16px) under a `$size-md` (20px)
box, so each asked its row for 4px less than it drew; and the advanced-search
criteria icon wrote `width: 20px` beside `height: $size-md`. Both are one
measure now.

### Fixed — one elevation ramp instead of four hand-rolled shadows

Every shadow in the package now comes from `procode-vs-theme`'s ramp, one level
per job: `$shadow-lg` for the four anchored popups, `$shadow-xl` for the five
modals (column chooser, Create New View, Select a new group, and both alerts),
`$shadow-md` for a surface that lifts over content scrolling under it (the
sticky filter header), `$shadow-sm` for a selected chip.

What it replaced, for the same two jobs: `-10px 0 50px rgba(0,0,0,.15)` on quick
filter and advanced search — a leftward blur on panels that open below and to
the right — `0 0 8px 0 $boxshadow-primary` on export and setting, which is a
*colour* token standing in for a whole shadow value, and two different two-layer
stacks on the modals (24/48 at .12 on the dialogs, 24/64 at .18 on the alerts).
Four values for two jobs, on surfaces that open from the same toolbar.

The one shadow deliberately left off the ramp is the export popup's format
chips: its second layer is an `inset` highlight that draws the chip's fill, not
an elevation.

`$boxshadow-primary` is no longer referenced by this package.

### Fixed — the alert dialogs kept their own copy of the button ramp

Both alert actions carry `qo-btn qo-btn-md` plus a variant, but
`.tmpl-dialog-box__footer .k-button` restated height, side padding, radius,
font size and weight at specificity (0,3,0) — which outranks `.qo-btn.qo-btn-md`
(0,2,0). The values agreed with the ramp on the day they were written; the
specificity meant the dialog would have kept its own copy as the ramp moved.
`.tmpl-confirm-btn--cancel` hand-rolled the default variant's fill, border, ink
and hover beside it.

Both are gone. The footer rule is `min-width: 110px` and nothing else — this
dialog's own measure, wide enough that a two-character action ("No", "OK") still
reads as a button. `.tmpl-confirm-btn--confirm` / `--cancel` keep their places in
the markup as consumer hooks but carry no rules.

Advanced search's footer had the same shape of problem: a `.k-button { height;
border-radius }` that both actions already got from the ramp, and which also
reached the two 20px criteria icons in that bar — they were winning their own
size back on equal specificity and source order alone.

### Fixed — `BLOCK`'s header border ignored its own prop

`enableHeaderBorder` toggles `.tmpl-border-header` from the schema, but
`.tmpl-block__header` declared the same `border-bottom` unconditionally and the
modifier then repeated all three declarations verbatim. The flag could not turn
anything off: every block header drew a rule whatever the schema said.

The base rule now carries only the label-to-value gap (`margin-bottom:
$space-sm`); the border and the padding above it belong to the modifier alone.

### Fixed — the anchored popups were pinned to a literal, not to their trigger

All four popups that hang off a toolbar button — advanced search, export,
setting, quick filter — opened at `top: 42px`. That was right when the buttons
were 32px and wrong the moment 2.7.0 put them on `$control-height`: 40px of
button, then a 2px gap that no longer matched anything.

Each now offsets from its own trigger's height —
`top: calc(100% + #{$popup-anchor-gap})` — so all four sit the same `$space-xs`
below the button they belong to, and follow the control ramp if it moves again.

### Fixed — five modals, three viewport caps

The two GridToolBar dialogs stopped at `85vh` / `90vw`, the column chooser at
`85vh` / `92vw`, the two alerts at `90vh` / `92vw`. Nothing distinguished
them — each was capped by whoever wrote it. One pair now
(`$dialog-viewport-max-width` / `$dialog-viewport-max-height`, `92vw` / `90vh`),
declared `!default` in both partials that need it so the copies cannot drift and
each partial still compiles standalone.

### Changed — every bespoke measure is declared once

What is left after the tokens is real geometry no ramp covers: how wide the 650px
filter panel is, how much of the export history should be visible at rest. Those
are a documented block at the top of each partial now, instead of literals
scattered through it — popup and dialog widths, the five scroll caps, the popups'
slide distance, and the two type-tracking values.

Two consequences beyond tidiness. The uppercase micro-labels tracked at `0.5px`
in the filter header and `0.4px` in the view summary; they are one
`$tracking-caps` now. And the file's four sub-grid `2px` insets — the unsaved
dot's overhang, the criteria icon's padding, two chip paddings — are one
`$optical-inset`, documented as the single measure the 4px ladder has no step
for.

### Fixed — the export format chips were lit from two directions

The chips' base fill and hover fill both run light at the top; the *selected*
fill ran `0.12` down to `0.06` — the only gradient on the toolbar lit from
below, under an outset shadow that says it is lit from above. It runs
`0.12 → 0.18` now: the same direction as its own hover state, and one step
stronger, so a selected chip still reads as selected under the pointer.

Their shadows split into the two things they actually are — the outer layer off
the theme's ramp like every other shadow in the package, the `inset` highlight
kept as the chip's own fill and stated in `$text-light` rather than a bare
`rgba(255,255,255,…)`. The alphas were `9%` / `12%` in one state and `0.12` /
`0.06` in the next; one notation everywhere now.

### Fixed — the remaining measures that were not on the scale

- **Menu.** The submenu column added `padding-top: $space-md` on top of the
  panel's own four-sided `$space-md`, so a column's heading started 24px below
  a panel edge that is 12px from the sides. The heading then spent `$space-md`
  on *both* sides of its rule, which read as a gap rather than as a label
  attached to the list under it — it is the package's usual `$space-sm` /
  `$space-md` pair now. A raw `1px` border width and the numeric spacing steps
  (`$space-6`, `$space-1`) are the named tokens the rest of the file uses.
- **Advanced search's footer** was 65px tall — a literal matching neither the
  ladder nor the 64px it worked out to — and was the only footer in the package
  dividing itself with a shadow instead of the hairline the others use. It is
  sized by its contents inside a `$space-md` / `$space-lg` band now, with a
  `border-top`.
- **The alert icon box.** The three paths a schema can take — the packaged
  illustration, `iconUrl`, `iconName` — rendered at 110×90, up to 96 square and
  88 square. The header and everything under it moved by up to 8px depending on
  which one a screen used. The wrapper is one fixed box and each path centres
  inside it.
- **The alert header** closes on the dialog rhythm's `$space-md`, not `$space-sm`.
- `$space-5`, `$space-4` and `$space-2` in the alert and block partials are the
  `$space-xl` / `$space-lg` / `$space-sm` aliases the rest of the package reads
  in.
- **The filter row's two icon tracks** were `25px` — on no ladder, holding a
  `$size-sm` button. Each spent 9px on nothing while the three editor cells
  beside them gave up 10px of the row between them. They are `$size-md`, the
  measure every other inline icon in that popup is squared on.
- **The AND/OR connector** drew its stubs `20px` tall at `1.5px` wide, offset by
  a `-20px` literal sitting beside the `$size-md` it mirrors. It is `-$size-md`
  and `$border-width-sm` now — a width that lands on a device pixel — and the
  chip's floor is `$size-xl`, not the `45px` three pixels off it.
- **The saved-view split button** sized its caret at `40px` and its label at
  `calc(100% - 40px)`: right by coincidence, with nothing tying either to the
  `$control-height` declared three lines above them.
- **The criteria box's two-line cap** was a measured `34px`; it is derived from
  the type it clamps now, `calc(2 * #{$font-size-sm} * #{$line-height-sm})`.
- **The filter-row id chip** padded `2px` on all four sides, which left its label
  touching its own side borders. Vertical keeps the optical inset, horizontal
  moves to `$space-xs`.
- **The dialog scrim** (`.k-overlay` on both alerts) was `rgba(0, 0, 0, 0.45)`.
  It is `rgba($shadow-color, 0.45)` — the theme's cool neutral — so the dim and
  the elevation sitting over it are lit by one light. Same for the segmented
  control's hover wash; and `outline-offset: -1px` is `-$border-width-sm`.

### Behaviour changes

1. **Block headers lose a border they were never asked for.** Any `BLOCK`
   without `"enableHeaderBorder": true` no longer draws a header rule. Add the
   prop to the ones that want it.
2. **The toolbar's tool group is 32px wider**, because it now occupies the width
   it draws. The global search is the only growing item in that row, so it gives
   up those 32px.
3. **Every Kendo dialog in the app moves to `$border-radius-lg`.** The unscoped
   `.k-dialog { border-radius: … !important }` in `ConfirmationAlert/scss` went
   from `md` (6px) to `lg` (8px) — the value the two alerts already overrode it
   with for themselves, and the one the three in-package dialogs use. One radius
   for every dialog in the stack instead of two.
4. **The four anchored popups open 2px lower**, at `$space-xs` below a 40px
   button rather than at a 42px literal.
5. **Advanced search's filter rows give their editors 10px.** The two icon
   tracks narrow from 25px to `$size-md`; the field, operator and value cells
   take what they release.
6. **The three GridToolBar dialogs may grow on a short or narrow screen** —
   `85vh` → `90vh`, and `90vw` → `92vw` for two of them. All are
   `height: fit-content` with an internal scroll section, so this raises a
   ceiling rather than resizing anything at rest.
7. **A selected export chip is a shade stronger**, and its gradient runs the same
   way as every other state of the same chip.
8. **The alert scrim is the theme's cool neutral**, not pure black, at the same
   45%.

## [2.7.1] - 2026-09-14

> Patch. Visual only — the export popup now spaces itself on the same measures
> as the other anchored popups on the grid toolbar. No API, class-name or
> markup change.

### Fixed — the export popup was the one anchored popup off the scale

2.7.0 put the four anchored popups on one pair of measures — `$space-md` to
open a section, `$space-sm` inside it — but the export popup only got the half
of that treatment that was visible at the time: the "Previous Exports"
separator. Its two other spacing zones were still carrying values chosen per
element, and on a 320px dropdown that stacks two labelled lists the mismatch is
the whole layout:

- **Scope sections** stacked with `$space-sm` and put `$space-xs` under a scope
  label — half a step tighter than every other popup on the toolbar. Both are
  the standard pair now (`$space-md` / `$space-sm`), and the last scope drops
  its bottom margin so the history header's own `$space-md` is not paid twice
  before the rule. The scope container's `max-height` is spelled out of those
  same two measures, so it still shows exactly three sections.
- **History rows** were `2px` of margin plus `2px` of padding — a literal that
  matched nothing else in the package, and read as a dense block rather than a
  list. They are `$space-sm` on both sides of each separator now, the same
  measure quick filter uses between its rows, with the outer two edges left to
  the popup's own inset.
- **Row separators are full-bleed**, matching the header rule directly above
  them and quick filter's row dividers: the scroll container cancels the
  popup's side inset and each row re-adds it as padding, so the rules reach the
  popup's edges while the text stays on its column. The negative margin sits on
  the container rather than on the rows deliberately — inside a scroll box
  (`overflow-y: auto` computes `overflow-x` to `auto`) row-level negative
  margins would have hung a horizontal scrollbar under the list.

Also drops a bare `h4 { color; margin-bottom }` inside the popup that both of
its `h4`s already overrode by class — a dead declaration that would have
silently claimed any `h4` added to the popup later.

## [2.7.0] - 2026-09-11

> Minor. Repairs the button sizing that `procode-vs-theme` 2.4.0 broke, and
> hands the scrollbar treatment to the theme. Requires `procode-vs-theme`
> >= 2.5.0 — it uses `$control-height` and relies on the theme owning the
> scrollbar rail.

### Fixed — every button in the package had snapped to 40px

Up to `procode-vs-theme` 2.2.0, `.qo-btn` was nearly inert: `display:
inline-block` and a border radius, nothing else. Every button in this package
composes it — and that is load-bearing rather than decorative, because the theme
scopes its button ink as `.qo-btn.qo-text-*` (0,2,0) so it can beat Kendo's own
`.k-button-*`. Drop `qo-btn` and the colour goes with it.

2.4.0 turned `.qo-btn` into a real component: `min-height: $control-height`
(40px), `padding: 0 $space-4`, `display: inline-flex`. **`min-height` beats
`height`**, so every button here snapped to 40px regardless of what its own rule
said, and picked up 16px of side padding it had never allowed for.

What that looked like:

- **Column chooser rows went from ~40px to ~61px.** Each row's remove control is
  an icon button with no `tmpl-*` rule of its own, so the 40px floor became the
  row height. Ten rows no longer fitted the pane and the list read as clipped.
- **Saved-view row icons went from 20px to 40px** inside a 26px row.
- **Export history actions** — download, retry, delete — the same.
- Toolbar buttons went from `$size-lg` (32px) to 40px. That one was an
  improvement and is kept: 40px is the listing specification's main control
  height and matches the global search input beside them. `.tmpl-button` now
  states it deliberately.

Rules that set an explicit height now state `min-height` beside it, and clear
the component's side padding where the button is a fixed square:
`.tmpl-button`, `.tmpl-button-sm`, `.tmpl-delete-icon`, `.tmpl-set-default`.

Nine call sites had `qo-btn` with no size class and no `tmpl-*` rule to fall
back on, so their size is now stated where the ramp expects it — `qo-btn-icon`
plus `qo-btn-xs` (24px) or `qo-btn-sm` (32px) for icon buttons, `qo-btn-md` for
dialog actions: `DragDropContext`, `ColumnChooserView`, `ExportPopup` (x4),
`GroupByPopUp`, `UserViewView`.

Two of those also carried `qo-text-light` / `qo-font-weight-medium qo-font-md`
on a `qo-btn-primary`, which never applied — the variant rule is (0,2,0) and
already sets white ink and the medium weight. They are dropped rather than left
to imply control they do not have.

### Fixed — one rhythm, one control ramp, across every template surface

The 2.4.0 button regression above was the visible half. Auditing the rest of the
package turned up the same shape of problem in the measures themselves: the
values were chosen per widget rather than from the scale, so surfaces that open
side by side did not match.

**Dialog gutters.** The vertical rhythm ($space-lg / $space-md) was already
shared, but the SIDE padding tracked each dialog's width — $space-xxxl (32px)
for the 900px column chooser, $space-xl (20px) for the 480/550px popups. Correct
in isolation, and the reason the chooser, Create New View, Select a new group and
the confirmation dialog read as four unrelated windows. All four now use
$space-lg, the same measure as the vertical rhythm. 14 declarations.

**The alerts were never on the rhythm at all.** ConfirmationAlert used
`padding: 32px 32px 28px` — three literals, none of them a token — plus an 18px
icon margin, a 12px gap and a 16px footer inset. Its action buttons were 38px
tall at 12px type, where `.tmpl-button` and every `qo-btn` are 40px at 13px, so
the confirm/cancel pair sat 2px short of every other button in the app. Both
alerts also hard-coded a 12px dialog radius, which matches no token (the ramp is
4/6/8/16) while every other dialog used $border-radius-lg.

**Control heights.** Every control in the package was sized with `$size-lg`.
That is the ICON ramp (8/16/20/32/48/64) — it reads 32px so it worked, but it
means "a large icon", not "a control". The theme has carried `$control-height`
(40px) and `$control-height-sm` (32px) since 2.3.0. Using the icon ramp is how
the heights drifted from the app's: a screen's own inputs are 40px, while every
input inside a template popup was 32px, so the same field was two heights
depending on which surface it appeared on. Text inputs, dropdowns and search
boxes are now $control-height; row floors stay 32px but say
$control-height-sm. `.tmpl-dropdown` also declared `height` twice.

**Off-ladder leftovers.** The saved-views control was 30px in a 40px toolbar
row. Its dropdown rows were 26px with 8px padding — a 10px content box for the
20px icons inside them, which therefore overflowed their own row; now 32px with
a 4px inset. A 5px popup offset became $space-1. Menu was entirely literal
(10px paddings, a 25px gap, a 3px row inset — 8, 12 and 24 are the neighbours on
the ladder). Block wrote `$space-md` three times in one shorthand and used 6px
header rules.

**Dialog height.** Both alerts set `height: 300px` on a dialog whose body is
free text, so a two-paragraph message scrolled inside a half-empty window. It is
now `min-height`, still capped by `max-height: 90vh`. Consuming apps carrying a
local `height: auto` override for this can drop it.

Measured after, on one screen: toolbar button, saved-view control, advanced
search dropdown and input, chooser search, Group By dropdown and every dialog
action button — all 40px. Chooser, advanced search and Group By gutters all
14px, headers all `14px 14px 12px`, footers all `12px 14px 14px`.

### Fixed — overflow now belongs to the section that overflows

Three dialogs, three different answers to the same question. The column chooser
and Create New View both capped at 85vh and scrolled an inner body; the Group By
popup had **no cap and no scroller** (`height: fit-content` + `overflow:
hidden`); and both alerts capped at 90vh with `overflow: hidden` and nothing
inside them scrolling — so a message longer than the window was cut off with no
way to reach the rest. The consuming app already writes multi-paragraph
confirmations, so that one was reachable in practice rather than theoretical.

Both now use the shape the other two already had: capped height, header and
footer holding their size, and the body owning the overflow
(`flex: 1 1 auto; min-height: 0; overflow-y: auto`). `min-height: 0` is the part
that matters — without it a flex child refuses to shrink under its content and
pushes the footer out of the dialog instead of scrolling.

The Group By footer was nested *inside* its body, so pinning the body would have
scrolled the action button away with the content it acts on. It is now a sibling,
matching every other dialog in the package, and drops the negative margin it used
to need to cancel the body's side padding.

### Fixed — every icon-only button is square

An icon-only control was sized by hand at each call site: an inline
`width: 100%` over a natural-width root, with Quick Filter additionally pinned to
32px so it sat narrower than its neighbours in a 40px row. None of the four
toolbar icons was actually square, and the Group By close carried `.tmpl-button`
— a height with no width. All now use `qo-btn-icon`, the theme class for exactly
this, which sets width to the button size and clears the side padding.

The global search magnifier and clear cross are Kendo's own `.k-clear-value`
spans, which this package had never styled, so they arrived at Kendo's
rem-derived sizes — a 29.6px button inside a 37.6px span. Square, but on no
ladder this stack owns, and a different size from every other icon in the row.
Both are now square at the full control height (40px). The full height matters:
at 32px in a 40px field the glyph measured centred in its own box but read
off-centre in the space, because the field 12px padding sat on one side of the
box while the separator hugged the other - 18.8px from the field border, 6px
from the separator. A 40px box with the field padding moved onto the affordances
puts an equal 10px on all four sides of the 20px glyph.

Measured: the five icon-only toolbar buttons are 40x40, the search and clear
icons and the two dialog closes 32x32, the chooser's per-row removes 24px. The
in-popup 16px and 20px icons were already square through their own `tmpl-*`
rules and are left alone — adding `qo-btn-icon` there would have forced them to
40px.

### Fixed — label pairing and the column chooser gutters

**A field label now sits with its control.** Both Create New View and Select a
new group put 8px between a label and the input under it and 14px between one
field and the next — near enough that the label read as floating between the two
rather than belonging to either. The label gap is now 4px, which is what the
theme own `.qo-label` uses, so a template popup and a screen form pair label to
input identically. The 14px between sections is unchanged.

**The column chooser scroll rails moved in beside their lists.** A scroll rail is
laid out at the right edge of the PADDING box, so the panes symmetric 14px
gutter parked both rails against the dialog edge with 14px of dead space between
each rail and the rows it scrolls - they read as window chrome rather than as
part of the list. The right gutter is now `$space-1`, which puts each rail 4px
clear of its content; the rail itself occupies what the left gutter spends on
padding, so the two sides still balance. The count band full-bleed negative
margins track the now-asymmetric gutters.

**The two panes start on the same rhythm.** The left pane search box carried
12px of its own bottom padding PLUS an 8px margin - 20px to the first row, where
the right pane count band leaves 12px. The two lists began level at the top and
drifted apart immediately below. Both are now 12px.

Measured: both panes `0 4px 14px 14px`, both rails 4px from their content, both
contents inset 8.8px on the right, both headers 12px clear of their first row.

### Changed — the scrollbar rail moved to the theme

The column chooser's panes were the only designed scrollbar in the stack, and
they were reachable only inside that one widget. That treatment is now
`procode-vs-theme`'s `utilities/_scrollbar.scss`, applied to every scroll
container, and the three copies here are removed — including two in the export
popup that had already drifted from it (2px thumb radius, no track, no hover).

No visual change to the chooser; every other scroll container in every consuming
app now matches it instead of falling back to the platform scrollbar.

## [2.6.0] - 2026-09-11

### Added

- **`Is Empty` / `Is Not Empty` in advanced search, on all five filter types.**
  `TEXT`, `NUMBER`, `DATE`, `BOOLEAN` and `LIST` each gain the pair
  (`Operator.IsNull` / `Operator.IsNotNull`) in `constant.ts`. They are
  value-less on purpose: the API's query generators map them to `IS NULL` /
  `IS NOT NULL` and skip the value entirely. **This is `NULL`, not `""`** — a
  text column holding an empty string is not caught by `Is Empty`.

  Three places had to agree for a value-less operator to be usable at all, and
  they now ask one helper, `operatorNeedsValue`
  (`helper/AdvancedSearchFunctions.ts:50`), instead of each deciding for itself:

  | Place | Before | Now |
  |---|---|---|
  | Apply button (`FilterPopup.tsx:468`) | required a non-empty value on every row | exempts the value-less operators |
  | Per-row validation (`FilterPopupView.tsx:52`) | same rule, decided separately | same rule, same helper |
  | Value cell (`TypeBasedWidgetFactory.tsx:392`) | fell through to the `TEXT` fallback and offered a text box the API ignores | renders `NoValueRenderer` — an empty `.tmpl-advanced-no-value` cell |

  When the first two disagreed, Apply lit up on a row that then failed
  validation, or the reverse — a row that could never be applied and never said
  why. The check is asked **before** the type lookup in
  `TypeBasedWidgetFactory`, so it wins over the per-type renderers.

  The cell is emptied but **not removed**: `.tmpl-advanced-no-value` keeps
  `display: block` and `min-height: $size-lg` (`GridToolBar/scss/index.scss`),
  because dropping it reflows the row's five columns and slides the delete
  buttons under the Value heading.

No schema changes — the operators appear in every advanced-search popup with no
configuration. Nothing is removed, and a filter row built before this release
behaves exactly as it did.

## [2.5.0] - 2026-08-26

### Changed

- **`CONFOMATIONALERTBOX` and `INFOMATIONALERTBOX` close on the click, not when
  the action behind them finishes.** Confirm, cancel, ESC and overlay-close all
  dismiss the dialog immediately and the handler continues in the background.

  Two consequences. (a) A confirm action whose `apiConfig` param reads the
  dialog's own field under `valueReplacePolicy: "STATE"` — e.g.
  `"screenData.promptDelete.id"` — now resolves **empty**, because
  `stateHandler` reads live store state and the field has just been nulled. No
  shipped schema does this; pass what the action needs through the event args
  instead. (b) A UI test that asserted the dialog was still visible while the
  request was in flight will now find it gone.

## [2.4.1] - 2026-08-20

### Fixed

- **Its four sides did not match, and the dropdown was indented past its own
  label.** Both came from one cause: `QuickFilter/Filter.tsx` puts
  `tmpl-dropdown-container` on the row `<div>` it renders (`:26`) *and* passes
  it again as the dropdown's `rootStyle` (`:35`), which the standard widget
  renders as a `<section>` inside that row. While the rows carried the popup's
  inset through a descendant selector, both copies took it — the dropdown is
  `width: 100%` of that inner wrapper, so it started one inset right of the
  label above it and stopped one inset short of the right edge, while the
  popup's foot paid the vertical inset twice and its head paid it once.

  **The inset now lives on the popup, and one measure spaces everything in it.**
  `$space-md` on all four sides of `.tmpl-quick-filter-popup`, and the same
  `$space-md` again above and below every row separator
  (`> .tmpl-dropdown-container { padding: $space-md 0 }`, zeroed on the first
  and last row so the outer edges stay the popup's own). Separators are still
  full-bleed: the row cancels the side inset with `margin: 0 (-$space-md)` and
  re-adds it as padding, so the rule reaches both edges while the content stays
  on its column — the trick advanced search's sticky header row already used.

  **The `>` is load-bearing.** A descendant selector puts the double inset
  straight back.

- **The label's leading no longer inflates the gaps that open on it.** A line
  box is taller than its text and `$line-height-md` puts half that difference
  above the glyphs, which quietly added ~3px to the popup's top inset and to the
  space under every separator — the two gaps that face a label rather than a
  control — so identical padding did not read as identical. The label is now
  `display: block` and trims half its leading with
  `&::before { margin-top: calc((1 - #{$line-height-md}) * 0.5em) }`, derived
  from the token rather than measured.

  **That rule is `> span:first-of-type`, and the combinator is required.** A
  Kendo picker is built out of nested spans — the picker itself,
  `.k-input-inner`, the caret's icon — and each is the first span among its
  siblings, so the bare `span:first-of-type` this rule used to be reached inside
  the widget and restyled its internals. Type and colour leaking in there merely
  overrode the widget's own 11px placeholder with 12px; `display: block` broke
  the picker's `inline-flex` layout outright, putting the placeholder 8.2px
  above centre and the caret 16.8px below it and ~200px adrift of the right
  edge. Scoped to the row's own child, the widget renders untouched: picker
  `inline-flex`, placeholder and caret both centred, caret 6.4px off the right
  edge, placeholder back to its intended `$font-size-sm`.

  Measured on the finished popup (225px, three rows): left, right and bottom
  insets 12.5px and the top 13.3px to the label's cap height; 12.5px above each
  divider and 13.3px below it; every divider flush to both edges; label and
  dropdown sharing a left edge.

## [2.4.0] - 2026-08-20

### Added
- **Advanced search: `BOOLEAN` filter options are declared in JSON.** A
  two-state field is rarely worded "True / False" — it is "Self / API Key",
  "Active / Inactive", "Own account / Sub-account" — and only the schema author
  knows which. The radio group now takes its options from the **same
  `dataProvider` block a `LIST` field uses**, inline (`SELF`) or by
  supportive-data key (`API`):

  ```json
  {
    "label": "Key Source",
    "apiPropertyName": "isSelfKey",
    "type": "BOOLEAN",
    "value": "isSelfKey",
    "dataProvider": {
      "valueLoadType": "SELF",
      "value": [
        { "label": "Self", "value": "self" },
        { "label": "API Key", "value": "apikey" }
      ]
    }
  }
  ```

  No new config block and no new loader: `useFilterData` simply stopped gating
  its option fetch on `LIST` alone and now serves both option-backed types
  (`helper/useFilterData.ts`). One radio is rendered per entry, so the type is
  in practice "a small fixed set rendered inline" rather than strictly
  two-valued; the operator list stays Equal / Not Equal.

  **Opt-in and backwards-compatible.** A `BOOLEAN` field that declares no
  `dataProvider` keeps the built-in `True` / `False` pair, with the same
  `"true"` / `"false"` string values earlier builds wrote, so an existing
  screen produces an identical request payload.

### Changed
- **Column chooser: tightened vertical spacing.** Four measures were costing the
  dialog height without earning it, on a panel whose two lists are the point:
  - the header's bottom padding (`$space-xxl` → `$space-md`), which had been
    pushing the divider — and everything below it — down the screen. The top
    padding stays: that lead-in is what the dialog opens with.
  - the "*n* Column(s) Selected" band: its type (`$font-size-lg` →
    `$font-size-md`) and its vertical padding (`$space-lg` → `$space-md`,
    taking the band from 50px to 42px). It labels the pane; it is not a second
    heading competing with the dialog title. Its **horizontal** padding stays
    `$space-xxxl` — that is the measure the band's negative margin cancels, and
    what keeps the count lined up with the rows beneath it.
  - the search box's top padding (`$space-xxl` → `$space-md`), held at the same
    measure as the count band opposite it, so the top of the search input sits
    exactly level with the top of the count text rather than 12px below it.
    These two move together — change one and change the other.
  - and one thing gained rather than trimmed: `.tmpl-chooser-body` now has a
    `padding-bottom`, so the lists no longer run flush into the dialog's bottom
    edge. It belongs on the body and not on the panes — a pane's own
    `padding-bottom` is inside its scroll box, so it only shows once the list is
    scrolled to the end.
- **The three dialogs now share one rhythm and one type scale.** The column
  chooser, **Create New View** and **Select a new group** open in the same app
  minutes apart and were each spaced by hand: three different header paddings,
  two title colours, and body padding that agreed with neither. They now run on
  one set of measures (documented at the top of the "Dialog rhythm" block in
  `GridToolBar/scss/index.scss`):

  | | Value |
  |---|---|
  | header | `$space-lg <side> $space-md`, title `$font-size-xl` / `$text-darker` |
  | body | `$space-lg <side> 0` — the bottom belongs to the footer |
  | footer | `$space-md <side> $space-lg`, full-bleed with a `border-top` |
  | field block | `margin-bottom: $space-lg` |

  Dialog titles drop from `$font-size-xxl` to `$font-size-xl`: 18px was set
  against the 900px chooser and was oversized on a 480px popup. Only the **side**
  padding still differs between them, and deliberately — it tracks the dialog's
  width (`$space-xxxl` for the 900px chooser, `$space-xl` for the 480/550px
  popups) so the content column stays proportionate rather than identical.

- **Create New View and Select a new group have a real footer separator.** Both
  footers already carried a `border-top`, but they sit inside the body, so the
  rule was inset by the body's padding and stopped short of each edge while the
  header's divider above ran full width. They are now full-bleed bars, the same
  way `.tmpl-title` is: negative inline margins cancel the body's side padding
  and matching inline padding puts the buttons back on the body's column.

  Their bottom padding moved from the body onto the footer. That is not
  cosmetic: Create New View's footer is `position: sticky`, and sticky offsets
  from the **padding box** — with a body `padding-bottom` the bar floated 17px
  short of the dialog's bottom edge. Both footers now sit flush.
- **The export popup's "Previous Exports" separator was spending 41px.** In a
  320px dropdown that is mostly list, one section break ran `$space-lg` above
  the rule, `$space-md` between rule and label, and `$space-md` below it. It now
  uses the anchored-popup pair — `$space-md` to open the section, `$space-sm`
  inside it — which takes the popup from 245px to **208px** with the break still
  perfectly legible.

- **The four anchored popups share one inset rule.** Advanced search (650),
  export (320), quick filter (225) and setting (200) hang off their toolbar
  button rather than centring on screen, and each had been spaced by hand:
  `$space-lg`, `$space-md`, `$space-sm` and `$space-sm` respectively. They now
  follow the same principle the dialogs do — the inset tracks the popup's width
  — which in practice moved two of them:

  - **quick filter** `$space-sm` → `$space-md`, and its `border-radius` from
    `$border-radius-sm` to `$border-radius-md`, which is what every other popup
    in the package already used. The inset was still carried by the rows here;
    2.4.1 moved it onto the popup itself — see its **Fixed** section above.
  - **setting** `$space-sm` → `$space-sm $space-md`. A menu row is full-bleed —
    its hover band spans the popup — so that padding *is* the popup's inset.
    Horizontal now matches its siblings; vertical stays tight, because the row's
    height comes from the band rather than from the text. It is the only split
    pair in the group; every other inset is square.

  Advanced search and export were already on the right measures and are
  unchanged. Within any anchored popup, sections open with `$space-md` and space
  inside themselves with `$space-sm` — half the dialogs' `$space-lg`/`$space-md`
  pair, since these are dropdowns rather than modals. Written up as "Popup
  insets" beside "Dialog rhythm" in `GridToolBar/scss/index.scss`.

  One trap recorded there: advanced search's `$space-lg` is load-bearing beyond
  padding. Its sticky header row cancels it with `margin: 0 (-$space-lg)` and
  re-adds it in `width: calc(100% + (2 * $space-lg))`, so changing that
  container's inset means moving all three together.

### Fixed
- **A `BOOLEAN` filter never showed its own selection.** The radio group wrote
  the strings `"true"` / `"false"` but read by comparing to the booleans
  `true` / `false`, so neither radio was ever checked and the row looked empty
  the instant it was filled in. Selection is now compared by a **string key**
  (`String(value)`), which is what Kendo echoes back through the DOM — `true`,
  `"true"` and `1` compare as one key, so a filter saved by an earlier build
  restores checked.

  The selection is stored as the whole `{label, value}` option, the shape a
  `LIST` selection already produces, so the wording survives into
  `appliedQuery` and a saved view. Adapters that unwrap a `LIST` value need no
  change — `getRawFilterValue` reads `.value` off it exactly as before.
- **A supportive-data key that resolves to nothing is now audible.** An `API`
  option load whose response lacks the requested `dataProvider.key` used to
  render an empty editor and say nothing, which reads as "this field has no
  options" rather than "the key did not resolve" — the most common
  advanced-search integration mistake. It now logs
  `[vs-template] Advanced search: no supportive data for key "…" (filter "…")`.
- **The `BOOLEAN` radio group rendered as stretched ovals.** The advanced-search
  row sizes its editors with `.tmpl-advanced-input input { height: $size-lg }`,
  which outranks Kendo's `.k-radio-md { width: 1rem; height: 1rem }` on height
  alone — so every radio was 16px wide and 32px tall. The rule now excludes the
  control inputs (`input:not([type="radio"]):not([type="checkbox"])`).

  The cell also gained real layout of its own, `tmpl-advanced-radio-group`
  (plus `tmpl-advanced-radio-group-disabled`), replacing the inline style the
  renderer used to carry: the options sit on one line at the same height as the
  dropdowns beside them, labels take the dropdowns' text colour rather than the
  row's lighter one, a set too wide for the cell wraps as whole items instead of
  clipping, and a disabled cell fades as a whole. Moving the layout out of the
  JSX also makes it overridable by a host theme.

## [2.3.0] - 2026-08-18

> Minor rather than patch: the column-chooser redesign is a visible UI change
> and `registerLocalization` is a new public API. **2.2.1 was written up but
> never published to npm**, so this release carries its fixes too.
>
> ⚠️ Ship alongside `procode-vs-theme` 2.1.0 or newer — the toolbar SCSS now
> uses `$space-xxl` and `$text-darker`, which older theme versions do not
> define, and Sass fails the build with "Undefined variable".

### Added
- **`registerLocalization` — a localization seam for host-resolved values.**
  The toolbar's advanced-search editors need values only the host app can
  resolve: the signed-in user's date pattern, the reseller's currency symbol.
  Each of those lives somewhere app-specific — a token claim, a cached
  user-defaults payload, a settings API — and this package must not learn any
  of those shapes. So the app registers **one resolver function** and the
  library asks it for a value by key.

  New root exports: `registerLocalization`, `getLocalizationValue`,
  `getCurrencyFormat`, `LocalizationKey`, and type `LocalizationResolver`
  (`src/utils/localization.ts`).

  Two properties worth having. **Any key works** — a widget may ask for a key
  this package has never heard of and the app can answer it, so adding a unit
  of measure or a number separator becomes an app-side edit only. And **values
  resolve at render, not at boot** — a symbol arriving from an async settings
  call is picked up the next time a widget asks, where a fixed object
  registered once would have frozen the first, possibly empty, read.

  Unregistered, every lookup returns the caller's fallback, so this is
  backwards-compatible by construction: the advanced-search date and numeric
  editors behave exactly as they did before. A resolver that throws is treated
  as "no value" rather than being allowed to take the render down with it — a
  formatting lookup must never be able to blank a screen.

  `getCurrencyFormat()` builds a Kendo pattern from a **literal** symbol
  (`'€'#,##0.00`) rather than Kendo's `"c"` specifier, which resolves the symbol
  from the Intl locale — the browser's idea of currency, not the reseller's.

### Changed
- **CRM-914 — Column chooser redesigned.** The selected-column list was a stack
  of bordered, shadowed cards separated by gaps; it is now one grouped list —
  a single rounded container with hairline dividers — which reads as one object
  and scans far faster. The count is a full-bleed sticky section band rather
  than floating text, the available-column list drops its per-row dividers for
  hover highlighting, the search box gains a magnifier adornment, and the popup
  opens at `min(900px, 92vw)` so a long label plus both toggles fit on one line
  without ellipsising. Button styling and the close/remove icon colours are
  unchanged.
- The two row toggles sit on fixed tracks (`$size-xxl + $size-xl` / `$size-xxl`) applied to the
  **widget roots**, so "Include in card" and "Freeze" line up down the column
  instead of drifting with each label's width. The tracks must not go on
  `.tmpl-include-card` or `.tmpl-radio` — those land on Kendo's
  `k-checkbox-wrap` and on the `<input>`, so sizing them stretches the control
  itself into a pill.

### Fixed
- **The drag handle was invisible.** Rows are drag-reorderable, but the grip
  was markup only: `k-i-drag` is a Kendo **font** icon and this stack ships the
  SVG icon set, so the glyph resolved to nothing and the span measured `0px`
  wide — the affordance the reorder depends on was never drawn. It is now
  painted as a radial-gradient dot grid on the same span, which needs no webfont
  and keeps the element a plain `<span>` (react-beautiful-dnd refuses to start a
  drag from a `<button>`). It mutes to `$border-secondary`, darkens on row
  hover, and carries `grab`/`grabbing` cursors.
- **The grip advertised a gesture that does nothing on `primary` rows.**
  `ColumnChooserView` renders those outside a `<Draggable>`, so they cannot be
  reordered, but `DragDropContext` drew the handle on every row regardless.
  It is now hidden there (`tmpl-drag-disabled`) — hidden rather than removed,
  so the labels stay aligned down the column, the same trick the remove icon
  already used for those rows.
- Both column lists rendered at **different sizes** for no reason — the
  available list was pinned to `qo-font-md` by the view while the selected list
  inherited the body's `$font-size-sm`. They are now deliberately tiered:
  selected labels `$font-size-md`, available labels `$font-size-sm`, both at
  `$text-secondary`.

### Fixed (group by)
- **Changing the grouping now returns to page 1.** `onGroupApply` wrote only
  `groupedBy`, leaving `pagination.skip` where paging had left it — the only
  toolbar filter that did not reset it (advanced search, global search and
  quick filter all do). Changing the group changes the result set, so the
  stale `skip` pointed into the old one: after a CARD view had scrolled to the
  end (skip 100 of 121) adding or removing a group refetched **page 3** and
  replaced the list with its 21-record tail — "1 - 21 of 121 items".

### Fixed (export popup)
- **"Previous Exports" had no separation from the format chips.** The heading
  sat flush against them (measured 0px apart), so picking a format and
  reviewing past exports read as one undifferentiated list. It now opens a
  section: a full-bleed rule with `$space-lg` above and `$space-md` below,
  and the heading is styled as a section header (`$font-size-md`,
  `$font-weight-semibold`, `$text-secondary`) rather than a bare `<h4>`.
  Required giving it a class — `tmpl-export-history-header` — as it had none.

### Fixed (chooser spacing)
- **The available-column rows padded the checkbox, not the row.**
  `.tmpl-column` is `widgetStyle.className`, which Kendo puts on the
  `k-checkbox-wrap` — so the row padding added in the redesign was inflating
  the checkbox-to-caption gap to 14.5px (against 2px on the right-hand rows)
  and shrinking the hover highlight to the box. Row styling moved to
  `.prc-label-field`, the wrapper `withLabel` actually renders.
- Every control-to-caption gap now comes from one declaration —
  `.tmpl-chooser-body { --prc-label-gap: $space-sm }` — so the left list,
  "Include in card" and "Freeze" are identical at 5px.

### Typography and spacing

Matched to the reference design, entirely from `vs-theme` tokens — no literals.
Sizes and colours were **measured off the reference screenshot** (glyph ink
height and core pixel colour per string) rather than eyeballed, after two passes
that ran too large and too dark:

| Role | Token |
|---|---|
| Dialog title | `$font-size-xxl` / `$font-weight-semibold` / `$text-darker` |
| "N Column(s) Selected" band | `$font-size-lg` / `$font-weight-semibold` / `$text-secondary` |
| Selected-column labels | `$font-size-md` / `$font-weight-regular` / `$text-secondary` |
| Available-column labels | `$font-size-sm` — a step down; it is a scanning list |
| "Include in card" / "Freeze" | `$font-size-sm` (the body base) / `$text-secondary` |
| Header padding | `$space-xxl $space-xxxl` (28/30) |
| Pane padding | `$space-xxxl` sides, `$space-xxl` bottom |
| Row / control heights | `$size-lg`, tracks `$size-xxl + $size-xl` and `$size-xxl` |
| Drag grip | `$size-xs` × `$size-sm`, dots on a `calc()` grid off those |

`$space-xxl` (28px) and `$space-xxxl` (30px) are **new tokens added to
`procode-vs-theme`** for this — the alias scale stopped at 21px. That makes this
release depend on a vs-theme carrying them; ship the two together or the app
fails to compile with "Undefined variable".

The only literals left in the block are pre-existing (the 900px modal width, the
400px pane height, the shadow, the scrollbar) plus the grip's dot radii, which
are glyph geometry rather than spacing.

## [2.2.1] - 2026-08-17

### Fixed
- **Advanced search: Cancel now closes the panel.** `onAdvancedSearchCancel`
  rebuilt `filterRows` from `appliedQuery` — correctly discarding the draft —
  but never called `handlePopupShow`, so the popup stayed open showing a
  reverted form with no way out but a click outside. Its two siblings, Clear
  (`FilterPopup.tsx:305`) and Apply (`:450`), had always closed it; Cancel now
  matches.
- **CRM-914 — Column chooser padding.** The panel was cramped: the available-
  column rows were pinned to a fixed `28px` with `2px` of vertical padding, the
  selected-column pane had **no** vertical padding at all (`0 $space-md`), and
  its rows sat on `$space-sm $space-md`. Rows are now sized by their padding
  (`min-height: $size-lg`, `$space-sm $space-md`) so long labels wrap instead of
  clipping, both panes carry `$space-lg` side and bottom padding, and the
  selected-column rows use `$space-md $space-lg` with `$space-md` between them.

## [1.0.4] - 2025-10-09

### Changed
- Removed all @progress/kendo-* packages from peerDependencies
- Kendo UI packages are no longer required dependencies
- Updated README.md to reflect new dependency requirements

## [1.0.3] - 2025-10-08

### Fixed
- Republish with correct util dependency in package.json
- Resolved dependency conflicts during package installation

## [1.0.2] - 2025-10-08

### Fixed
- Emergency version bump due to dependency resolution issues

## [1.0.1] - 2025-10-08

### Fixed
- Resolved "util.inherits is not a function" error in browser environments
- Fixed rollup configuration to properly handle Node.js built-ins in browser
- Added util package as dependency to provide missing inherits function

## [1.0.0] - 2025-10-08

### Added
- Initial release of VarStreet Template Library
- TemplateWidgetFactory for dynamic widget creation and management
- TemplateActionFactory for action handler and template action management
- Comprehensive TypeScript support
- Modular architecture with tree-shaking support
- Multiple export endpoints