programmati.ca Open Studio

Examples · Video site

A YouTube-style video site: 53 lines of AppSPEC

A video site built on Blender open movies: a home grid with category filters, a watch page with the player, likes, saves, comments and an up-next list, full-text search and a shorts playlist.

Running live in the sandbox with fixed demo data. Anything you add is saved in this browser; nothing leaves your device.

The state graph

4 screens, 0 wires

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

homewatchsearchshorts

How it works

Walking through the spec

01Screens are states

4 screens, each a <STATE>. home, search, shorts 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="home" NAV ICON="house">
…
<STATE ID="watch" BACK="false">
…
<STATE ID="search" NAV ICON="search">
…
<STATE ID="shorts" NAV ICON="zap">

02Screens connect by label

Each <WIRE> names an action and where it leads; a button, card or row that fires the label follows it, taking the clicked record along. It writes no WIRE at all: it uses the short forms OPEN. OPEN="state" opens an item in that state, and FORM SAVE or UPDATE saves the form to a collection; the engine adds the wires.

      BADGE="{duration|dur}" OPEN="watch" />
…
    <RELATED BY="tags,category" HEADING="Up next" META="{channel} · {duration|dur}" LIMIT="8" OPEN="watch" />
…
      BADGE="{duration|dur}" OPEN="watch" EMPTY="Nothing matches." />
…
<PLAYLIST SRC="videos" WHERE="duration&lt;300" SORT="duration" META="{channel} · {duration|dur}" OPEN="watch" />

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}. No wire writes a record here; the pipe drives filtering and display, and comments and likes are stored by the social components themselves.

    <VIDEO YOUTUBE="{record.youtube}" TITLE="{record.title}" />
    <TEXT STYLE="h2" TEXT="{record.title}" />
…
        <AVATAR NAME="{record.channel}" />
…
          <TEXT STYLE="h3" TEXT="{record.channel}" />
          <TEXT STYLE="small" TEXT="{record.views|num} views · {record.year}" />
…
      <ROW GAP="6px"><BADGE TEXT="{record.category}" /><BADGE TONE="muted" TEXT="{record.licence}" /></ROW>
      <TEXT TEXT="{record.description}" />
…
<INPUT ID="q" TYPE="search" PLACEHOLDER="Search films…" LIVE="true" />

04Social components attach themselves

ACTION-BAR, COMMENT-THREAD, RELATED bind to the record on screen through its GUID (for example videos/…). The spec never says which record: the engine supplies it, so the same tags work unchanged on a video, a product or a story.

  <ACTION-BAR />
…
<COMMENT-THREAD />
…
<RELATED BY="tags,category" HEADING="Up next" META="{channel} · {duration|dur}" LIMIT="8" OPEN="watch" />

05One attribute sets the look

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

<APP ID="clips" NAME="Clips" THEME="linear" LOGO="clapperboard" WIDTH="1240px">

Convention over configuration

What the engine infers

Every attribute this spec leaves out has a default inferred from names, data and context. These are the ones this app relies on; writing any of them out would override it.

  • STATE NAV → Home, Search, ShortsNAV with no label uses the state ID, humanised
  • LIST ACTION → watchOPEN="watch" opens the clicked item in that state
  • LIST TITLE → titleThe records have a title field
  • LIST THUMB → thumbThe records have a thumb field
  • LIST LAYOUT → gridThe records have images
  • LIST WHERE → category={filter-category}FILTER="category" adds filter pills and this condition
  • Filter pillsFILTER="category" adds these pills
  • RELATED ACTION → watchOPEN="watch" opens the clicked item in that state
  • PLAYLIST ACTION → watchOPEN="watch" opens the clicked item in that state

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 53-line document built from general-purpose components: ACTION-BAR, AVATAR, BADGE, CARD, CATEGORY-NAV, COMMENT-THREAD, INPUT, LIST, PLAYLIST, RELATED, TEXT, VIDEO.

RoutingRoutes and navigation for home, watch, search and shorts, plus passing the current video between them.
Video player integrationEmbedding and sizing a third-party player, with a thumbnail placeholder while it loads.
CommentsA comments data model, an API to read and post them per video, threading and a form component.
Likes and savesPer-video counters, per-user state and the endpoints behind them.
Related videosA query that ranks other videos by shared tags and category.
Search and filtersSearch input handling, filtering, empty states and category pills.
State and stylingState management across screens, a component library and styles for every screen.
Build toolingA bundler, a dev server and a build configuration.
The full specclips.appspec · 53 lines
The same spec with every inferred attribute written out (50 lines)
<APP ID="clips" NAME="Clips" THEME="linear" LOGO="clapperboard" WIDTH="1240px">

  <STATE ID="home" NAV ICON="house">
    <ROW JUSTIFY="between" ALIGN="end">
      <COL GAP="2px">
        <TEXT STYLE="eyebrow" TEXT="Open movies" />
        <TEXT STYLE="h1" TEXT="Watch something made in the open" />
      </COL>
      <TEXT STYLE="small" TEXT="Films by Blender Studio, CC BY" />
    </ROW>
    <LIST SRC="videos" FILTER="category" COLS="4" SUBTITLE="{channel} · {year}" META="{views|compact} views"
          BADGE="{duration|dur}" OPEN="watch" />
  </STATE>

  <STATE ID="watch" BACK="false">
    <ROW ALIGN="start" GAP="28px">
      <COL SPAN="2" MIN="320px" GAP="16px">
        <VIDEO YOUTUBE="{record.youtube}" TITLE="{record.title}" />
        <TEXT STYLE="h2" TEXT="{record.title}" />
        <ROW JUSTIFY="between">
          <ROW GAP="10px">
            <AVATAR NAME="{record.channel}" />
            <COL GAP="0">
              <TEXT STYLE="h3" TEXT="{record.channel}" />
              <TEXT STYLE="small" TEXT="{record.views|num} views · {record.year}" />
            </COL>
          </ROW>
          <ACTION-BAR />
        </ROW>
        <CARD STYLE="flat" GAP="6px">
          <ROW GAP="6px"><BADGE TEXT="{record.category}" /><BADGE TONE="muted" TEXT="{record.licence}" /></ROW>
          <TEXT TEXT="{record.description}" />
        </CARD>
        <COMMENT-THREAD />
      </COL>
      <COL MIN="280px">
        <RELATED BY="tags,category" HEADING="Up next" META="{channel} · {duration|dur}" LIMIT="8" OPEN="watch" />
      </COL>
    </ROW>
  </STATE>

  <STATE ID="search" NAV ICON="search">
    <INPUT ID="q" TYPE="search" PLACEHOLDER="Search films…" LIVE="true" />
    <LIST SRC="videos" WHERE="title LIKE {q}" LAYOUT="rows" SUBTITLE="{channel} · {year}" META="{description}"
          BADGE="{duration|dur}" OPEN="watch" EMPTY="Nothing matches." />
  </STATE>

  <STATE ID="shorts" NAV ICON="zap">
    <TEXT STYLE="h2" TEXT="Under five minutes" />
    <PLAYLIST SRC="videos" WHERE="duration&lt;300" SORT="duration" META="{channel} · {duration|dur}" OPEN="watch" />
  </STATE>

</APP>