glissade 0.14.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. glissade-0.14.4/.gitignore +22 -0
  2. glissade-0.14.4/LICENSE +21 -0
  3. glissade-0.14.4/PKG-INFO +443 -0
  4. glissade-0.14.4/README.md +404 -0
  5. glissade-0.14.4/pyproject.toml +83 -0
  6. glissade-0.14.4/src/glissade/__init__.py +47 -0
  7. glissade-0.14.4/src/glissade/__main__.py +6 -0
  8. glissade-0.14.4/src/glissade/app.py +709 -0
  9. glissade-0.14.4/src/glissade/assets.py +271 -0
  10. glissade-0.14.4/src/glissade/build.py +67 -0
  11. glissade-0.14.4/src/glissade/check.py +353 -0
  12. glissade-0.14.4/src/glissade/cli.py +699 -0
  13. glissade-0.14.4/src/glissade/data/demo/decks/gallery.json +313 -0
  14. glissade-0.14.4/src/glissade/data/demo/decks/media/activities/sorter.html +137 -0
  15. glissade-0.14.4/src/glissade/data/demo/decks/media/images/gradient-amber.jpg +0 -0
  16. glissade-0.14.4/src/glissade/data/demo/decks/media/images/gradient-blue.jpg +0 -0
  17. glissade-0.14.4/src/glissade/data/demo/decks/media/images/gradient-moss.jpg +0 -0
  18. glissade-0.14.4/src/glissade/data/demo/decks/media/images/gradient-plum.jpg +0 -0
  19. glissade-0.14.4/src/glissade/data/demo/decks/media/images/gradient-slate.jpg +0 -0
  20. glissade-0.14.4/src/glissade/data/demo/decks/media/images/gradient-teal.jpg +0 -0
  21. glissade-0.14.4/src/glissade/data/demo/decks/tour.json +336 -0
  22. glissade-0.14.4/src/glissade/data/scaffold/.gitignore +5 -0
  23. glissade-0.14.4/src/glissade/data/scaffold/AGENTS.md +202 -0
  24. glissade-0.14.4/src/glissade/data/scaffold/decks/welcome.json +62 -0
  25. glissade-0.14.4/src/glissade/data/scaffold/glissade.toml +6 -0
  26. glissade-0.14.4/src/glissade/data/schema.json +315 -0
  27. glissade-0.14.4/src/glissade/data/themes.json +104 -0
  28. glissade-0.14.4/src/glissade/decks.py +138 -0
  29. glissade-0.14.4/src/glissade/fix.py +176 -0
  30. glissade-0.14.4/src/glissade/project.py +196 -0
  31. glissade-0.14.4/src/glissade/scaffold.py +156 -0
  32. glissade-0.14.4/src/glissade/templates/control.html +883 -0
  33. glissade-0.14.4/src/glissade/templates/deck.html +1276 -0
  34. glissade-0.14.4/src/glissade/term.py +63 -0
  35. glissade-0.14.4/src/glissade/upgrade.py +196 -0
  36. glissade-0.14.4/website/README.md +40 -0
