# `<tosi-virta>` — the board

The 2D board: one filter, tasks grouped into lanes by the value of one tag key
(`status` by default — the empty lane is the **inbox**), a card view with the
progressively-disclosed transcript, and keyboard navigation. Every view is a
composition of the one primitive: tag filter plus sort.

Point it at a [store](/store/) and it binds. The store below is seeded with a
few hundred tasks so the board is exercised at a realistic size; the demo
identity writes to an in-memory log, so play freely.

```html
<tosi-virta lane="status" color="sky" style="width: 100%; height: 100%"></tosi-virta>
```
```js
import { MemoryLog, VirtaStore, seedEvents } from 'tosijs-virta'

const store = new VirtaStore({ log: new MemoryLog(), identity: 'you' })
await store.load()
await store.import(seedEvents({ tasks: 300, seed: 3 }))
preview.querySelector('tosi-virta').store = store
```
```test
const board = preview.querySelector('tosi-virta')
await new Promise((r) => setTimeout(r, 100))
// a lane builds only the cards on screen, so count what the lanes hold
const held = () =>
  [...board.querySelectorAll('.lane h3 .lane-count')].reduce(
    (n, el) => n + parseInt(el.textContent, 10),
    0
  )
const all = held()
test('the board renders lanes and cards', () => {
  expect(board.querySelectorAll('.lane').length).toBeGreaterThan(2)
  expect(board.querySelectorAll('.card').length).toBeGreaterThan(0)
  expect(all).toBeGreaterThan(50)
})
test('typing a filter narrows the board', async () => {
  const input = board.querySelector('input[part="filter"]')
  input.value = 'project:tosijs-ui'
  input.dispatchEvent(new Event('change', { bubbles: true }))
  await new Promise((r) => setTimeout(r, 100))
  expect(board.querySelectorAll('.card').length).toBeGreaterThan(0)
  expect(held()).toBeGreaterThan(0)
  expect(held()).toBeLessThan(all)
})
```

## The card window

The title, the body rendered as markdown, and an editable tag list: add a
tag by typing it or picking from every tag on the board, remove one with its
×. Each edit is one `tag`/`untag` event, so the transcript shows who changed
what. Below that: move, close/reopen, work on this, and the transcript.

## Working on it

