# Writing an app for ShipLock

Paste this file into any model, then describe the app you want. It teaches the one pattern ShipLock apps use.

**Output format: your entire reply is one ```html code block containing the complete file. No plan, no explanation, no checklist, nothing before or after it.**

You are writing **one self-contained HTML file**. Inline CSS and JavaScript, no build step, no framework, no external scripts or fonts, no server code. The file is deployed to a ShipLock server, which serves it and injects a global called `SL` before your script runs. `SL` gives the page a database and file storage. Do not invent any other backend, do not use `localStorage` for shared data, and do not put API keys in the file.

## The API

```js
SL.role        // 'view' | 'submit' | 'edit' | 'admin' | 'owner'   what THIS visitor may do
SL.appId
SL.closed      // '' normally; for an add-only visitor while the owner has closed entries, a sentence saying so

const c = SL.db('dishes')              // a collection. Created on first write, no schema.
await c.list({ where: { done: false }, order: '-created', limit: 50, offset: 0 })   // -> array of rows (limit up to 1000; page with offset)
await c.get(id)
await c.add({ name: 'Priya', serves: 8 })        // -> { id, created }
await c.update(id, { serves: 10 })               // shallow merge, -> the row
await c.remove(id)

const f = await SL.files.upload(fileObject)      // -> { id, name, bytes }
SL.files.url(f.id)                               // use as <img src> or <a href>
await SL.files.list()
await SL.files.remove(id)
```

Rows are plain JSON objects. Every row you read also has `id`, `_created` and `_updated` (milliseconds). `order` is `'created'`, `'-created'` or a field name with an optional leading `-`. `where` matches fields by equality only (string, number, boolean, null). There is no SQL, no joins, no realtime. To refresh, call `list` again.

Every call returns a Promise and rejects with an `Error` whose message says what went wrong. Catch errors and show the message on the page.

## Roles decide what the page should show

The server enforces roles. Your job is to not show buttons that will fail.

| Role | Can read rows and files | Can add | Can change or delete |
|---|---|---|---|
| `view` | yes | no | no |
| `submit` | **no** | yes | no |
| `edit`, `admin`, `owner` | yes | yes | yes |

So:

```js
const canRead = ['view', 'edit', 'admin', 'owner'].includes(SL.role)
const canAdd = SL.role !== 'view'
const canChange = ['edit', 'admin', 'owner'].includes(SL.role)
```

- A `submit` visitor (for example, someone with a sign-up link) can add but **cannot read anything back**. Never call `list` for them. Show the form, then a thank-you message after a successful add. Do not show a list.
- A `view` visitor sees the content and no forms or edit buttons.
- Hide forms and buttons with the `hidden` attribute, don't just disable them.

## Rules for good apps

1. One file, mobile first, readable on a phone. Dark or light, but set `background` and `color` explicitly. Tap targets at least 44px.
2. Build DOM with `createElement` and `textContent`. **Never put user data into `innerHTML`**. Anyone with a link can write to this app.
3. Store data as flat objects in a few named collections (`'dishes'`, `'items'`, `'photos'`). Keep a file's id in the row (`{ fileId: f.id }`) and show it with `SL.files.url(row.fileId)`.
4. Give important elements stable `id` attributes so the app is easy to test.
5. If `SL.closed` is set, hide the form and show that sentence instead; a write would be refused anyway. Put a small, quiet line at the bottom saying what this visitor can do, in plain words: `SL.can` is that sentence ("You can look", "You can add; you cannot see what others added", "You can look and change things"). Never show the raw role name.
6. After every write, call `load()` again so the page matches the database.
7. Empty states should say what to do ("No items yet. Add the first one.").
8. If `window.SL` is missing (the file was opened directly), show a short message saying the app needs to be deployed to ShipLock.

## Build every feature the request implies

Work out what the request needs (in your head, do not write it out) and make sure each one has code:

- "tick off", "done", "mark" means a checkbox per row that calls `update(id, { done })`.
- "delete", "remove", "take down" means a delete button per row (editors only) that calls `remove(id)`.
- "totals", "count", "how many", "streak" means you compute it from the rows you read (filter and count them in JavaScript).
- "only I can see the answers" means guests get a submit form with a thank-you, **and the page also has a results view that appears whenever `canRead` is true**. The owner opens the same file and sees the results. Do not forget this half.
- Several roles means one page with two faces: a form for people who may add, a list for people who may read, controls for people who may change. Show whichever the role allows.

If you change an element's `id` in the HTML, change every `$('#...')` that uses it. A wrong id crashes the whole page.

## Patterns

These are shapes, not templates. Copy none of them blindly; change names, fields and layout to fit the request.

**Tick and delete (editors).** Inside your row builder:

```js
const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = !!r.done; cb.disabled = !canChange
cb.onchange = async () => { await items.update(r.id, { done: cb.checked }); load() }
if (canChange) { const x = document.createElement('button'); x.textContent = 'Delete'; x.onclick = async () => { await items.remove(r.id); load() } }
```

**Options people tap, totals for readers (a poll).** Voters only add. The totals are computed from rows and shown only when `canRead`.

```js
const votes = SL.db('votes')
for (const choice of ['Pizza', 'Tacos']) {
  const b = document.createElement('button'); b.textContent = choice
  b.onclick = async () => { await votes.add({ choice }); thanks.hidden = false; load() }
  buttons.append(b)
}
async function load() {
  if (!canRead) return
  const rows = await votes.list()
  totals.textContent = ['Pizza', 'Tacos'].map(c => c + ': ' + rows.filter(r => r.choice === c).length).join('   ')
}
```

**Form for guests, answers for the owner.**

```js
form.hidden = !canAdd                       // guests and editors can add
results.hidden = !canRead                   // owner, viewers, editors can read; submit-only guests cannot
if (canRead) showAnswers(await answers.list({ order: 'created' }))
```

## Skeleton (a plain list; adapt it, do not copy it)

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Shopping list</title>
<style>body{font:16px/1.5 system-ui;background:#0f0f17;color:#ecebf5;margin:0}main{max-width:32rem;margin:0 auto;padding:1.5rem 1rem}[hidden]{display:none!important}</style>
</head>
<body>
<main>
  <h1>Shopping list</h1>
  <form id="form" hidden><input id="text" placeholder="Add an item" required><button>Add</button></form>
  <ul id="list"></ul>
  <p id="role"></p>
</main>
<script>
if (!window.SL) { document.body.textContent = 'Deploy this file to ShipLock to use it.'; throw new Error('no SL') }
const items = SL.db('items')
const canAdd = SL.role !== 'view'
const canRead = ['view', 'edit', 'admin', 'owner'].includes(SL.role)
const $ = s => document.querySelector(s)
$('#form').hidden = !canAdd
$('#role').textContent = SL.can

async function load() {
  if (!canRead) return
  const rows = await items.list({ order: 'created' })
  $('#list').replaceChildren(...rows.map(r => { const li = document.createElement('li'); li.textContent = r.text; return li }))
}
$('#form').onsubmit = async e => {
  e.preventDefault()
  try { await items.add({ text: $('#text').value }); $('#text').value = ''; load() }
  catch (err) { alert(err.message) }
}
load()
</script>
</body>
</html>
```

## Before you reply

Silently check each of these against your code. Do not write the checks out:

1. Every feature the request names has code (tick, delete, totals, results view).
2. Every `$('#id')` has an element with that id.
3. Submit-only visitors never trigger `list`.
4. No user data goes through `innerHTML`.

Then reply with only the one code block.

## Publishing it (for agents with a shell)

Save the file and upload it: `curl -F file=@app.html -F name="App name" -F link="Label:role" -H "Authorization: Bearer $SHIPLOCK_TOKEN" $SHIPLOCK_URL/api/deploy`. Pass `link` when the app is for other people, to get a share link. No file access? Tell the user to drop the file on the dashboard. Use `submit` for sign-up forms, `view` for read-only pages and `edit` for shared lists. New apps start private. Give the user the URL the tool returns and say which role the link has.
