programmati.ca Open Studio

Examples · Video site

A YouTube-style video site: 58 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 stays in this browser.

The state graph

4 screens, 1 wire

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

homewatchsearchshortswatchwatchwatchwatch

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="Home" ICON="house">
…
<STATE ID="watch">
…
<STATE ID="search" NAV="Search" ICON="search">
…
<STATE ID="shorts" NAV="Shorts" 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 the wire; the clicked record travels with it. This app declares 1 wire.

<WIRE LABEL="watch" TO="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.

<CATEGORY-NAV ID="cat" SRC="videos" FIELD="category" />
…
    <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" ACTION="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">

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 58-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 · 58 lines
<APP ID="clips" NAME="Clips" THEME="linear" LOGO="clapperboard" WIDTH="1240px">

  <DATA SRC="api/" />

  <STATE ID="home" NAV="Home" 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>
    <CATEGORY-NAV ID="cat" SRC="videos" FIELD="category" />
    <LIST SRC="videos" WHERE="category={cat}" COLS="4" THUMB="thumb" TITLE="title"
          SUBTITLE="{channel} · {year}" META="{views|compact} views" BADGE="{duration|dur}" ACTION="watch" />
  </STATE>

  <STATE ID="watch">
    <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" ACTION="watch" />
      </COL>
    </ROW>
  </STATE>

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

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

  <WIRE LABEL="watch" TO="watch" />

</APP>