diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 0a9ad9c..39b7837 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,5 +1,5 @@
# Keep pinned GitHub Actions current. Nothing else here is managed by
-# Dependabot: Python dependencies are pinned per docs/requirements.txt or
+# Dependabot: Python dependencies are
# resolved at build time, not from a lockfile in this repo.
version: 2
updates:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..b0bf4cc
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,50 @@
+# Contributing
+
+Bug reports and fixes are welcome. Open an
+[issue](https://github.com/PyDevices/pyscript-template/issues) first for
+anything larger than a fix.
+
+## Run the tests
+
+```bash
+./scripts/vendor_pyscript.sh
+python3 -m unittest discover -s tests -v
+```
+
+The suite checks that the shell, service worker, manifest and `pyscript.json`
+agree: the JSON parses, every local file `index.html` and `sw.js` mention
+exists, the PyDevices source URLs are pinned to the release the test names, the
+page uses the Pyodide interpreter, and `pwa.js` leaves the browser's own install
+prompt alone. Two tests also run `tests/worklet_sim.mjs` under Node to check the
+audio worklet's queue. It does not replace a real browser test of pointer
+input, installation or offline caching.
+
+Read `tests/test_template.py` before moving a shell file or adding a generated
+asset to the cache list.
+
+## Update a pin
+
+- **PyScript.** Change `PYSCRIPT_VERSION` in `scripts/vendor_pyscript.sh`.
+ Check and confirm
+ `https://pyscript.net/releases//offline_.zip` returns 200
+ before committing. Re-run `./scripts/vendor_pyscript.sh` and the tests.
+ The wheel tag in [Bring in a bigger
+ app](docs/guides/bring-in-a-bigger-app.md) depends on the Pyodide that
+ release ships.
+- **PyDevices sources.** Every URL in `pyscript.json` names one PyDevices
+ release. Change them together and update the release string in
+ `tests/test_template.py`, then load the app in a browser.
+- **Cache.** Change `VERSION` in `sw.js` whenever you change a cached file, so
+ browsers that already have the app pick the change up.
+
+## Docs
+
+The pages are plain Markdown under `docs/`, starting at
+[docs/index.md](docs/index.md). There is no docs build. Check that any link you
+add resolves.
+
+## Releasing
+
+The template has no packages and no version of its own: a change is released
+by merging it to `main`, and users get it the next time they press **Use this
+template** or pull.
diff --git a/README.md b/README.md
index 06a376c..f887eac 100644
--- a/README.md
+++ b/README.md
@@ -1,112 +1,50 @@
-# PyDevices PyScript template
+
-A minimal, installable PyScript application for the portable
-[PyDevices](https://github.com/PyDevices/pydevices) display stack.
+# PyScript template
-Use this repository as a GitHub template, edit `main.py`, and run it on your
-own machine with Python's built-in web server ([below](#run-it-locally)). The
-service worker caches the application shell and the pinned PyScript
-interpreter, so after one visit the app launches offline too. The template
-pins PyDevices source files to release `v0.7.0`, so a new app does not
-silently change when the product's default branch advances.
+PyScript template is a starter for an installable browser app that draws with
+the PyDevices display stack: Python (Pyodide) on a canvas, working offline once
+it has loaded.
-## Starter Example: Interactive Touch / Paint
+On GitHub, press **Use this template**, or clone it, then serve it from
+localhost:
-This is exactly what ships in `main.py` — an interactive paint application
-demonstrating the PyDevices Board Contract (`board_config` and
-`appdev.App`) running in the browser:
-
-```python
-import board_config
-import appdev
+```bash
+git clone https://github.com/PyDevices/pyscript-template my-app
+cd my-app
+./scripts/vendor_pyscript.sh
+python3 -m http.server 8000
+```
-display_drv = board_config.display_drv
-app = appdev.App(board_config)
+Open . Here is a paint app in 20 lines; it is a cut-down
+`main.py`, which also sets a 500x500 display size:
+```python
+import board_config, appdev
+from pyscript import document
+display, app = board_config.display_drv, appdev.App(board_config)
colors = [0xFFFF, 0xF800, 0x07E0, 0x001F, 0x07FF, 0xF81F, 0xFFE0, 0x0000]
-block_size = display_drv.width // len(colors)
-selected = 0
-
-def draw_palette():
- for i, color in enumerate(colors):
- x = i * block_size
- display_drv.fill_rect(x, 0, block_size, 30, color)
- display_drv.show()
-
-draw_palette()
-
+block, selected = display.width // len(colors), 0
+for i, color in enumerate(colors):
+ display.fill_rect(i * block, 0, block, 30, color)
+display.show()
+document.querySelector("#status").textContent = "Pick a colour, then paint."
def on_touch(event):
global selected
x, y = event.pos
if y < 30:
- selected = min(len(colors) - 1, x // block_size)
+ selected = min(len(colors) - 1, x // block)
else:
- display_drv.fill_rect(x - 3, y - 3, 6, 6, colors[selected])
- display_drv.show()
-
+ display.fill_rect(x - 3, y - 3, 6, 6, colors[selected])
+ display.show()
app.on(app.events.MOUSEBUTTONDOWN, on_touch)
app.on(app.events.MOUSEMOTION, on_touch)
app.run()
```
-## How It Works
-
-1. **HTML5 Canvas Backend**: `board_config.display_drv` binds to the `