@remotedraw/svelte
v0.5.0
Published
Svelte stores and typed clients for RemoteDraw receivers and senders.
Readme
@remotedraw/svelte
Svelte adapter for RemoteDraw. It wraps the framework-free
@remotedraw/client receiver store in the Svelte store contract, so receiver
state works with $store auto-subscription. It has no dependency on the
Svelte runtime — the store contract is structural.
npm install @remotedraw/svelte # npm
pnpm add @remotedraw/svelte # pnpm
yarn add @remotedraw/svelte # Yarn
bun add @remotedraw/svelte # bunTo use an unreleased build from a RemoteDraw checkout instead, see Working from a checkout.
Usage
<script lang="ts">
import {
createHttpReceiverClient,
createRemoteDrawReceiver,
} from "@remotedraw/svelte";
import { onDestroy } from "svelte";
export let session: {
id: string;
receiverToken: string;
joinUrl: string;
// From the create response, beside `session`: when the QR dies.
joinTokenExpiresAt: number;
};
const receiver = createRemoteDrawReceiver({
receiver: createHttpReceiverClient("https://api.remotedraw.com"),
session,
});
receiver.start();
onDestroy(() => receiver.stop());
</script>
<svg viewBox="0 0 1000 1000">
{#each $receiver.drawings as drawing (drawing.id)}
<polyline
points={drawing.points.map((p) => `${p.x * 1000},${p.y * 1000}`).join(" ")}
fill="none"
stroke="#151512"
stroke-width="6"
stroke-linecap="round"
/>
{/each}
</svg>All HTTP clients, joinTokenFromInput, geometry helpers, and types are
re-exported from @remotedraw/client, so one import covers backend session
clients, sender clients, and receiver state.
Open On My Phone
When the phone is the user's own app rather than a scan, createDirectSender
is the Svelte half of the direct-sender flow. It is the same state machine
React's useDirectSender binds (createDirectSenderController in
@remotedraw/client), so the two adapters cannot disagree about when a launch
has failed.
<script lang="ts">
import { createDirectSender, createRemoteDrawReceiver } from "@remotedraw/svelte";
export let receiver: ReturnType<typeof createRemoteDrawReceiver>;
const sender = createDirectSender({
receiver,
// YOUR endpoint, which calls `connectSender` with the rd_sk_... key.
// The key never reaches the browser.
connect: () =>
fetch("/api/remotedraw/direct-sender", { method: "POST" }).then((response) =>
response.json(),
),
});
</script>
<button on:click={() => sender.connect()} disabled={$sender.status === "connecting"}>
{$sender.status === "ready" ? "Waiting for your phone…" : "Draw on my phone"}
</button>
{#if $sender.status === "error"}
<p>{$sender.error?.message}</p>
<!-- Render your own QR from $receiver.joinUrl here. -->
{/if}| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| connect | () => Promise<DirectSenderResult \| null \| undefined> | — | required; your own backend call. It may return the raw connectSender response, a create-session response carrying it under senderConnection, or just { launchUrl } |
| receiver | the store from createRemoteDrawReceiver | — | supplies the session and sender list that drive drawing, submitted, and expired. Without it the store stops at ready |
| autoLaunch | boolean | true | false mints without navigating; call launch() yourself |
| openUrl | (url: string) => void | window.location.href = url | |
| onError | (error: RemoteDrawLaunchError) => void | — | |
$sender is { status, launchUrl, senderToken, senderId, connection, error },
where status is idle \| connecting \| ready \| drawing \| submitted \|
expired \| error and error is a RemoteDrawLaunchError with
kind: "connect-failed" \| "no-launch-url" \| "launch-blocked". The store also
exposes connect(), launch(), reset(), and stop() (which detaches from
the receiver store).
A custom scheme that nothing has registered fails silently — no error, no
navigation, no event — so the only evidence a launch failed is a status that
stays ready. Time it out yourself and show the QR; React's
RemoteDrawLaunchButton does exactly this with a 12 s default.
When a session pauses, is rate-limited or ends
$receiver.status says what the last read did, using the same table as every
kit (receiverFailureDisposition, re-exported from @remotedraw/client):
"retrying": a transient failure (too_many_requests,temporarily_unavailable,internal_error,request_timeout, network errors). The session and ink stay; the store polls again on its own, never before$receiver.retryAt(the server'sRetry-After). Nothing to do."paused": billing, scope or version (billing_limit_reached,wallet_balance_exhausted,billing_not_configured,included_capacity_exhausted,capability_not_granted,insufficient_scope,beta_access_required,sdk_too_old, other 4xx). The session and ink stay and reading stops until you resume."stopped"with$receiver.error: the session is over (invalid_receiver_token,session_not_found,session_not_active,receiver_lease_expired, 401/404/410), and$receiver.session.statusis"ended"(or"expired"). Create a new session.
refetch() or start() on a paused or stopped store is an explicit resume:
the failure clears in one update and one new read follows (after retryAt if
a Retry-After is still running). stop() keeps the halt for those
credentials, so a later start() resumes the same way; setSession() with
other credentials starts fresh.
{#if $receiver.status === "paused"}
<p>Drawing is paused ({$receiver.error?.message}).</p>
<button on:click={() => receiver.refetch()}>Try again</button>
{/if}The capture controller (createCaptureController, also re-exported) wraps
the same rules in phases and events: see When a session pauses, is
rate-limited or ends
in @remotedraw/client. For the capture components in a Svelte page, use
<remotedraw-capture> from @remotedraw/elements
(its "Frameworks" section covers Svelte 5). While paused it hides the board,
its pass reads "Code unavailable" and it shows no error line, so show the
reason from its remotedraw-error event: see
its pause section.
What is not here yet
Pairing UI, sender pads, and receiver rendering are React-only today: this
package deliberately has no .svelte files and no dependency on the Svelte
runtime, so anything visual is markup you write against the stores above. The
behavior contract those React components implement is documented in
docs/plans/2026-06-11-multi-framework-sdk.md so Svelte components can follow.
