<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 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.
<tosi-virta lane="status" color="sky" style="width: 100%; height: 100%"></tosi-virta>
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
const board = preview.querySelector('tosi-virta')
await new Promise((r) => setTimeout(r, 100))
test('the board renders lanes and cards', () => {
expect(board.querySelectorAll('.lane').length).toBeGreaterThan(2)
expect(board.querySelectorAll('.card').length).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))
const cards = board.querySelectorAll('.card').length
expect(cards).toBeGreaterThan(0)
expect(cards).toBeLessThan(150)
})
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 (or the button in the card window) 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. 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 (defaultstatus). A task with no status is in the inbox — the implicitstatus: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 name for the board background (defaultsky). Boards are saved filters, so each one gets its own color.hide— lane values to leave out, space-separated (defaultfreezer archive); the toggle row under the filter writes it back.db— with nostoreset, 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);seed— seed that many tasks if the log is empty;identity— who the board writes as (defaultyou). Together they make a board that needs no code: see the full-screen board.
Lanes
status: lanes come in a fixed order — inbox, freezer, backlog, ready,
in-progress, review, done, archive; 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 and archive start off.
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 ⚙
opens its settings — only what cannot be derived: the repo, and the look
(icon, color, background). Description, site, 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. 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 scope 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 box above the tiles, or a project's "refresh from GitHub"): point it at a repo and its description, homepage, package, version, and issues arrive — see GitHub import. 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 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); 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). 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 with who you are, or offline: <reason>.
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, mode, scope
The top line reads left to right: a view switch (tiles = overview,
columns = board), a mode select — project, status, owner — and a
scope select the mode labels. In project mode the scope is a project
or all projects, and the lanes are statuses: the default board. In
status mode the scope is one status and the lanes are projects — "what is
in progress, everywhere". In owner mode the scope is one status and the
lanes are owners — who is carrying what, the view for balancing a team.
Only project mode has "all". 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.
Moving a task to another project
The card window's project select moves a task upstream or downstream —
"this should not be done here". Leaving a project asks what that project is
waiting on: it blocks it, it delays it, or neither. The move is one
commit: project: changes, from:<old> keeps the provenance, blocks:<old>
or delays:<old> records the dependency, and a comment says so. The old
project's 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.
Nothing stops a task living in several projects: it is just two project:
tags, and in project lanes the card shows in both. Alt-drag a card onto
a project lane to add it there without leaving its own (the verb's
also), or add the tag in the card's tag list.
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: 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 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 exposes — so a human and an agent editing the same task produce indistinguishable events.