# Changelog

All notable changes to Masu are documented in this file.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and Masu follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.7.0] - 2026-10-24

### Added

- `on_row_activate`: a row opens on a click on it, or Enter on its
  active cell, where the cell does not edit: the function gets the row
  (`id`, `row`, `grid`, `scope`), and gives `{:patch, path}` or
  `{:navigate, path}` for the grid to follow. A list whose rows open in
  a panel beside the grid needs no link in a cell for it. A click on a
  link, a button or an input in a cell, a drag, and the click that ends
  an editor do not open the row; the cells that open it show a pointer.
  See the Actions guide.
- `Masu.Test.activate_row/3`.
- The development app's `/open` page: rows that open in a panel.
- `grow` on a column: it takes the room the columns leave in a grid wider
  than them, from its width up, and follows the grid's width; a grid
  narrower than its columns scrolls sideways as before. `grow={2}` takes
  twice the share of `grow`. A person who resizes it gives it their
  width, and it grows no more (in their view too). See the Columns guide.
- The development app's `/grow` page: a column that grows.

### Fixed

- A column whose `min_width` was more than its width (`min_width={280}`
  with the default 200 of text) was drawn at its width, narrower than a
  person could resize it to. It is at least its `min_width` wide.
- The edge of the last column's header, which a person drags to resize
  it, went 4 pixels past the end of the columns: a grid whose columns
  fill it, or wider, scrolled sideways 4 pixels more, over nothing.

## [0.6.0] - 2026-10-24

### Added

- The filter bar, the default: the filters as chips above the grid, each
  saying its column, its operator and its value apart and opening its
  filter on a click, and **Add filter**, a menu of the columns that filter
  that opens the filter of the one picked, under it. A header's menu, its
  **⋯**, has its sorts (ascending, descending, Clear sort) above its
  items, Group by and filter, for every column that sorts. See the Filters
  guide.
- `header_filters={false}`: the filters in the bar alone; a header's menu
  has its sorts, items and Group by.
- `filter_bar={false}`: the leaner header of before, a funnel on each
  header that filters, and the chips only while there are filters.
- The messages `add_filter`, `no_columns` and `sort_clear`.
- The development app's `/header` page: the three headers.

### Changed

- A grid has the filter bar unless `filter_bar={false}`: a line above the
  grid, and **⋯** in place of the funnel on its headers.
- The chips are drawn anew: the column's label, the operator and the
  value in their weights, on a tint of `--masu-accent`, and a × drawn as
  an icon. Their text (what a screen reader says) is the same.

- The grid's browser code is a core (`grid.ts`) and a class for each of
  its features (`features/`), as AG Grid's services are, with the pure
  parts in modules of their own; see CONTRIBUTING. Nothing changes in
  what the grid does, and it draws and scrolls its rows in the same time;
  the bundle is 1.4 KB larger, gzipped (43.1 KB): the features' calls to
  the core keep their names, which a minifier does not shorten.

### Fixed

