programmati.ca Open Studio

Examples · Kanban board

A Trello-style kanban board: 54 lines of AppSPEC

A kanban board with drag-and-drop columns, an add-card form, an edit screen with delete, and a sortable list of every card. Create, move, update and delete are all declared on wires.

Running live in the sandbox with fixed demo data. Anything you add stays in this browser.

The state graph

3 screens, 4 wires

Screens are nodes and WIRE labels are edges. Screens in blue appear in the navigation; orange edges write a record on the way.

boardeditlistaddediteditupdateremove

How it works

Walking through the spec

01Screens are states

3 screens, each a <STATE>. board, list appear in the navigation because they carry a NAV label. Children lay themselves out by containment: ROW, COL and GRID arrange them, and everything else stacks.

<STATE ID="board" NAV="Board" ICON="kanban">
…
<STATE ID="edit" WIDTH="640px">
…
<STATE ID="list" NAV="All cards" ICON="list">

02Screens connect by label

Each <WIRE> names an action and where it leads. A button, card or row that fires the label follows the wire; the clicked record travels with it. This app declares 4 wires.

<WIRE LABEL="add" SAVE="cards" SET="status=todo" TOAST="Card added" />
<WIRE LABEL="edit" TO="edit" />
<WIRE LABEL="update" UPDATE="cards" TO="board" />
<WIRE LABEL="remove" DELETE="cards" TO="board" />

03Data flows through the key-value pipe

Every input publishes its value into the pipe under its ID, and every screen can read the pipe with {placeholders}. Writes are declared on wires: add save cards, update update cards, remove delete cards. SET adds key-value pairs to the record being written.

  <INPUT ID="title" LABEL="Title" VALUE="{record.title}" REQUIRED="true" />
…
    <SELECT ID="status" LABEL="Column" OPTIONS="todo:To do,doing:In progress,done:Done" VALUE="{record.status}" />
    <SELECT ID="tag" LABEL="Tag" OPTIONS="Planning,Design,Engineering,Marketing" VALUE="{record.tag}" />
…
  <INPUT ID="owner" LABEL="Owner" VALUE="{record.owner}" />
…
<WIRE LABEL="add" SAVE="cards" SET="status=todo" TOAST="Card added" />

04One attribute sets the look

THEME="notion" sets every colour, radius and typeface. Change the word and the whole app restyles; all 31 themes work with every component.

<APP ID="kanban" NAME="Launch Board" THEME="notion" LOGO="kanban" WIDTH="1240px">

What this would typically take

The pieces a conventional stack needs

A typical implementation of the same app assembles these parts, each with its own code, libraries and configuration. The size depends on the team and the tools, so we do not quote a number for it.

Here the same app is one 54-line document built from general-purpose components: BOARD, BUTTON, FORM, INPUT, SELECT, TABLE, TEXT, TOTAL.

Drag and dropA drag-and-drop library, drop targets per column and keyboard alternatives.
CRUD APIEndpoints to create, move, update and delete cards.
FormsAdd and edit forms prefilled from the selected card.
StateKeeping the board, the list view and the edit screen in sync.
Build toolingA bundler, a dev server and a build configuration.
The full speckanban.appspec · 54 lines
<APP ID="kanban" NAME="Launch Board" THEME="notion" LOGO="kanban" WIDTH="1240px">

  <DATA SRC="api/" />

  <STATE ID="board" NAV="Board" ICON="kanban">
    <ROW JUSTIFY="between">
      <COL GAP="2px">
        <TEXT STYLE="h1" TEXT="Launch board" />
        <TEXT STYLE="muted" TEXT="Drag cards between columns, or use the arrows. Every change is a key-value write." />
      </COL>
      <TOTAL SRC="cards" WHERE="status=done" OP="count" LABEL="Done:" />
    </ROW>
    <FORM ACTION="add" SUBMIT-LABEL="Add card" COLS="3">
      <FIELD NAME="title" LABEL="New card" PLACEHOLDER="What needs doing?" REQUIRED="true" />
      <FIELD NAME="tag" LABEL="Tag" TYPE="select" OPTIONS="Planning,Design,Engineering,Marketing" />
      <FIELD NAME="owner" LABEL="Owner" PLACEHOLDER="Who?" />
    </FORM>
    <BOARD SRC="cards" GROUP="status" COLUMNS="todo:To do,doing:In progress,done:Done" TITLE="title" SUBTITLE="{owner}" BADGE="tag" ACTION="edit" />
  </STATE>

  <STATE ID="edit" WIDTH="640px">
    <BUTTON STYLE="ghost" SIZE="sm" ICON="arrow-left" LABEL="Board" ACTION="back" />
    <TEXT STYLE="h1" TEXT="Edit card" />
    <INPUT ID="title" LABEL="Title" VALUE="{record.title}" REQUIRED="true" />
    <ROW>
      <SELECT ID="status" LABEL="Column" OPTIONS="todo:To do,doing:In progress,done:Done" VALUE="{record.status}" />
      <SELECT ID="tag" LABEL="Tag" OPTIONS="Planning,Design,Engineering,Marketing" VALUE="{record.tag}" />
    </ROW>
    <INPUT ID="owner" LABEL="Owner" VALUE="{record.owner}" />
    <ROW JUSTIFY="between">
      <BUTTON STYLE="danger" ICON="trash-2" LABEL="Delete" ACTION="remove" />
      <ROW GAP="8px">
        <BUTTON STYLE="secondary" LABEL="Cancel" ACTION="back" />
        <BUTTON ICON="check" LABEL="Save" ACTION="update" />
      </ROW>
    </ROW>
  </STATE>

  <STATE ID="list" NAV="All cards" ICON="list">
    <TEXT STYLE="h1" TEXT="All cards" />
    <TABLE SRC="cards" SORT="status" ACTION="edit">
      <COL FIELD="title" LABEL="Card" />
      <COL FIELD="status" LABEL="Column" TYPE="badge" />
      <COL FIELD="tag" LABEL="Tag" />
      <COL FIELD="owner" LABEL="Owner" />
    </TABLE>
  </STATE>

  <WIRE LABEL="add" SAVE="cards" SET="status=todo" TOAST="Card added" />
  <WIRE LABEL="edit" TO="edit" />
  <WIRE LABEL="update" UPDATE="cards" TO="board" />
  <WIRE LABEL="remove" DELETE="cards" TO="board" />

</APP>