Files and the web
Keep the document for state worth saving
Section titled “Keep the document for state worth saving”For taking photos, recording audio and on-device transcription, see capabilities and permissions. Finished captures use the same attachment storage described below; ordinary file import needs no capability.
Save what a person would want back tomorrow. Drag positions, playback progress, timers and hover state belong in component state: writing them on every frame grows the document’s history for no benefit. Store files as attachments, never as base64 inside fields. A document’s saved data is capped at 32 MiB; a failed save keeps every edit and leaves the last saved version intact.
Opening, hiding and closing
Section titled “Opening, hiding and closing”Your app mounts once the document is open, and again after Reload Interface. Start
timers, observers and animation loops in $effect or onMount and return a cleanup.
Closing the window unmounts the app, which runs every cleanup. By then the document has
saved and closed, so a write made during teardown is refused; save values as they
change instead.
A minimized window, or one hidden with the app, reports document.visibilityState as
"hidden", and its timers all but stop. Pause work while hidden and catch up when the
window shows again. Timers don’t run while the Mac sleeps either, so derive elapsed time
from the clock rather than counting ticks.
<script lang="ts"> let visible = $state(document.visibilityState === "visible"); let now = $state(Date.now()); $effect(() => { if (!visible) return; const timer = setInterval(() => (now = Date.now()), 1000); return () => clearInterval(timer); }); function visibilityChanged() { visible = document.visibilityState === "visible"; if (visible) now = Date.now(); }</script>
<svelte:document onvisibilitychange={visibilityChanged} />Attachments
Section titled “Attachments”In a native window, an ordinary <input type="file"> opens a file picker attached to the
document. Cancelling changes nothing. Background rendering and closed CLI commands never
show a picker. Validate the chosen file in your app before storing it.
import { attachments } from "hitslop/svelte";
await attachments.import(file, (tx, ref) => { tx.fields.cover.$set(ref);});import stores the file, then runs your collector like a change: synchronously, with
its edits accepted together with the file or the promise rejects. Prepare the file
asynchronously before calling import. If storing the file fails, the collector never
runs; if the reference edit is refused, the file stays stored but unreferenced. A
reference is { id, name, mimeType, byteLength }; model it with ordinary fields. The name
is descriptive, never a path. The type comes from the file’s bytes, not its extension:
images, audio, video and PDFs keep theirs, and anything else is
application/octet-stream.
Show or play a file by its URL. It is same-origin and never changes, and media can seek:
<img src={attachments.url(ref.id)} alt={ref.name} /><audio controls src={attachments.url(ref.id)}></audio>attachments.read(id) returns the bytes as a Blob when your code needs them. A file
never runs as a script, stylesheet or page.
Files are immutable and deduplicated by content: up to 10 MiB each, 100 MiB and 256 files
per document. Store the reference’s id as it is, in a field or inside text: when the
document closes, files whose ID appears nowhere in it are removed, and a duplicate or shared
copy carries only the files it references. Close, export and duplicate wait for imports in
progress. Templates never contain imported files. slop dev stores them in each preview’s
own disposable document, with the same limits. From the terminal, use
attachments ref and --attach.
HTTPS data, media and frames
Section titled “HTTPS data, media and frames”A slop may fetch HTTPS data, play HTTPS media and embed HTTPS <iframe>s without
declaring them. Images may use HTTPS, data or blob URLs. CORS still applies, and remote
scripts and eval are blocked, so bundle your code. An embedded frame can’t reach the
document or the host; drive it through its own postMessage API.
YouTube
Section titled “YouTube”A slop’s page has no web origin, and YouTube’s player refuses to play without one
(error 153, or 150 from slop dev). Use hitslop/embed, which plays videos
through a relay page on hitslop.com. You don’t run a server, and the relay never sees which
video is playing. hitslop/youtube holds the URL builders.
import { youtube } from "hitslop/embed";import { src } from "hitslop/youtube";
const id = youtube.findId(pastedText); // null unless it holds a YouTube linkif (!id) throw new Error("Paste a YouTube link.");const { title, author } = await youtube.lookup(id); // rejects with a LookupError if it can't be embedded// <iframe bind:this={frame} src={src(id, { controls: false })}// allow="autoplay; encrypted-media" title="Player"></iframe>const player = youtube.player(frame, { onState, onTime, onEnded, onError });player.play();player.seek(30);player.setVolume(0.5);player.dispose();hitslop/youtube exports thumbnail(id), src(id, options), defaultRelay and idShape
without browser APIs; static views may import it, including Widget. These helpers only
build or validate strings: native capture cannot load a remote thumbnail. Keep an image
as an attachment in the editor and pass its saved ID to attachmentURL in the static
view. Static views may not import hitslop/embed, which owns youtube.parseId,
findId, lookup, player and LookupError. Playback needs the network; onError(-1)
means the relay never answered. Pass relay to src to host the relay page yourself.