Anyone — a person or an agent — can say what they are working on: the
`work` verb puts a `working:<identity>` tag on the task and takes it off
everything else that identity was working on, since you work on one thing
at a time. Say `also: true` to keep the others. Working and in-progress
imply each other: working on a task that isn't started moves it to
in-progress, moving a card into in-progress (a drag, or the card's
**status** select) puts you on it, and moving it out takes everyone off.
So the card window needs no work button, and no close button either:
status **done** closes a task, and moving it out of done reopens it
(board #173). The card shows a small jiggling avatar per worker; `working:owner`
is an ordinary tag, so `working:owner` is a filter and `working` is a board
of everything in progress.

## Keys

| key | action |
| --- | --- |
| `j` / `k` or `↓` / `↑` | next / previous card |
| `Enter` | open the selected card |
| `Escape` | close the card view |
| `/` | focus the filter |

## Attributes

- `lane` — the tag key that becomes the lanes (default `status`). A task
  with no status is in the inbox — the implicit `status:inbox`; moving it
  out adds a status, moving it back removes it.
- `filter` — the initial filter text; the input writes it back.
- `color` — a [palette](/palette/) name for the board background (default
  `sky`). Boards are saved filters, so each one gets its own color.
- `hide` — lane values to leave out, space-separated (default `freezer
  obsolete rejected`); the toggle row under the filter writes it
  back.
- `show` — a project board's view toggles, space-separated: `no-own`
  leaves out the project's own tasks, `no-others` leaves out related tasks
  (owned by another project, upstream, downstream or blocking this one),
  and `stakeholders` keeps only own tasks that another project cares about.
  The "show" control (own | related | archived, #765) and the filter menu
  write it. Own and related can't both be off. Archived tasks are the filter's business:
  hidden unless the filter says `archived` (only those) or `archived:any`
  (those too), which is the filter menu's "archived tasks" submenu.
- `db` — with no `store` set, open (or create) an IndexedDB log with this
  name as the base (on a host board the name joins the host and is keyed by
  the credential); `snapshot` — a URL of a JSON list of events to fill an
  empty log with (the demo board's real projects); `seed` — else seed that
  many generated tasks if the log is empty;
  `identity` — who the board writes as (default `you`). Together they make
  a board that needs no code: see the [full-screen board](/board/).

## Lanes

`status:` lanes come in a fixed order — inbox, freezer, backlog, ready,
in-progress, review, then the endings: done, obsolete, rejected. Every one is shown even when empty, so there is always somewhere
to drop. The inbox is the implicit status: no tag
is written for it, `status:inbox` in a filter finds it, and dropping a card
there removes its status. Other values a board meets follow
alphabetically. The row of toggles under the filter switches lanes on and
off. Freezer starts off, and so do obsolete and rejected, whose tasks are
archived anyway.

## Overview

The toolbar's view switch (the tiles icon) swaps the lanes for one tile per project:
its icon, color or background, version, a one-line description, open /
in-progress / blocked counts, the latest activity, and links to its site,
npm package, and repo. Click a tile to open that project's board; the
settings button opens its settings — only what cannot be derived: the
repo, the site, and the look (icon, color, background — each a text field,
with a file or color picker beside it that only fills the field in).
Description, package, and version come from the repo and the tasks.
Settings are stored as `project` events, so a project has a transcript like
a task does. Unset projects get a color and initials from their name. A
**retired** project (the settings panel's retire) leaves the slice menu and
trails the overview, muted; its history is untouched, and unretire brings it
back. `view` is part of the URL state. A tile also counts
what the project is **waiting on elsewhere**: open tasks moved to other
projects that were marked as blocking or delaying it. Tasks with no project
get a derived **no project** tile, first, only while any exist — and "no
project" joins the slice menu for the same span. Nothing is written for
it: `project:none` is the implicit value, as `status:inbox` is for status.

**Import from GitHub** (the account menu's Import from GitHub…, or a
project's "refresh from GitHub"): point it at a repo and its description,
homepage, package, version, and issues arrive — see [GitHub import](/github/).
Repeating it only adds what is new.

## Connecting to a host

```
<tosi-virta host="https://us-central1-<project>.cloudfunctions.net" poll="10" hash></tosi-virta>
```

A board with a `host` syncs with a [service-compris](/remote/) host. The
avatar at the toolbar's right end is the account: signed out it offers
**Sign in with Google**, **Sign in with email…**, and, for agents, **Use an
agent token…**; signed in it shows your initials, and its menu has who you
are and **sign out**. Every write is then that person — the host resolves
their roles from the ID token on each request. The Firebase SDK and the
project's public web config load on demand from the host's own project
(see [sign-in](/firebase-auth/)); nothing is bundled, and the session
persists in this browser until sign-out. An agent token is kept in this
browser's storage, never in markup or the URL. The `auth` attribute lists
what the menu offers (`google password token` by default); `docs` adds a
Docs item that goes there, for a page without the site header (the hosted
board's). The board loads
from its local replica — an IndexedDB named for the host, or `db` as the
name's base, always keyed by the credential (`replicaDbName`) — and pulls
the delta; writes go to the host and come back through the replica; other
writers' events arrive by polling the delta every `poll` seconds (10 by
default), when the tab comes back into view, and after each write. The
toolbar's right edge says `live`, or `offline: <reason>`; who you are is the
avatar's tooltip. A replica only asks for what comes after its cursor, so a
host whose log was reset needs the account menu's **Reload from server**: it
drops the local copy and loads the log again, keeping the credential.
`seed` is ignored on a host board — a shared host is not a demo. (The
sign-in UI belongs upstream — tosijs-ui#177 asks for it; this is the interim.)

## Size

Teams that estimate write `size:<points>` on the Fibonacci scale
(1 2 3 5 8 13 21; stop wherever the team stops — the top value means "or
more"). It is a tag like any other: filter on it, color it, pick it from
the menu. When any card in a lane carries a size the lane header shows the
lane's points beside its count, and the sort menu offers biggest / smallest
first, unsized last. `size:8` is orange and `size:13` and up red — big
tasks call themselves out as candidates for breaking down. Nothing asks
for a size: an untagged task is a normal task, not a lapse.

## Continue

The card window's **continue** button is "yes, and": a new task that
carries on from this one, linked both ways (`continues:#n` on the new,
`continued-by:#m` on the old, plus a comment) and inheriting only the
obvious tags — project, kind, area, priority — never status, size, or who is
working. It opens with its title selected so the first keystrokes rename it.
The `continue` verb does the same for agents.

## The toolbar: view, and the slice menu

The top line reads left to right: a **view** switch (tiles = overview,
columns = board), then one **slice** menu — the mode and the slice of it:
**project** › all projects or one project, **status** › one status,
**owner** › one status. Each mode has its own icon on its leaves (a project
shows its own), so the button reads compactly: a project's icon and name,
or ⚑ `ready`. In project mode the lanes are statuses: the default board. In
status mode they are projects — "what is in progress, everywhere". In owner
mode they are owners — who is carrying what, the view for balancing a team.
Only project mode has "all". One menu rather than two keeps the toolbar
narrow: the slice and the tag list give way on a small screen, so the view
switch and the account button always stay on it. Dragging between lanes
moves along whichever axis is showing; between project lanes it asks the
question the card's project select does (below). `axis` and `scope` are
attributes and URL state; `lane` still accepts any other tag key directly.

**Find** (board #4048), at the toolbar's end in every view once signed
in, jumps rather than filters: click it, press ⌘⇧K (Ctrl Shift K), or
press `/` where the filter isn't shown, and type a card number (`4001` or `#4001`, any project),
words from a title or a body, or a project's name. Choosing a card opens
it where it lives; a project opens its board; the last row filters the
board by the words instead. On the board (where the filter shows), the
board filters as you type, throttled: closing find keeps that filter,
choosing a card or a project puts the board's own back. A card with only
some of the words still shows, below the ones with all of them. A link does the same from outside:
`/host/?4001` (on virta's own site, `virta.tosijs.net/?4001` or `/4001`)
opens #4001 the way `#?virta.task=4001` does.

## Moving a task to another project

A task has one owner: one `project:`. The card window's **project** select
(or a drag between project lanes) hands it upstream or downstream — "this
should not be done here". No question is asked: the project it leaves stays
**interested** (it still sees the task on its board). Delete the
`interested:` tag to end that, or change it to `blocks:` / `delays:` when
the old project is waiting on it (the `move` verb takes the relation
directly). The move is one commit: `project:` changes, `from:<old>` keeps
the provenance, `interested:<old>` records the old project's interest, and
a comment says so. The old project's board shows the task,
marked as the new owner's; its tile counts what it is waiting on;
`blocks:<name>` as a filter lists it. A support ticket handed to engineering
is exactly this: two tags change, nothing is cloned, and the transcript
keeps the whole history.

## Sort, state, and the URL

Cards sort by **priority** by default: `priority:critical` … `low`, then
unprioritized; blocked tasks first within a priority; then oldest in the
lane first. The other sorts: oldest / newest in lane (age is time since the
last `status:` change), recently updated, recently created, number, title.

`filter`, `project`, `sort`, and `hide` are attributes, so a board's view can
be written in markup. With the boolean `hash` attribute the same four are
mirrored into the URL hash (namespace `virta`, defaults written as absent),
plus `task=<num>` for the open card — so a URL can point at a card. It runs
both ways: every control writes the hash, and editing the hash (or the back
button) moves every control.

Tag colors come from one [registry](/tag-colors/): chips on cards, chips in
the card window, and the dots in the filter menu all agree.

## The filter

Two controls that make one filter. The search box takes free text — each
word must appear in the title or body — and also accepts terms typed
directly (`project:tosijs-ui`, `-status`, `#38`). The tag list beside it holds
the terms; its menu is hierarchical: every tag key as a submenu of its
values (plus "any" and "no"), the bare tags, and a closed-tasks switch. The
`filter` attribute always holds the composed result, so a board can be
saved as one string.

Drag a card to another column to move it (one `setTag`, the same verb the
"move to" select uses). Drag it out of the browser and it lands as a
markdown file — see [markdown interchange](/markdown/) for the shape. Drop a
markdown, text, or HTML file — or a dragged selection of text — on a column
and it becomes a task there (a `.md` that came from a board moves instead of
duplicating). Try dragging some of this paragraph onto the board above.

Columns scroll sideways; each column scrolls its own cards. Opening a card
floats it in a window over the board; click the backdrop, the ×, or press
`Escape` to put it back.

The component never talks to the log itself. It calls the store's verbs — the
same ones the [agent API](/verbs/) exposes — so a human and an agent editing
the same task produce indistinguishable events.