@@ -0,0 +1,22 @@
1
+ # Build artefacts
2
+ build/
3
+ dist/
4
+ *.egg-info/
5
+
6
+ # Environments
7
+ .venv/
8
+ .pixi/
9
+
10
+ # Presenter state
11
+ .glissade/
12
+
13
+ # Python
14
+ __pycache__/
15
+ *.py[cod]
16
+
17
+ # Website
18
+ website/node_modules/
19
+ website/dist/
20
+
21
+ # macOS
22
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Fullmer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,443 @@
1
+ Metadata-Version: 2.5
2
+ Name: glissade
3
+ Version: 0.14.4
4
+ Summary: Presentation decks written as JSON, driven from your phone, built into a single self-contained HTML file
5
+ Project-URL: Homepage, https://github.com/techmuch/glissade
6
+ Project-URL: Issues, https://github.com/techmuch/glissade/issues
7
+ Author-email: David Fullmer <david.d.fullmer@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: deck,offline,presentation,projector,remote,slides
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: Microsoft :: Windows
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Multimedia :: Graphics :: Presentation
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: fastapi>=0.110
28
+ Requires-Dist: qrcode>=7.4
29
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
30
+ Requires-Dist: uvicorn>=0.27
31
+ Provides-Extra: dev
32
+ Requires-Dist: httpx>=0.27; extra == 'dev'
33
+ Requires-Dist: playwright>=1.54.0; extra == 'dev'
34
+ Requires-Dist: pytest>=8.0; extra == 'dev'
35
+ Requires-Dist: ruff>=0.6.0; extra == 'dev'
36
+ Provides-Extra: images
37
+ Requires-Dist: pillow>=10.0; extra == 'images'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # Glissade
41
+
42
+ [![Website](https://img.shields.io/badge/Website-techmuch.github.io%2Fglissade-amber?style=flat-square)](https://techmuch.github.io/glissade/)
43
+
44
+ Presentation decks written as JSON, driven from your phone, and built into a
45
+ single HTML file that works when the Wi-Fi doesn't.
46
+
47
+ ```bash
48
+ uv tool install --from git+https://github.com/techmuch/glissade glissade
49
+
50
+ mkdir my-talk && cd my-talk
51
+ glissade init # scaffold a deck and an AI-agent guide
52
+ glissade start # present — prints a QR code for the remote
53
+ ```
54
+
55
+ No project yet? `glissade demo` runs the decks that ship with the tool from
56
+ anywhere, and they document the whole system.
57
+
58
+ ## Installing
59
+
60
+ Glissade is not on PyPI yet, so install it from GitHub for now.
61
+
62
+ | | |
63
+ | --- | --- |
64
+ | **uv** (recommended) | `uv tool install --from git+https://github.com/techmuch/glissade glissade` — installs Python too if you don't have it |
65
+ | **pipx** | `pipx install git+https://github.com/techmuch/glissade.git` |
66
+ | **pip** | `pip install git+https://github.com/techmuch/glissade.git` |
67
+ | **Try without installing** | `uvx --from git+https://github.com/techmuch/glissade glissade demo` |
68
+
69
+ One universal wheel covers Windows, macOS and Linux — every dependency is pure
70
+ Python, so installation never needs a compiler. Python 3.10 or newer.
71
+
72
+ If you want Pillow for image downscaling, install the `images` extra from Git
73
+ instead, for example:
74
+
75
+ ```bash
76
+ pip install 'git+https://github.com/techmuch/glissade.git#egg=glissade[images]'
77
+ ```
78
+
79
+ Without Pillow, images embed at full size; nothing breaks.
80
+
81
+ ## Commands
82
+
83
+ | Command | Does |
84
+ | --- | --- |
85
+ | `glissade init [dir]` | Scaffold a project: a starter deck, `AGENTS.md`, and the JSON schema |
86
+ | `glissade start` | Present, with a phone remote on the same Wi-Fi |
87
+ | `glissade build [deck]` | Write standalone HTML to `build/` |
88
+ | `glissade check [deck]` | Validate decks — `--fix` applies the obvious corrections |
89
+ | `glissade decks` / `themes` | List what's available |
90
+ | `glissade demo` | Present the built-in tour and layout gallery |
91
+ | `glissade update` | Bring a project's Glissade-owned files up to date |
92
+ | `glissade schema` | Refresh the project's JSON schema copy |
93
+ | `glissade upgrade` | Update to the latest release |
94
+
95
+ Every command takes `-C PATH` to run as if started elsewhere. Commands find
96
+ your project by walking up from the working directory, the way git does, so
97
+ they work from any subdirectory.
98
+
99
+ ### Versions and compatibility
100
+
101
+ A deck can declare the release it needs:
102
+
103
+ ```jsonc
104
+ {
105
+ "glissade": ">=0.6",
106
+ "title": "My talk",
107
+ "slides": [ ... ]
108
+ }
109
+ ```
110
+
111
+ Ranges work too (`">=0.6,<1.0"`), and a bare `"0.6"` means *at least* 0.6.
112
+
113
+ Older releases don't fail silently on it. `glissade check` treats an unmet
114
+ requirement as an error, and `start` and `build` print a warning but still run
115
+ — a deck that renders most of itself beats one that refuses in front of an
116
+ audience.
117
+
118
+ The same applies to fields. Anything this release doesn't recognise is
119
+ reported rather than quietly ignored:
120
+
121
+ ```
122
+ warn slide 1: 'transition' isn't a field Glissade 0.6.0 understands — it will be ignored
123
+ If the deck was written for a newer release, run `glissade upgrade`.
124
+ ```
125
+
126
+ `init` copies the schema into your project, and your editor validates against
127
+ that copy — so it goes stale when you upgrade. `check` notices and tells you to
128
+ run `glissade schema`, which refreshes it.
129
+
130
+ ### Fixing decks
131
+
132
+ `glissade check --fix` applies the corrections that have one obviously correct
133
+ answer:
134
+
135
+ ```
136
+ Fixed 3:
137
+ [talk] deck: record the deck format 'absent' -> '1'
138
+ [talk] slide 4: correct the layout name 'media-rite' -> 'media-right'
139
+ [talk] slide 9: use the YouTube embed URL '…watch?v=ID' -> '…/embed/ID'
140
+ Originals kept as <deck>.json.bak
141
+ ```
142
+
143
+ That's the whole list: layout and modifier names that are unmistakably typos,
144
+ YouTube watch links, and the format stamp. It deliberately will **not** rename
145
+ an unrecognised field — the nearest spelling is often the wrong meaning
146
+ (`subtitle` looks closer to `title` than to `subheading`), and a field this
147
+ release doesn't know may belong to a newer one. Those stay reported.
148
+
149
+ Decks are never rewritten without `--fix`, and the original is kept as
150
+ `<deck>.json.bak`.
151
+
152
+ ### The format stamp
153
+
154
+ A deck records the format it was written against:
155
+
156
+ ```jsonc
157
+ { "format": 1, "title": "My talk", "slides": [ ... ] }
158
+ ```
159
+
160
+ Absent means 1. It exists so a future release can migrate a file rather than
161
+ guess at its age. `init` writes it, and `check --fix` adds it to older decks.
162
+
163
+ Distinct from `"glissade": ">=0.6"`, which is a *requirement* — what the deck
164
+ needs from the tool, rather than what the tool should assume about the deck.
165
+
166
+ ### Upgrading a project
167
+
168
+ Upgrading the tool doesn't touch your projects. Two files in a project belong
169
+ to Glissade and track the release — `AGENTS.md`, which describes the deck
170
+ format, and `glissade.schema.json`, which your editor validates against. Both
171
+ go stale when you upgrade:
172
+
173
+ ```bash
174
+ glissade update --dry-run # what would change
175
+ glissade update # refresh them
176
+ ```
177
+
178
+ It refreshes only those two. Your decks, `themes.json`, `glissade.toml` and
179
+ `.gitignore` are yours from the moment `init` writes them and are never
180
+ rewritten.
181
+
182
+ If you've edited a Glissade-owned file — house rules appended to `AGENTS.md`,
183
+ say — it isn't discarded. `update` saves yours as `AGENTS.md.bak` and tells
184
+ you, or use `--keep` to leave it alone entirely. It knows the difference
185
+ because `init` and `update` record a hash of what they wrote, in
186
+ `.glissade/scaffold.json`, which also records the release the project was
187
+ created with.
188
+
189
+ `init --force` refreshes the scaffold too, but it will not overwrite anything
190
+ under `decks/` or your config — it lists what it kept.
191
+
192
+ ### Upgrading Glissade
193
+
194
+ ```bash
195
+ glissade upgrade # check, then update
196
+ glissade upgrade --check # just tell me if there's a newer one
197
+ ```
198
+
199
+ It works out how Glissade was installed — uv, pipx, or pip — and runs that
200
+ tool's upgrade command, showing you the command first. It never rewrites its
201
+ own files: a running process can't safely replace them, and on Windows the
202
+ console script is locked outright. If the installer isn't on your PATH it
203
+ prints the command instead of guessing.
204
+
205
+ **Glissade only reaches the network when you run this command.** There's no
206
+ startup check and no background ping. It's a tool you run in front of an
207
+ audience; it shouldn't stall or print a notice at the wrong moment.
208
+
209
+ ## A project
210
+
211
+ ```
212
+ my-talk/
213
+ ├── decks/
214
+ │ ├── talk.json your deck
215
+ │ └── media/ images and activities it references
216
+ ├── AGENTS.md guide for an AI assistant writing slides
217
+ ├── glissade.schema.json editor autocomplete and validation
218
+ ├── glissade.toml optional defaults (deck, port, host)
219
+ ├── themes.json optional — overrides the built-in themes
220
+ ├── .glissade/ generated presenter state and live notes
221
+ └── build/ generated; rebuild any time
222
+ ```
223
+
224
+ Only `decks/` is required. A directory containing it is a project.
225
+
226
+ ## Writing decks with an AI assistant
227
+
228
+ `glissade init` writes an `AGENTS.md` alongside your deck: the full field
229
+ reference, the layout catalogue, worked examples, and the rules that matter
230
+ (every slide needs speaker notes; media paths are relative to the deck; use
231
+ `/embed/` YouTube URLs). Point Claude Code, Cursor, or any agent at the
232
+ directory and ask it to build your talk.
233
+
234
+ The loop that makes this work is `glissade check`. It validates structure and
235
+ the things a schema can't — missing image files, layouts that are near-miss
236
+ typos, grids with one image, YouTube watch links — and exits non-zero so an
237
+ agent knows it isn't done:
238
+
239
+ ```
240
+ error slide 3: unknown layout 'media-rite'
241
+ Did you mean 'media-right'?
242
+ error slide 7: image not found: media/chart.png
243
+ error slide 9: YouTube watch link will not embed
244
+ Use the /embed/ form: https://www.youtube.com/embed/VIDEO_ID
245
+ ```
246
+
247
+ ## Presenting
248
+
249
+ `glissade start` prints two addresses and a QR code:
250
+
251
+ | Address | Open it on |
252
+ | --- | --- |
253
+ | `http://<your-ip>:8000/` | the machine driving the projector — press **F** for fullscreen |
254
+ | `http://<your-ip>:8000/control` | your phone (scan the QR) |
255
+
256
+ Both devices need to be on the same Wi-Fi. The server holds the slide
257
+ position, so whoever advances — phone or laptop — everyone follows, and a
258
+ device that reconnects lands on the right slide.
259
+
260
+ The remote also has a **Live notes** editor for capturing feedback or action
261
+ items during the meeting. Those notes autosave per slide and can optionally be
262
+ shown on the projector as an overlay. The overlay is plain text: line breaks
263
+ are preserved, and the projector toggles it with <kbd>L</kbd>.
264
+
265
+ `glissade start` also watches your project while it is running. Edit a deck
266
+ JSON file, swap an image under `decks/`, or change `themes.json`, and every
267
+ connected display and remote refreshes to the newest valid version
268
+ automatically. If an edit leaves the deck invalid JSON, Glissade keeps showing
269
+ the last good version and logs the reload error instead.
270
+
271
+ `glissade start --open` opens the deck in your browser once the server is
272
+ actually accepting connections — put `open = true` in `glissade.toml` to make
273
+ that a project's default. Use `--no-watch` if you want a fixed presentation
274
+ that ignores on-disk changes.
275
+
276
+ On Windows the first run may raise a firewall prompt: allow private networks
277
+ so your phone can reach the remote. `--host 127.0.0.1` avoids it if you only
278
+ want the laptop.
279
+
280
+ ### The remote
281
+
282
+ The main view holds what you read while presenting — the current slide's
283
+ speaker notes, a preview of the next one, and capturing audience feedback as you go. A **Notes** drawer shows every captured
284
+ live note across the deck and jumps back to that slide when you tap an entry.
285
+ Everything else sits behind a header button: **Deck**, **Slides** (jump list),
286
+ **Notes** (captured live notes), and a gear for text size and theme. **Blank**
287
+ blacks out the projector.
288
+
289
+ Live notes are saved automatically to `.glissade/live-notes.json` in a normal
290
+ project. For the built-in demo decks, which live in the installed package,
291
+ Glissade keeps them in your user cache instead.
292
+
293
+ ### Keys on the projecting machine
294
+
295
+ | Key | Does |
296
+ | --- | --- |
297
+ | <kbd>←</kbd> <kbd>→</kbd> | navigate |
298
+ | <kbd>N</kbd> | speaker notes on screen |
299
+ | <kbd>L</kbd> | toggle the live-notes overlay |
300
+ | <kbd>B</kbd> | blank the screen |
301
+ | <kbd>D</kbd> | switch deck overlay |
302
+ | <kbd>+</kbd> <kbd>−</kbd> <kbd>0</kbd> | text size (70–160%) |
303
+ | <kbd>T</kbd> | cycle theme |
304
+ | <kbd>R</kbd> | reload a failed embed |
305
+ | <kbd>F</kbd> | fullscreen |
306
+
307
+ ## Writing a slide
308
+
309
+ Pick a `layout` and fill the fields it uses. Nothing is required.
310
+
311
+ ```jsonc
312
+ {
313
+ "title": "My talk",
314
+ "slides": [
315
+ {
316
+ "title": "Growth came from one region", // label in the jump list
317
+ "layout": "media-right",
318
+ "eyebrow": "Results",
319
+ "heading": "Growth came from one region",
320
+ "body": "<p>Everything else held flat.</p>",
321
+ "image": { "src": "media/q3.png", "alt": "Bar chart", "fit": "contain" },
322
+ "notes": "Don't read the chart aloud. Say the sentence and let them look."
323
+ }
324
+ ]
325
+ }
326
+ ```
327
+
328
+ Layouts: `title`, `title-content`, `section`, `title-only`, `two-content`,
329
+ `comparison`, `content-caption`, `picture-caption`, `media-right`,
330
+ `media-left`, `media-full`, `media-caption`, `grid`, `quad-chart`, `blank`.
331
+
332
+ `cls` adds modifiers independent of layout: `"ask"` (dark, for questions),
333
+ `"story"`, `"center"`.
334
+
335
+ Run `glissade demo --deck gallery` to see all fifteen.
336
+
337
+ ## Media
338
+
339
+ **Images embed, they don't link.** Point `src` at a file beside your deck; the
340
+ build inlines it as a data URI, downscaling anything over 2560px. The result is
341
+ one file with no folder to forget.
342
+
343
+ **Embeds** come in three kinds:
344
+
345
+ | Field | Behaviour | Needs network |
346
+ | --- | --- | --- |
347
+ | `"file": "media/game.html"` | Local HTML inlined as the iframe's `srcdoc` | No |
348
+ | `"srcdoc": "<html>…"` | Inline markup | No |
349
+ | `"src": "https://…"` | External page | **Yes** |
350
+
351
+ Embeds mount only while their slide is on screen — otherwise a video keeps
352
+ playing, audibly, after you've moved on. An external embed that can't load
353
+ shows a QR-code fallback rather than a blank frame; <kbd>R</kbd> retries.
354
+ `glissade build` lists every slide that depends on the network.
355
+
356
+ ## Themes
357
+
358
+ **Paper** (default), **Georgia Tech**, and **Texas A&M** ship with the tool.
359
+ Press <kbd>T</kbd> or pick one on the remote.
360
+
361
+ Drop a `themes.json` in your project to replace them with your own. Every
362
+ colour and typeface in the deck comes from a token, so one entry restyles
363
+ everything:
364
+
365
+ ```jsonc
366
+ {
367
+ "id": "housestyle",
368
+ "name": "House style",
369
+ "vars": {
370
+ "--paper": "#ffffff", // slide background
371
+ "--ink": "#111111", // headings
372
+ "--ink-soft": "#444444", // body
373
+ "--accent": "#8a6d2f", // eyebrows, citations
374
+ "--accent-light": "#c0a878", // quote bars, bullets
375
+ "--ask-bg": "#25303a", // discussion slides
376
+ "--deck-font": "Georgia, serif"
377
+ }
378
+ }
379
+ ```
380
+
381
+ Missing tokens fall back to the Paper defaults. Both university themes are
382
+ built from published brand swatches and every text/background pair clears
383
+ WCAG AA.
384
+
385
+ ## If the network is down
386
+
387
+ Open the built file from `build/`. No server, no network, everything embedded.
388
+ Build the night before — it costs a second and it's the difference between a
389
+ presentation and an apology.
390
+
391
+ ## Developing
392
+
393
+ Use `uv` plus the cross-platform task script:
394
+
395
+ ```bash
396
+ git clone https://github.com/techmuch/glissade && cd glissade
397
+ uv run python scripts/dev.py install
398
+ uv run python scripts/dev.py run
399
+ ```
400
+
401
+ That installs Python 3.12 if needed, creates `.venv`, and installs the editable
402
+ project with the `dev` and `images` extras. Override the Python version if you
403
+ need to:
404
+
405
+ ```bash
406
+ uv run python scripts/dev.py install --python 3.11
407
+ ```
408
+
409
+ Common tasks:
410
+
411
+ ```bash
412
+ uv run python scripts/dev.py install # bootstrap .venv
413
+ uv run python scripts/dev.py test # run the default test suite (excludes browser e2e)
414
+ uv run python scripts/dev.py test -- tests/test_check.py -q
415
+ uv run python scripts/dev.py run # defaults to: glissade demo
416
+ uv run python scripts/dev.py run -- start
417
+ uv run python scripts/dev.py build # build dist/ packages
418
+ uv run python scripts/dev.py e2e-install # install Playwright's browser binaries
419
+ uv run python scripts/dev.py e2e # run browser end-to-end tests
420
+ ```
421
+
422
+ If you prefer the raw `uv` commands:
423
+
424
+ ```bash
425
+ uv python install 3.12
426
+ uv venv .venv --python 3.12
427
+ uv pip install --python .venv/bin/python -e ".[dev,images]" # Windows: .venv\Scripts\python.exe
428
+ uv run --python .venv/bin/python pytest -q # Windows: .venv\Scripts\python.exe
429
+ uv run --python .venv/bin/python python -m playwright install chromium
430
+ uv run --python .venv/bin/python pytest -m e2e tests/e2e -q
431
+ ```
432
+
433
+ `src/glissade/` is the package; `templates/` holds the deck and remote HTML;
434
+ `data/` holds the themes, JSON schema, `init` scaffold and demo decks — all
435
+ shipped inside the wheel so the tool works from any directory.
436
+
437
+ Browser behavior is covered by Playwright end-to-end tests in `tests/e2e/`.
438
+ They start a real `glissade` server, drive both the projector and remote UIs,
439
+ and verify live navigation, live notes, and watch-mode reloads.
440
+
441
+ ```bash
442
+ uv build # sdist + universal wheel into dist/
443
+ ```