Files
deckcraft/packaging/web/README.md
T
Helen Kwok c7919102ac deckcraft-web: window.deckcraft, the page command API from craft-web-api
The web shell queues the page's calls as control requests (the channel the desktop control server uses) and answers them on the next frame. The API is installed in every mode; opening, saving, the file name and the unsaved flag stay with the host messages.
2026-11-07 08:05:09 +08:00

167 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Hosting DeckCraft for the web
`deckcraft-web-<version>.zip` (from the GitHub release, or `packaging/web/package.sh`) holds a
static site in `deckcraft-web-<version>/`:
| File | What it is |
|---|---|
| `index.html` | The page. It loads everything through relative URLs. |
| `deckcraft-web-<hash>.js` | wasm-bindgen glue (generated, ES module) |
| `deckcraft-web-<hash>_bg.wasm` | The app, size is printed by `packaging/web/package.sh` |
| `_headers`, `.htaccess` | Sample header rules for Netlify/Cloudflare Pages and Apache |
There is no server-side code. Upload the folder's contents anywhere that serves static files.
## Any path works
All URLs in `index.html` are relative (`public_url = "./"` in `apps/deckcraft-web/Trunk.toml`),
so the site works at a domain root (`https://example.com/`), under a prefix
(`https://example.com/tools/deckcraft/`) and from a CDN bucket. The asset names carry a content
hash, so they can be cached forever. Only `index.html` needs revalidation.
## Required server settings
- **MIME type:** serve `.wasm` as `application/wasm`. Browsers refuse to stream-compile it under
any other type, and the app then loads slowly or not at all. Serve `.js` as `text/javascript`.
Most hosts already do both. For nginx, check that `mime.types` has `application/wasm wasm;`.
- **Compression:** turn on gzip or Brotli for `.wasm`, `.js` and `.html`. That shrinks the
download by roughly 60%. You can also precompress (`brotli -k *.wasm`) and let
the server send `Content-Encoding: br`.
- **Caching:** `Cache-Control: public, max-age=31536000, immutable` on the hashed `.wasm` and
`.js` files, and `no-cache` on `index.html`.
- **HTTPS:** WebGPU (and the clipboard) only work in a secure context, which means `https://`
or `http://localhost`. Over plain HTTP elsewhere, the app falls back to WebGL2.
- **No special isolation headers:** DeckCraft doesn't use `SharedArrayBuffer`, so it doesn't
need `Cross-Origin-Opener-Policy` or `Cross-Origin-Embedder-Policy`. If your site already sends
COEP `require-corp`, also send `Cross-Origin-Resource-Policy: same-origin` (or `cross-origin`
when the files live on a CDN) on the app's files.
nginx example:
```nginx
location /deckcraft/ {
types { application/wasm wasm; text/javascript js; text/html html; }
gzip on;
gzip_types application/wasm text/javascript text/html;
location ~* \.(wasm|js)$ { add_header Cache-Control "public, max-age=31536000, immutable"; }
location ~* index\.html$ { add_header Cache-Control "no-cache"; }
}
```
Local test: `python3 -m http.server 8765` inside the folder, then open http://localhost:8765/.
## Embedding in a page (iframe)
```html
<iframe
src="https://example.com/deckcraft/"
title="DeckCraft presentations"
style="width: 100%; height: 720px; border: 0;"
allow="fullscreen; clipboard-read; clipboard-write"
allowfullscreen>
</iframe>
```
- The app fills the iframe and follows its size, so size the iframe and not the app.
- Keyboard shortcuts go to the iframe after the user clicks into it, as with any embedded app.
- **Cross-origin embeds** work. The web build keeps no preferences between visits; it starts
with the sample deck (or `?blank`) every time. Opening and saving through the page
(`?host=parent`, below) needs the same origin.
- **Sandboxed iframes** need at least
`sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"`. Without `allow-downloads`, Save and Export (browser
downloads) are blocked. With `?host=parent`, `allow-scripts allow-same-origin` is enough for
opening and saving (without `allow-same-origin` the frame's origin is `null` and there is no
bridge), and `allow-downloads` isn't needed.
- Don't send `X-Frame-Options: DENY` or a `frame-ancestors` CSP that excludes the embedding page.
## Opening and saving through the page (`?host=parent`)
A page that keeps the files itself (a Nextcloud app, a file server) can frame DeckCraft with
`?host=parent` on the iframe `src`, hand it a presentation and take back what it saves, instead of
the user downloading. GridCraft, WordCraft and PdfCraft have the same bridge with their own prefix.
The page and DeckCraft must be on the **same origin**: DeckCraft only hears and answers its parent
window on its own origin, and without one it downloads as usual.
Messages are plain objects with a `type`:
| Message | Direction | Fields |
|---|---|---|
| `deckcraft:ready` | to the page | `version`, `appVersion`. The app runs and listens: send the file now. |
| `deckcraft:open` | from the page | `name` (a bare file name, such as `Talk.pptx`), `bytes` (`ArrayBuffer` or `Uint8Array`). Opens like File › Open: a presentation (.pptx, .potx, .ppsx, .deckcraft) has that name as its file; an outline (.txt, .md, .rtf), a file sent without a name and a damaged file open untitled. |
| `deckcraft:save` | to the page | `name`, `saveAs`, `bytes` (a transferred `ArrayBuffer`), for every file DeckCraft would otherwise download. The page stores it. |
| `deckcraft:dirty` | to the page | `dirty`, whenever unsaved changes appear or go away. |
| `deckcraft:failed` | to the page | `error`, when the app couldn't start (no WebGPU or WebGL2) or refused a file of 2 GiB or larger. |
`saveAs` says whether the file becomes the presentation's file, as on the desktop:
- **Save** sends the file the page sent, under its own name and format (`Talk.pptx` comes back
as `Talk.pptx`), with `saveAs: false`. DeckCraft keeps only the part after the last `/`, so
`docs/Talk.pptx` also comes back as `Talk.pptx`. A presentation that has no file the page sent
(a new one, an outline, a file sent without a name, a file the user dropped, a repaired copy of
a damaged file) is a new file with `saveAs: true`: Ctrl+S names it `<title>.deckcraft` as Save
suggests on the desktop, and File › Save opens the Save As page first, as on the desktop.
- **Save As** (.pptx or .deckcraft) sends `saveAs: true`. The presentation is now that file; the
next Save sends its name with `saveAs: false`.
- **Exports and Print** (PDF, handouts, pictures, outline, GIF) send `saveAs: false` and are
copies: the presentation keeps its file and its unsaved changes.
`saveAs: false` doesn't mean "safe to overwrite": an export is `false` under a name the page may
already hold (`Talk.pdf`, or `Talk.pptx` exported from a presentation that is now
`Talk.deckcraft`). Replace a file without asking only when the name is the one the page sent, or
the name of the last `saveAs: true` file it stored for this presentation; ask for anything else.
DeckCraft doesn't learn the name the page finally stores a Save As under, so a page that renames
on a conflict should map later Saves of the old name itself.
In host mode DeckCraft starts with an empty presentation instead of the sample, and the first
presentation the page sends takes its place unless the user has changed it; later ones open
beside it. The page guards leaving itself (DeckCraft adds no guard of its own), and
`&author=Name` signs the user's comments.
```html
<iframe id="deck" src="/deckcraft/?host=parent&author=Ada" style="width: 100%; height: 720px; border: 0;"></iframe>
<script>
const frame = document.getElementById("deck");
addEventListener("message", async (e) => {
if (e.origin !== location.origin || e.source !== frame.contentWindow) return;
if (e.data.type === "deckcraft:ready") {
const bytes = await (await fetch("/files/Talk.pptx")).arrayBuffer();
frame.contentWindow.postMessage({ type: "deckcraft:open", name: "Talk.pptx", bytes }, location.origin, [bytes]);
} else if (e.data.type === "deckcraft:save") {
// Store e.data.bytes as e.data.name. Replace silently only when !e.data.saveAs and the name
// is the one sent (or the last saveAs file stored); ask before replacing anything else.
}
});
</script>
```
The messages and both transports are the shared contract of [`craft-web-host`](https://github.com/storytold/craft-libs/tree/main/crates/web-host) (storytold/craft-libs#420), the same for every Craft app: its README lists every message, including `saved`, with the compatibility notes. In `deckcraft:ready`, `version` is the contract's (`"1"`) and `appVersion` is this app's release. DeckCraft reads bytes, so a host sends `bytes`, not a `File`.
### Driving it from the page (`window.deckcraft`)
Every DeckCraft web build puts `window.deckcraft` on the page, from [`craft-web-api`](https://github.com/storytold/craft-libs/tree/main/crates/web-api) (storytold/craft-libs#413), the same shape in every Craft app. `deckcraft.request(method, params)` calls any method of the control channel the desktop app serves (`docs/control-protocol.md`), and `execute(command, params)`, `commands()`, `docInspect(params)`, `uiInspect(params)` and `screenshot(params)` are shortcuts for the common ones. Each returns a Promise that resolves with the method's result and rejects with an `Error`; a call the app does not answer within 30 seconds rejects.
```js
const deck = await deckcraft.docInspect(); // a summary of the open presentation
await deckcraft.execute("shape.insert", { preset: "roundRect", rect: [100, 100, 240, 120], text: "Hello" });
const { data } = await deckcraft.screenshot(); // the canvas as PNG (base64)
```
The API is there in every mode, with or without a host. Opening a file, handing a saved or exported file to the page, the file name and the unsaved flag stay with the host messages above.
## Renderer selection and fallback flags
DeckCraft renders with wgpu. It uses **WebGPU** when the browser has it and falls back to
**WebGL2** on its own. URL query flags override this, and they work on the iframe `src` too:
| Flag | Effect |
|---|---|
| *(none)* | WebGPU if available, otherwise WebGL2 |
| `?webgl` | Force the WebGL2 backend (useful when a WebGPU driver misbehaves) |
| `?blank` | Start with an empty presentation instead of the sample deck |
| `?show` | Start the slide show immediately (kiosk/embedded decks) |
| `?host=parent` | Open and save through the framing page ([above](#opening-and-saving-through-the-page-hostparent)); `&author=` names the user |
For example: `<iframe src="https://example.com/deckcraft/?webgl" ...>`.
A browser with neither WebGPU nor WebGL2 gets a message in place of the app.