A grid is a view of your app's rows; a document is text people write. A grid in a document (a Kotoba embed) is the rows themselves, live, among the text: people sort, filter and edit them where they read about them, and the document keeps only which grid and which view.

A table of the document (Kotoba's own) is the other thing: a few rows that exist only because of the document, with no source. If the rows would be there without the document, it is a grid.

The grid in a page

Three attributes make a grid a part of a page that scrolls:

<.data_grid {MyApp.Grids.tasks(@scope)} height="auto" max_height="24rem" wheel={:page} view={@view_params} />
  • height="auto": the grid is as tall as its rows, up to max_height (32rem unless given); past it, the rows scroll. Five open tasks take five rows of the page, not a box of 32rem.
  • wheel={:page}: the wheel scrolls the page past the grid, until the person is in it (a click on it, Tab); then the wheel is the grid's. A grid alone on a page keeps the wheel (:grid, the default).
  • view: the view the grid opens with, as a view's params (see Layout and views): its order, filters, search and columns. A saved view's params (Masu.Views) are such a view.

A grid in a Kotoba document

Kotoba keeps an embed's kind and ref, and your LiveView renders it (Embeds). For a grid, a good ref is a saved view: "Unpaid invoices over €1,000" is what a document talks about, more than the invoices table.

<.kotoba field={@form[:body]} id="doc" embeds />

<.kotoba_embeds for="doc" label="Grid" icon="grid" readonly={@reading}>
  <:embed :let={embed}>
    <% view = MyApp.GridViews.get(embed.ref) %>
    <.data_grid
      :if={view}
      {MyApp.Grids.for_view(view, @scope)}
      id={"grid-" <> embed.id}
      view={view.params}
      height={if embed.full, do: "100%", else: "auto"}
      wheel={if embed.full, do: :grid, else: :page}
      changeset={if !embed.readonly, do: &MyApp.Tasks.Task.changeset/2}
    />
  </:embed>
  <:picker>
    <.kotoba_embed_option :for={v <- @views} phx-click="insert_grid" phx-value-id={v.id} title={v.name} />
  </:picker>
</.kotoba_embeds>
/* A grid takes the height of its rows; a canvas keeps the embed's. */
.kotoba-embed[data-kind="grid"] .kotoba-embed-slot { height: auto; }
  • Give the grid an id of the embed's ("grid-" <> embed.id): the same view can be in a document twice.
  • Check that the person may see the view the ref names, as for any of your pages: a document's refs are the person's input.
  • Full screen (embed.full) is the whole screen: the grid takes it all (height="100%") and the wheel.
  • A page that reads the document (embed.readonly) shows the grid with no changeset: people sort and filter it, and change nothing.
  • Kotoba keeps the keys of an embed in it: the arrows, Enter and ⌘C are the grid's.

Markdown and HTML

A document exported to Markdown has its grid as a table of its rows, as they are: a snapshot (Masu.Snapshot), which Markdown viewers draw, and which Kotoba reads back as the grid (it keeps the embed's kind and ref around it).

def render(%{kind: "grid", ref: ref}, format) when format in [:markdown, :html] do
  view = MyApp.GridViews.get(ref)
  sheet = Masu.Sheet.new(MyApp.Grids.for_view(view, scope))

  case format do
    :markdown -> Masu.Snapshot.markdown(sheet, view: view.params, rows: 20)
    :html -> Masu.Snapshot.html(sheet, view: view.params, rows: 20)
  end
end
<!-- kotoba:embed {"kind":"grid","ref":"12","title":"Open tasks"} -->

| Task | Due | Done |
| :--- | :--- | :---: |
| Write the guide | 2026-11-02 | false |

_And 37 more rows._

<!-- /kotoba:embed -->

The snapshot is the view's rows (its order, filters, search, and the columns that show), at most rows of them (20 unless given), and a line of how many more. A cell is its value as text; a cell of your own HEEx is its column's value. An HTML snapshot is a table.masu-snapshot, for a page or an email of the document.

Collaboration

A grid with issho is shared in its own room, whatever the document's: its people are that room's, wherever they are. Give it its document, the same as on the grid's own page, so that the two are one grid, and a reader's can:

<.data_grid
  ...
  issho={[instance: MyApp.Issho, document: "tasks:#{@account.id}/tasks", can: (embed.readonly && :read) || :write]}
  user={@current_user}
/>

Someone reading the document and someone on the grid's page see each other's cells and pointers. See Collaboration.