- The label of a header that aligns to the end (a number's) went to its
  start, under its funnel: its row was reversed, and laid out from its
  end, which a reversed row has at the start. It is at the header's end,
  over its column's values, and the button keeps clear of it.

## [0.5.2] - 2026-10-23

### Fixed

- Rows that came empty made every column of learned type (`columns`, a
  query tool's results) a text column: a filter that left out every row,
  as a controlled grid's LiveView runs it, gave text columns. The filters
  were cast again as text: a decimal's went, and a number's became a text
  (`"200"`), which the next query sent as it was (PostgreSQL: "expected an
  integer"). A column keeps the type it had when the rows give it no value.

## [0.5.1] - 2026-10-23

On Issho 0.4.

### Changed

- Requires `{:issho, "~> 0.4.2"}`, with Issho (it is optional). Nothing
  changes in a grid's `issho`.

### Added

- Comments on a grid: Issho's comments (`Issho.Components.comments/1`)
  go on a cell or a range, and their marks on its cells, in each
  person's order. `Masu.Issho.label/2` says a grid's place in words
  ("City of Acme", "Name to City of Acme", "City of 3 rows"), for the
  panel's `:place` slot. The development app's `/comments` page. See the
  Collaboration guide.
- An agent that a person paused or stopped (`Issho.Work`) changes a grid
  no more: `select_cells`, `edit_cells`, `add_rows` and `delete_rows`
  say who and what to do, as Issho's tools do; `read_grid` still reads.
  An edit of many cells, one by one, stops at the cell it is on.

### Fixed

- A filter of a decimal column went when the grid's rows came again: a
  controlled grid's (`on_query`) answer, or new rows of the LiveView. The
  grid cast the filter's value again, and a decimal it had read was "not
  of the type decimal". A decimal is now taken as it is.
- A filter of a column of datetimes without a zone (`NaiveDateTime`s, a
  `timestamp` column) took the time typed in its menu as the browser's
  local time, and sent it in UTC: in Tokyo, 11:00 filtered on 02:00,
  though the cells show the times as they are written. The menu now sends
  them as typed for such a column; a column of `DateTime`s is unchanged.

## [0.5.0] - 2026-10-22

On Issho 0.3: following and agents' motion are the room's.

### Changed (breaking)

- Following is Issho's, a room's: the grid's avatars, the page's people
  list (`Issho.Components.people/1`) and every block of the room follow
  the same person; the grid shows their view, the canvas beside it their
  viewport, the text their caret. Scrolling or a key in the grid stops
  following in the room. A grid alone on a page follows within itself, as
  before (Issho's contract: `data-issho-following` and the
  `issho:following` event, with no import of Issho's script). See the
  upgrade guide.
- An agent goes to a cell and "types" there for `pace` before the value
  lands (it saved, then waited). It moves with `Issho.Motion`: only when
  someone of the room sees the grid, within a tool call's time budget;
  Issho's `travel: false` turns it off for every component.
- Marks glide and ranges sweep only for a person who wants motion, as the
  app says it (Issho's `data-issho-motion`), else the browser's setting.
- Requires `{:issho, "~> 0.3"}`.
- A grid joins with `issho`'s keys of every component (`Issho.Join`:
  `:instance`, `:document`, `:can`, `:people`, `:follow`, `:on_behalf_of`,
  `:claims`) and Masu's `:actor`: `issho={[instance: MyApp.Issho,
  document: "notebook:42/tasks"]}`. `:room` (and this version's `:grid`)
  and the `people` and `follow` attributes are removed, and raise with
  what to write instead; an unknown key raises.
- Masu's own keys of a person's state are under the grid's document, in
  `"blocks"`, not in a top-level `"masu"`; Issho owns the top-level keys.

### Added

- A write's author: `Masu.Write`'s `by`, an `Issho.Identity` on a grid
  with `issho` and for an agent's tools (the agent and whom it acts for),
  else the grid's `user`. Every callback (`on_update`, `on_create`,
  `on_delete`, `on_restore`) has it. `Masu.Sheet` takes `by`.
- Each saved write of a grid with `issho`, or of an agent, emits
  `"document.changed"` in the room (`Issho.Events`), with `by` and
  `data: %{op:, count:}`.
- A place in a grid has a documented format, part of the API
  (`Masu.Issho.Place`: a cell, a range, a pointer in a cell).

- A grid in a document (a Kotoba embed, see the "In a document" guide):
  `height="auto"`, as tall as its rows up to `max_height`; `wheel={:page}`,
  the wheel the page's until the person is in the grid; and `view`, the
  view it opens with (a saved view's params).
- A grid in a document of another room (a Kotoba document): `issho`
  takes its `document`, the same wherever it is, so that an embed and the grid's
  own page are one grid; on a page without Issho's pointers of its room,
  the grid shares its person's pointer over a cell and draws the
  others'; the copy of a grid in use says where its person is when a
  page has it twice; a grid that goes while its LiveView stays releases
  its room and takes its place with it; following from its avatars
  brings it into view. See the Collaboration guide.
- `Masu.Snapshot`: a grid's rows as they are now, in a view, as a
  Markdown or an HTML table, with how many more: a document exported with
  a grid in it, an email of it.
- An agent moves as a person does: it goes to each cell it edits, types
  in it (`pace`, the milliseconds it is there before the value lands),
  and selects a range as a drag does, pressed on its first cell; it
  selects rows a moment before it deletes them. Only where it goes is
  sent, and each browser draws the way. `config :masu, Masu.Issho,
  travel: false` for changes that land at once.
- A person's mark glides from cell to cell, longer the farther it goes
  (up to 400ms; `--masu-peer-glide` for a fixed time), and a range
  selected from a cell sweeps over its rows (`--masu-sweep`, 350ms); none
  of it with reduced motion.
- An agent's page pointer (`Issho.Components.pointers/1`) goes to its
  cell or range: the grid says where they are on the page.
- A person's page pointer over a cell is that cell (Issho's `point`: its
  row's id, its column's key, and where in it): each page draws it on
  that cell, whatever its order or scroll.
- `follow={false}`: a grid that takes no part in following, though its
  room's other blocks do (its avatars are not buttons, and its view stays
  its person's).
- The following banner and ring are in Issho's look of following, the
  same on every block of the page, restyled with Issho's
  `--issho-following-background`, `--issho-following-text` and
  `--issho-following-ring`.

### Fixed

- A shared grid that goes with its page (a navigation) throws no error:
  it let its room go through a LiveView already gone, whose promise no
  one awaited.

## [0.4.0] - 2026-10-21

### Changed (breaking)

- A grid's people are those of an Issho room: `issho` takes the place of
  `presence`, which raises and says what to give instead, and
  `Masu.Presence` is gone. Their cells, ranges, the cell each edits (a
  lock of the room) and their views (for following) are in the room's
  awareness, their colors Issho's; agents are among them. A grid without
  people, and `live` rows, are unchanged. Why, and the steps: the
  [Upgrading to 0.4](guides/upgrading.md) guide.

### Added

- `Masu.Issho`: agents in a grid, in an Issho room. `Masu.Issho.tools/0`
  reads a grid (in an order, filtered, searched), points at cells, edits
  cells, adds and deletes rows, the grid's way (`Masu.Sheet`), under the
  room's policy; the people see the agent's cell, and its rows flash in
  its color. An agent leaves a cell that someone is editing. Issho is an
  optional dependency.
- `people={false}` hides the avatars above the grid, for a page that
  lists its room's people itself.
- `Masu.Sheet`: a grid outside the LiveView, for a job, a test or an
  agent. Define a grid once as `data_grid`'s attributes
  (`<.data_grid {MyApp.Grids.tasks(@current_scope)} />`), and
  `Masu.Sheet.new/1` of the same reads its rows in an order, filtered and
  searched, and edits, adds and deletes them the grid's way: its
  changeset or `on_update`, its read-only rules, the compare of a cell
  changed meanwhile, its history, and its `live` topic, so the people
  who have the grid open see each change, in the color of who made it.
- `views_scope`: whose views a grid's views are, such as the account's
  id. The store keeps each scope's views apart (under `"orders@42"`), so
  a view shared in one account is not offered in another. Without it,
  every account has the grid's one pool, as before.
- `create_with`: fields every new row gets, as a map or keyword list
  (`create_with={[account_id: @current_scope.account.id]}`). The grid
  puts them on `struct` before the changeset and on the changeset after
  it, so params cannot change them, and a column of one of these fields
  neither takes a value on a new row nor edits.
- A `read` of your own gets `seek` with the block: for a jump, `seek.()`
  gives the sort values of the row before the block, as `after` does,
  from one query of the sort values only. `after_row || seek.()` reads a
  jump by key too (#53).

### Changed

- Every error the grid shows a person is a sentence, with a capital and a
  full stop: "The filter contains is for text, not integer.", "A query
  has more than 100 filters.", "There is no column \"nope\"." The errors
  of a query were lowercase, without one. They are also the errors of
  `Masu.Filter.cast/2` and `Masu.Query.new/2`, and of the `ArgumentError`
  of a grid whose own `filters` do not check: a test or a log that matches
  on their text needs the new text. A view that is no longer there says
  "This view no longer exists." (it said "no such view").

- What the browser sends is bound where it comes in, and refused past
  the bound, never cut short (see "What the browser may send" in the
  limits guide): a search, the search of a column's values and the text
  of a filter value of at most 1,000 bytes; at most 100 filters, 100 sorts
  and 100 groups in a query; at most 10,000 values in a list filter; a
  view's name of at most 255 bytes. Texts are counted in bytes.
  - An event answers with an error ("The search is longer than 1,000
    bytes."), and `Masu.Filter.cast/2` returns one, also for the grid's
    `filters`. The search of a column's values gets no values.
  - A link or a saved view drops a part past a bound, whole, and says so
    ("Part of this view no longer applies: search"), as it does for a
    column that is gone.

- The browser asks for no rows while rows it asked for have not come
  (for ten seconds at most), and then for the rows on the screen: a drag
  of the scrollbar no longer reads the rows of each place it paused at
  (#53).

- A jump of an Ecto source (the scrollbar dragged to rows the grid did
  not read) counts its `OFFSET` over the sort values of the rows, not
  the rows, from the nearest block before it that the grid read, and
  reads the block by key. With an index of the sort's columns, the
  database reads only the index: on PostgreSQL, a jump to the middle of
  ten million rows sorted by a date takes 0.2 s, not 12 s (minutes from
  the disk), and a jump of 1,000 rows on from a block 1 ms (#53).

### Fixed

- A view's name was checked against its bound of 255 bytes before it was
  trimmed, and saved trimmed: a name with spaces around it that took it
  past the bound was refused, and a name of only spaces past it was "too
  long" rather than "A view needs a name." The name is now trimmed first,
  and checked as it is saved.

- An id from the browser that the source could not read crashed the
  LiveView: an integer key sent `"abc"` raised `Ecto.Query.CastError`,
  and a text or UUID key with a NUL reached PostgreSQL, which rejects it.
  Every id an event names (an edit, paste, fill or undo, a delete, a
  selection, a row's action, detail, history or editor, a locate) is now
  checked before a source reads it: a number, or a text of at most 1,000
  bytes without a NUL; an Ecto source also casts it with its primary
  key's type and dumps it as its repo's adapter does (a `:binary_id` on
  PostgreSQL is a UUID). An id that does not check is a row not found. A
  source of your own may check its ids with the new optional callback
  `id?/2` of `Masu.Source`.
- A search, a text filter or the search of a column's values on a field
  that is not stored as text failed on PostgreSQL ("function
  lower(bigint) does not exist"): a column without a `type` is of text,
  and searched, whatever its field. An Ecto source now casts such a field
  to a text first, as each database casts (Ecto's `type/2`); a field of
  text stays `lower(field)`, which an index serves.
- A text with a NUL character crashed the LiveView: PostgreSQL rejects
  `\u0000` in the jsonb of the last view the grid keeps (`views`), and the
  grid matched on `:ok`. A NUL is now refused in the texts the browser
  sends to a query, a view or a cell: the search and the filters of a
  query, a link or a saved view (that part is dropped), the name of a
  view, and an edited or new cell (that cell is refused). The history
  asks its store only for the key of a column, and for a batch token the
  grid could have made.

## [0.3.0] - 2026-10-20

### Added

- Columns of related rows: a `:col`'s `field` may be a path of
  associations to a field, `field={[:customer, :name]}`. Its key is
  `"customer.name"` and its label "Customer name" unless given.
  - A path of `belongs_to` and `has_one` is one value. An Ecto source
    sorts, filters, searches, groups and totals by it, and counts its
    values for the enum menu. It adds a `LEFT JOIN` for each step only
    when one of these needs it, as a named binding (`:customer`,
    `:customer_company`), and uses a binding of that name that the
    query already has. The source preloads the paths' associations, so
    that the blocks are still read by key. A list follows the path in
    each row.
  - A path through a `has_many` or `many_to_many` is of many values: a
    list in each cell, shown apart ("gift, urgent"). It filters by "has
    any of" (`:in`), "has all of" (`:all_of`), "has none of"
    (`:none_of`), "has none" (`:is_nil`) and "has some" (`:not_nil`). An
    Ecto source runs these as subqueries of the related rows, never as
    joins, so that no row repeats and the count stays right. The search
    finds a row by any of its values. Such a column does not sort or
    group, and its enum menu counts the rows that have each value.
  - Columns of related rows are read-only. A selection of every row
    that filters by one is a query of the schema's rows by their keys,
    so that an `update_all` of it runs in PostgreSQL, MySQL and SQLite.
  - `Masu.Column.path?/1`, `Masu.Column.many?/1`, `Masu.Filter.many_ops/0`,
    the optional `c:Masu.Source.many?/2`, and the column attribute
    `many`.
  - The messages `op_in_many`, `op_all_of`, `op_none_of`,
    `op_is_nil_many`, `op_not_nil_many`, `filter_none` and
    `filter_some`.
- Groups that start open: `expanded={:all}` on a grouped grid opens
  every group at every level, and `expanded={[:status]}` the groups of
  the columns it names. A group the person closes stays closed, with a
  new filter or view too; the groups of a column grouped from the
  header menu start open by the same rule.

### Changed

- A source of your own without `c:Masu.Source.supports?/2` does not sort
  or filter by a column of a related row's field. Its other columns
  work as before. A controlled grid (`on_query`) sorts and filters by
  every column, paths too: its LiveView gets the path's key.
- An Ecto source names its query's first binding `:masu`, unless the
  query names it, so that the conditions on its fields and on joined
  fields take their types.

### Fixed

- A selected cell of a pinned column shows the selection on every second
  row and on chosen rows, not the row's tint (#45).
- A menu or panel that floats (a cell's menu, a header menu, a panel, the
  history, a cell editor) is as tall as its content, whatever height the
  app's styles give popovers: it no longer stretches to the bottom or the
  top of the window (#44).
- With no more groups, no cell of the first column keeps a group's
  indent (#49).

## [0.2.0] - 2026-10-19

### Changed

Breaking: every write goes through a `Masu.Write`, and the functions
that write rows your way take one.

- `on_update`, `on_create`, `on_delete` and `on_restore` take the place
  of `update`, `create`, `delete` and `restore`. Each is a function of a
  `Masu.Write`, a batch of changes (the cells of an edit, a paste or a
  fill; new rows; rows deleted or back) with the rows found again
  through the grid's query, the read-only and stale cells already
  refused, what the person did (`origin`), the grid's `scope`, and a
  `ref`. It gives the write back with a result for each change:
  `Masu.Write.each/2` (row by row), `all/2` (the whole batch, an
  `Ecto.Multi`), `ok/3`, `error/3`, `save/1` (the grid's own way, which
  your function may wrap), or `async/1` (later, by a job).
- `on_save` is gone, and `on_delete` writes: hear of what was saved in
  your `on_update` (`Masu.Write.saved/1`). A grid that has one of the
  old attributes raises, naming the new one.
- A grid with `on_delete` undoes a delete only with `on_restore`
  (`on_restore={&Masu.Write.save/1}` when the rows delete the grid's
  way).
- The items of `:header_action` and `:cell_action` get `scope` and `ref`
  in their context, and their results are said: `{:ok, message}`,
  `{:error, message}`, `:async`.
- An Ecto source's rows of equal values in a descending sort come in
  the descending order of the primary key, not the ascending: the
  primary key that ends the order goes the way of the last sort, so that
  an index of the sorted column and the primary key reads the rows in
  their order, forwards or backwards.
- A search of a grid of rows (`Masu.Source.List`) makes a text of each
  row once, at the first search, and matches in it: each search after it
  is about eighteen times faster (a hundred thousand rows: from 491 ms to
  27 ms). The texts stay until the rows or the searchable columns change.

### Added

- The history of cells (`history` on a grid, `Masu.History`): who
  changed a cell, when, from what to what, and what they did (typed,
  pasted, filled, restored); new and deleted rows too. Every write of the
  grid that saves is recorded, the grid's own and an `on_update`'s;
  `Masu.Write.record/3` records in a transaction of the app's own, and
  `Masu.History.record/2` what the app changes another way. A batch is
  one write (a paste).
- Stores: `Masu.History.Ecto` (`use Masu.History.Ecto, repo: ...`), a
  table of any Ecto repo, PostgreSQL, MySQL or SQLite, with
  `mix masu.gen.history` for its migration: in the grid's own repo, a
  change and its record are saved in one transaction (a record that
  fails takes the change back); in a repo of its own (a SQLite file next
  to a PostgreSQL app), the change is recorded after it saves.
  `Masu.History.Memory` for development and tests; a store of your own
  over your audit.
- The history of a cell or a row in a popover, from the cell's menu or
  Ctrl+Alt+H: Restore a value, Undo all of a batch (someone's paste), as
  edits through the changeset, refused for a cell that changed since,
  recorded as restores.
- `Masu.Test.history/4` and `restore/3`.
- Editors of your own (`editor` on a `:col`), with no dependency of
  Masu's on any editor:
  - a function component of a form field (`@field`, named
    `cell[value]`), which the server renders in a popover under the
    cell, with the row as the source reads it now: what its form posts
    is the new value, text or params (`from` and `to`), cast by the
    changeset. A Kotoba editor or a Koyomi picker is one component, and
    a `Phoenix.LiveComponent` in it searches the server. Ctrl+Enter or a
    submit saves, a click away saves, Escape lets it go (after the
    editor's own Escape); an edit carries a version of the value it
    opened with, and a cell changed meanwhile is refused;
  - an editor of the browser's, by name, given to `configure({editors})`
    in the app's JavaScript, with options of the column
    (`editor={{"score", %{max: 100}}}`): it draws in a box over the
    cell, and the grid keeps Enter, Tab, Escape (unless the editor
    prevents them) and a click away.

  Either way the grid keeps undo and redo, the errors on the cell, "Ada
  is editing", and paste, fill down and Delete through the column's type.
- `Masu.Wire`: a protocol for values of your own types, as a cell shows,
  copies and exports them (a rich text's text, a range of days).
- `Masu.Test.open_editor/4` and `close_editor/2`; `edit/4` takes the
  params of an editor's form.
- Guides: [Editors of your own](editors.md), and how to plug in
  [Kotoba](kotoba.md) (rich text in cells) and [Koyomi](koyomi.md)
  (pickers in cells, and a picker above the grid that filters it through
  its URL), with no dependency of Masu's on either.
- `:action`: actions on the rows selected (`selectable`), buttons of
  the bar above the grid, with the selection as `Masu.Live.selected/1`
  gives it (`{:query, query}` for every row of the query), `confirm`
  ("Delete {count} orders?"), `danger` and `keep_selection`; the grid
  reads its rows again when one is done, and clears the selection.
- `:row_action`: actions on a row, in a menu at the end of each row
  (and in the menu of its cells: a right click, the context-menu key,
  Shift+F10), as a function, an event to the LiveView, or a link
  (`navigate`, `patch`); `show` and `disabled` by the row, read again on
  the server; `editors` in the context, the people editing the row.
- `scope` on the grid: in every write and every action's context.
- `Masu.reply/2` and `Masu.progress/3`: a write or an action that goes
  on in a job (`Masu.Write.async/1`, `:async`) says how far it is and
  how it went, from any process; the grid shows it running, its
  progress, and its result.
- `Masu.Test.action/3`, `row_action/4`, `actions/3` (`:selection`) and
  `actions/4` (`:row`); `origin:` for `edit/4`, `add_rows/4` and
  `delete_rows/4`, which give `:async` for a write saved later.
- Messages: `row_actions`, `action_done`, `action_running`,
  `action_progress`, `saving_later`, `deleting_later`.
- `read` on a grid of an Ecto source: a read of a block of rows of the
  app's own, a function of the query (with the grid's filters, search
  and order) and `%{offset:, limit:, order:, after:}`, `after` the sort
  values of the last row of the block before. The rest of the source
  (filters, count, values, selection, export) stays Masu's.
- `count_limit` on a grid of an Ecto source: the count stops after the
  row past the limit, and the grid says it has at least that many
  ("10,000+"), then counts every row when the person goes to the end. A
  `count(*)` of a large table reads every row; with more rows than the
  limit, the count is done after the limit. A query of fewer rows than
  the limit is still read whole, and PostgreSQL then reads it in one
  process, not in parallel: it can take longer than a `count(*)`.

### Fixed

- A grid that reads in tasks (`load={:async}`) reads the block after a
  block by key, as an inline grid does, not by `OFFSET`.
- An Ecto source keeps the keys of its blocks in its state, not in the
  LiveView's process dictionary, where the keys of every query before
  stayed for as long as the LiveView lived.
- With an adapter whose order of `NULL`s Masu does not know (not
  PostgreSQL, MySQL, SQLite or SQL Server), a sort other than the primary
  key's is read by `OFFSET`: by key, it took the order of SQLite's and
  could skip or repeat rows.
- The README and the guides said each block of an Ecto source is a
  `LIMIT` and `OFFSET`.
- A grid taller than a browser draws an element scrolls to its last
  row: Firefox did not scroll a grid of more than about 500,000 rows (of
  36 pixels) at all, and Chromium and Safari did not show the rows after
  about 930,000. The scroll box of such a grid is 10,000,000 pixels
  tall; the scrollbar dragged moves in proportion, and a wheel and the
  keys move by their own pixels.
- An undo (or a redo) right after an edit, before the server answered
  it, did nothing: it waits for the answer now, and takes the edit back.
- In Safari, the Columns panel lost the focus at each click on a
  control: the control keeps it, as in the other browsers.

## [0.1.2] - 2026-10-18

### Added

- A cell's menu compares text (Greater than, Less than, At least, At
  most) when the column's filters, or its source's, name the
  comparisons: a SQL client's text compares in its order.

## [0.1.1] - 2026-10-18

### Added

- A column's own filters: `filters` on a `:col` (or a column of
  `columns`) limits its filters to those operators, as a source's
  `filters/2` does, for a controlled grid whose app runs fewer.

- `raw` on a column shows its values as the source gives them (`12345`,
  `2026-10-18T08:00:00Z`), not formatted for the locale; the column
  keeps its type for its alignment, filters and editor.
- A cell's menu filters by the cell's value as a bound (Greater than,
  Less than, At least, At most; After, Before... for dates and times) in
  a column of numbers or dates, and by a part (Contains) in a column of
  text, as far as the column's filters go.
- `Masu.Messages.defaults/0`: every word of a grid, the browser's
  included, so that an app in another language can translate them all
  and test that none is left in English. The words go to the browser
  apart from the grid's config (`data-masu-messages`), once.

### Fixed

- `height` is the whole grid's: the rows take the room the toolbar, the
  chips and the status line leave. A percentage (`height="100%"`) fills
  the grid's container; the rows had no height at all.
- Menus and panels float above the page (the popover top layer), under
  their header or cell, or over it when there is more room there, and
  inside the viewport: a box around the grid that hides its overflow no
  longer cuts them.
- Masu's buttons, fields and selects keep their look next to an app's
  form styles (Tailwind's preflight and `@tailwindcss/forms`, which came
  later and won).
- Tokens made of others (`--masu-active`, `--masu-focus-ring`,
  `--masu-header-background`...) follow a grid's own `--masu-accent`,
  `--masu-surface`...: set on a class of the grid, they stayed Masu's
  blue.
- A controlled grid (`on_query`) offers no "Group by": its app runs the
  query.

## [0.1.0] - 2026-10-17

### Added

- `Masu.Components.data_grid/1`: a virtualized data grid for LiveView,
  with a `:col` slot for each column and cells rendered by HEEx.
- Sources: a list of rows (`Masu.Source.List`) and an Ecto query or
  schema (`Masu.Source.Ecto`), which sorts, filters, searches and counts
  in the database. `Masu.Source` for your own.
- Sorting by several columns, filters (`Masu.Filter`) and a search box.
- Columns that resize, and fit their content on a double click.
- Keyboard navigation of the cells, a selected range, and copy of the
  range as tab-separated text that spreadsheets paste.
- Cells formatted for the locale: numbers, decimals, dates and times.
- `Masu.Test`, to read and query a grid in `Phoenix.LiveViewTest`.
- A filter menu on each column (the funnel of its header, or Alt+↓):
  the filters of the column's type, an enum's values with their counts
  (`c:Masu.Source.values/5`: a list in the LiveView, one `GROUP BY` for
  Ecto), and the filters as chips above the grid. `Masu.Test.values/4`.
- Editing: `changeset` (or `update`) on the grid, `editable={false}` on a
  column. A cell edits on a key typed, Enter, F2 or a double click, with
  an editor for its type; the edits of a row are saved together, through
  the changeset and the repo (or applied to a row of a list), and the
  changeset's errors go on their cells. Paste of tab-separated text, fill
  down, Delete, undo and redo. A row is found again through the grid's
  own query (`c:Masu.Source.fetch/3`). `Masu.Test.edit/3`.
- Read-only rows and cells by their state: `row_editable` on the grid,
  and `editable` as a function of the row on a column. The server
  refuses an edit of a read-only cell (with the row it finds again) and
  saves the row's other edits; the browser gets each row's read-only
  cells, which open no editor and have `aria-readonly`.
- Edits of a cell that changed meanwhile are refused: each edit carries
  the value the person saw (when the editor opened), compared on the
  server by the column's type, a compare-and-swap of each cell; the cell
  then shows its value now. An Ecto source compares and saves each row in
  a transaction that locks it (`SELECT ... FOR UPDATE` on PostgreSQL and
  MySQL), and takes `optimistic_lock/3`'s `Ecto.StaleEntryError` as the
  same refusal. `Masu.Test.edit/3` takes `{id, key, value, before}`.
- `on_save`: a function called with the saved rows, in the LiveView's
  process, for an audit, a flash or the LiveView's own assigns.
- Collaboration (`Masu.Presence`): `presence` and `user` on a grid with
  `live`. The others who have the grid open show as avatars (a `:person`
  slot draws them), each one's cell as a line in their color with their
  name (by row id and column key, whatever each person's order and
  filters), a cell someone types in as "Ada is typing" and locked for the
  others, and the rows they save flash in their color. `configure/1` of
  the bundle draws the labels; CSS for the rest.
- Following a person: a click on their avatar shows their view (order,
  filters, search) and keeps their place in it on the screen; Stop brings
  back one's own view and place, and taking over (scrolling, clicking,
  typing) keeps theirs, with "Back to your view". `share_view={false}`
  keeps a person's view to themselves; whoever follows them stays in
  their own order, and the grid finds the person's row in it
  (`c:Masu.Source.locate/3`, a `row_number()` window for Ecto;
  `Masu.Test.place/3`).
  Each person's selected range shows to the others in their color. A
  refused edit names who changed the cell: "Changed by Ada meanwhile."
- Export (`Masu.Export`): `export` adds an Export menu, CSV and XLSX
  (written with no library, zipped as it is written) of every row of the
  query, downloaded by the browser from the route `masu_exports` mounts
  (`Masu.Router`) as the rows are read, with a link good once, for five
  minutes; nothing of the file goes over the LiveView's socket.
  `Masu.Live.export/1` gives an app the export to write or stream
  anywhere. Selection: `selectable`
  boxes, Shift ranges, Ctrl and Space, and every row of the query at
  once, which `Masu.Live.selected/1` gives as an `Ecto.Query`;
  `on_select`. An Ecto source reads the next block by key, not by
  `OFFSET`. `Masu.Test.export/4`, `export_url/3`, `select/4`, `select_all/3` and
  `selection/2`.
- Theming to match an app's own look: tokens for the grid's box
  (`--masu-grid-border`, `-radius`, `-shadow`), the header (its height,
  type, padding, border or shadow, hover), the rows (background, every
  second row's, the line under each) and a cell's hover; `.masu-row-even`
  on every second row; a row's hover over its own background; `density`
  (`:compact`, `:normal`, `:comfortable`); a dark palette under a `dark`
  class or `data-theme="dark"`; Tailwind in the Theming guide, and Lotus
  Web's results table as its worked example, compared pixel for pixel in
  e2e.
- Slow sources: `load={:async}` reads a source in a task (a new
  query's first rows and count, a block, a page, a count): the LiveView
  answers meanwhile, the grid says "Running…" with Stop and keeps its
  rows, `timeout` stops a long read, and a source's error (a raise, or
  `{:error, reason}` from `rows/3`, `Masu.Source.Error`) shows in the
  grid with Run again. `load={:inline}`, the default, reads as before.
- Controlled grids: with `on_query`, the LiveView runs its own queries;
  the grid shows the `rows` it is given (with `total` and `loading`),
  and reports the person's sort, filter, search, page and view. One sort
  at a time with `multi_sort={false}`. `Masu.Test` waits for tasks.
- Columns not known in advance, as a query tool's results: rows as
  lists of values (`field={2}` reads a place of a list), `type={:auto}`
  (the type of the values of the first block of rows), `columns` on a
  grid with no `:col` slots, and `c:Masu.Source.columns/1`. Rows of
  other columns keep the person's view of the columns there still and
  name the rest (`Masu.Query`'s sorts, filters and groups too); an
  order the person did not change follows the new columns.
  `Masu.Column.infer/1`.
- The filters a source runs: the optional `c:Masu.Source.filters/2`
  gives the operators a source filters a column by. The filter menu,
  the chips, the menu of a cell, a view's params and `Masu.Test.filter/3`
  offer only those, and the server refuses the others (a link's or a
  saved view's are dropped and named). Masu runs "between" as "at least"
  and "at most" for a source without it; an enum without "is one of"
  filters by one of its values. `Masu.Filter.ops/1`. The Sources guide
  maps each operator to SQL.
- Menus of your own (`Masu.Action`): `:header_action` items in each
  column's header menu, and a menu on each cell, opened by a right
  click, the context-menu key or Shift+F10, as an ARIA menu. `cell_menu`
  gives it copy, copy with headers, filter by the value, exclude it,
  empty and not empty, and sort; `:cell_action` adds the app's items.
  An item runs a function (`on_action`), sends the LiveView `{Masu.Action,
  name, context}` (`event`), runs a JS command in the browser (`js`, after
  a `masu:action` event), or opens a panel of its content under the header
  or the cell; for some columns (`columns`, `types`, `show`). The context
  has the column, and the cell's row and value read from the source, and
  the range it is in. The server runs only the items the grid offers.
  `Masu.Test.actions/4`, `header_action/4`, `cell_action/6` and
  `close_panel/2`.
- Row ids: `row_id={:position}` knows rows by their place in the query,
  for a read-only grid of rows with no id; a grid that needs no ids does
  so for a row without one. A grid whose features need ids that stay
  (editing, new and deleted rows, selection, live rows, presence,
  details, trees) refuses `:position`, and a row without an id, saying
  which.
- Reads: `block_size` (100) is the rows of one read as the person
  scrolls, and `max_block` (500) the most of one read; a page
  (`paging={:pages}`) is one read, up to `max_block`, and the next page is
  not read ahead. The first render reads one block, which the browser
  keeps.
- Counts and pages: `count={:deferred}` counts after the first rows, in
  a task; `count={:none}` counts when the person goes to the end. A
  source's `count/1` may give `{:at_least, n}` or `:unknown`, and
  `rows/3` `{rows, count}`: the grid scrolls over the rows it read and
  grows, `aria-rowcount="-1"`, "1,000+ rows". An Ecto source counts only
  when asked. `paging={:pages}` and `page_size`: a page at a time, with a
  pager, the page in the view and the URL. `Masu.Test.count/2`,
  `ask_count/2`, `page/3`.
- New and deleted rows: `create` (or `changeset` with `struct`) adds a
  "+ New row" row, Ctrl+Enter and Insert; a new row is a draft until the
  person leaves it, with its errors on its cells; a paste past the last
  row offers to add the rest. `deletable` (`Repo.delete`, or out of a
  list; a function of the row says which) or `delete` deletes rows with
  Ctrl+Delete or from the selection bar, after a question; Ctrl+Z brings
  them back (`restore` for a grid with `delete`). New and deleted rows go
  to the grid's topic; `on_delete`. `Masu.Test.add_rows/3`,
  `delete_rows/3`, `restore_rows/3`.
- Editors: an enum edits with a list of its values (`values`, or a
  schema's `Ecto.Enum` field's), which narrows as the person types;
  `decimals` on a number column; a value the server would refuse (text
  in a number, a day that is not one) is caught in the browser.
- Groups and totals (`Masu.Group`): `aggregate` on a column (`:sum`,
  `:avg`, `:min`, `:max`, `:count`, `:count_distinct`, or a function of
  a list's rows) and a row of totals at the bottom; `group_by` and
  "Group by" in the header menu, with a chip, as part of the view. Groups
  open on groups and rows as a tree does, with their counts and totals,
  and sort by value or by a total. An Ecto source groups and totals in
  the database, a `GROUP BY` for each level that shows, and reads an open
  group's rows a block at a time. `Masu.Test.group/3`, `groups/3` and
  `totals/2`. A filter on a hidden column keeps its chip.
- Trees and details (`Masu.Tree`): `tree={:parent_id}` shows rows in a
  tree (a `treegrid`), opened and closed with their arrow, →, ← and `*`,
  as one list of the rows that are open: a tree of twenty thousand rows
  scrolls as a list does. Siblings sort among themselves, and a filter
  keeps its matches and their ancestors. Both sources take it; the Ecto
  source reads the children of a row when it opens. A `:detail` slot
  opens a row on anything rendered by the server under it (another
  `data_grid` too), with Alt+→/←, Alt+↓ into it and Escape out.
  `Masu.Test.nodes/3`, `expand/3`, `collapse/3`, `expand_siblings/3` and
  `detail/4`.
- Layout and views: `pinned={:start | :end}` and `hidden` on a `:col`;
  columns that move by dragging their header (or Alt+Shift+←/→), and a
  columns menu (`columns_menu`) to show, hide, pin and move them. A
  person's view (`Masu.View`: order, filters, search and `Masu.Layout`)
  goes into the URL with `url`, and into a store with `views`
  (`Masu.Views`: `Masu.Views.Ecto` with `mix masu.gen.views`, and
  `Masu.Views.Memory`): their last view comes back, and a views menu
  saves named views, shared or not. A view's parts that no longer apply
  are dropped, and the grid says so. `Masu.Test.view/2`,
  `apply_view/3` and `layout/3`.
- Live rows: `live={{pubsub, topic}}`, the `Masu.Live` hook, and
  `Masu.Live.broadcast_upsert/3` and `broadcast_delete/3`. A grid takes
  the changes of its topic into the person's query, a batch at a time;
  the browser keeps the scroll and the rows on the screen until the new
  ones come, and flashes the changed rows.
