Developers

Example app: Tow Board

A complete UplinkOS app you can copy. Tow Board tracks tow jobs: add a plate and street, mark it done, and switch between Open and Done.

#Try it in Studio

  1. Make a project. In Studio, press New project and pick Empty.
  2. Add the four files. Make each file below with New file and paste its code in.
  3. Run it. Type run in the terminal. Tow Board installs on the test phone and opens.

You can also open it in your browser to see how it looks.

Optional. Sets the name and the page the app opens on.

uplink.json
{
  "name": "Tow Board",
  "entry": "index.html"
}

#index.html

The page the app opens on. It links its CSS and JavaScript with relative paths.

index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Tow Board</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="app">
    <header>
      <h1>Tow Board</h1>
      <p class="sub" id="summary">No jobs yet</p>
    </header>

    <form id="add" class="card" autocomplete="off">
      <input name="plate" placeholder="Plate" maxlength="8" required>
      <input name="street" placeholder="Street" maxlength="40" required>
      <button type="submit">Add job</button>
    </form>

    <nav class="tabs">
      <button type="button" data-show="open" class="on">Open</button>
      <button type="button" data-show="done">Done</button>
    </nav>

    <ul id="jobs" class="list"></ul>
    <p id="empty" class="sub">Nothing here.</p>
  </main>
  <script src="app.js"></script>
</body>
</html>

#style.css

Looks like the rest of the phone, in light and dark.

style.css
:root {
  color-scheme: light dark;
  --bg: #f2f2f7;
  --card: #fff;
  --text: #000;
  --muted: #8e8e93;
  --accent: #ff9f0a;
}
@media (prefers-color-scheme: dark) {
  :root { --bg: #000; --card: #1c1c1e; --text: #fff; }
}
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
  background: var(--bg);
  color: var(--text);
}
.app { padding: 20px 16px; display: grid; gap: 14px; }
h1 { margin: 0; font-size: 30px; }
.sub { margin: 0; color: var(--muted); }
.card {
  display: grid;
  gap: 8px;
  padding: 12px;
  border-radius: 14px;
  background: var(--card);
}
input, button { font: inherit; border: 0; border-radius: 10px; }
input { padding: 12px; background: var(--bg); color: var(--text); }
button { padding: 12px; background: var(--accent); color: #000; font-weight: 600; }
.tabs { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.tabs button { background: var(--card); color: var(--muted); }
.tabs button.on { background: var(--accent); color: #000; }
.list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.job {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border-radius: 14px;
  background: var(--card);
}
.job b { display: block; font-family: ui-monospace, monospace; }
.job span { color: var(--muted); font-size: 14px; }
.job div { flex: 1; }
.job button { padding: 8px 12px; }

#app.js

Keeps the jobs in memory while the app is open. Browser storage is blocked on the phone, so a real app would save jobs on its own server.

app.js
const state = {
  jobs: [],
  show: "open",
};

const $ = (sel) => document.querySelector(sel);

function addJob(plate, street) {
  state.jobs.unshift({
    id: Date.now(),
    plate: plate.trim().toUpperCase(),
    street: street.trim(),
    done: false,
    at: new Date(),
  });
  render();
}

function toggleJob(id) {
  const job = state.jobs.find((j) => j.id === id);
  if (job) job.done = !job.done;
  render();
}

function jobRow(job) {
  const li = document.createElement("li");
  li.className = "job";

  const info = document.createElement("div");
  const plate = document.createElement("b");
  plate.textContent = job.plate;
  const where = document.createElement("span");
  where.textContent = `${job.street} · ${job.at.toLocaleTimeString()}`;
  info.append(plate, where);

  const btn = document.createElement("button");
  btn.type = "button";
  btn.textContent = job.done ? "Reopen" : "Done";
  btn.addEventListener("click", () => toggleJob(job.id));

  li.append(info, btn);
  return li;
}

function render() {
  const open = state.jobs.filter((j) => !j.done).length;
  const shown = state.jobs.filter((j) => j.done === (state.show === "done"));

  $("#summary").textContent = state.jobs.length
    ? `${open} open · ${state.jobs.length - open} done`
    : "No jobs yet";
  $("#jobs").replaceChildren(...shown.map(jobRow));
  $("#empty").hidden = shown.length > 0;

  document.querySelectorAll("[data-show]").forEach((b) => {
    b.classList.toggle("on", b.dataset.show === state.show);
  });
}

$("#add").addEventListener("submit", (e) => {
  e.preventDefault();
  const form = e.target;
  addJob(form.plate.value, form.street.value);
  form.reset();
  console.log("Job added", state.jobs[0]);
});

document.querySelectorAll("[data-show]").forEach((b) => {
  b.addEventListener("click", () => {
    state.show = b.dataset.show;
    render();
  });
});

render();
console.log("Tow Board started");

#Make it yours

  • Add an icon.png and put "icon": "icon.png" in uplink.json.
  • Save jobs on your own server so they are still there next time.
  • When it works, type package and submit it.