UNPKG

@stainless-code/svelte-layers

Version:

Svelte adapter for @stainless-code/layers — call a layer like an async function and await its response.

462 lines (353 loc) 19.7 kB
--- name: svelte-layers description: Svelte bindings for @stainless-code/svelte-layers (runes + stores). Use to open overlay UI imperatively from anywhere, manage ordered named stacks, choose the Svelte 5.7+ runes or Svelte 3+ stores entry, and wire layer context or stack outlets. license: MIT keywords: - tanstack-intent - svelte - modal - dialog - stack - typescript metadata: library: "@stainless-code/svelte-layers" library_version: "0.2.4" framework: "svelte" sources: - https://stainless-code.com/layers/adapters/svelte/runes - https://github.com/stainless-code/layers/blob/main/docs/architecture.md --- # Svelte layer/stack UI with @stainless-code/svelte-layers Open any layer from anywhere with `client.open()` and manage modal, dialog, drawer, popover, and toast UI as ordered, named stacks. Awaiting a typed result and fire-and-forget with `void client.open(...)` are equally first-class; named stacks, singleton `upsert` with live `update`, serial queues, nested child stacks via `useLayerGroup`, async actions via `useMutationFlow`, transitions, blockers, and validation are valuable independently. Both entries re-export `@stainless-code/layers`, so adapter and core APIs share one import path. Use the [`layers` core skill](https://github.com/stainless-code/layers/blob/main/packages/core/skills/layers/SKILL.md) for engine behavior. ## When to use this skill Reach for `@stainless-code/svelte-layers` to open overlay UI imperatively from anywhere and manage ordered, named stacks instead of prop-drilling, lifting state, or threading callbacks. It fits when you need any of: - Open or close layers from anywhere. - Await a typed result or fire-and-forget; both are first-class. - Named stacks, singleton `upsert` with live `update`, or serial queues. - Nested child stacks via `useLayerGroup`, transitions, dismissal guards, or validation. Skip it only for a single, always-local overlay with no return, stacking, queue, animation, or guard needs and no wish for a global registry. Full fit matrix: [When to use Layers](https://stainless-code.com/layers/concepts/when-to-use). ## Install ```bash bun add @stainless-code/svelte-layers ``` Core is pulled in automatically. `svelte` is a required peer. ## Two entry points | Entry | Requires | Pick it when | | ------------------------------------- | --------------- | ----------------------------------------------------------------------------------------- | | `@stainless-code/svelte-layers` | Svelte **5.7+** | The app uses runes and Svelte 5 reactive component syntax. | | `@stainless-code/svelte-layers/store` | Svelte **3+** | The app uses the store contract, including Svelte 3/4 or a Svelte 5 store-based codebase. | Choose one entry for a stack surface. The runes entry depends on `createSubscriber` from `svelte/reactivity`; the stores entry depends on `readable` from `svelte/store`. ## Declare Mount Call ### Declare ```ts // layers/confirm.ts import { layerOptions } from "@stainless-code/svelte-layers"; import ConfirmDialog from "./ConfirmDialog.svelte"; export type ConfirmPayload = { title: string; }; export type ConfirmResponse = boolean; export const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({ stack: "confirm", key: ["confirm", "remove"], component: ConfirmDialog, exitingDelay: 200, }); ``` ```svelte <!-- ConfirmDialog.svelte --> <script lang="ts"> import type { LayerComponentProps } from "@stainless-code/svelte-layers"; import type { ConfirmPayload, ConfirmResponse } from "./confirm"; let { call, payload }: LayerComponentProps<ConfirmPayload, ConfirmResponse> = $props(); </script> <div role="dialog"> <h2>{payload.title}</h2> <button type="button" onclick={() => void call.end(true)}>Yes</button> <button type="button" onclick={() => void call.end(false)}>No</button> </div> ``` ### Mount — runes ```svelte <script lang="ts"> import { setLayerClient, useStack } from "@stainless-code/svelte-layers"; import { confirm } from "./layers/confirm"; setLayerClient(); const stack = useStack({ stack: "confirm" }); </script> {#each stack.current as s (s.id)} {@const call = stack.callFor(s)} {#if call} {@const Comp = confirm.component} <Comp {call} payload={s.payload} data={s.data} error={s.error} phase={s.phase} transition={s.transition} actionStatus={s.actionStatus} dismissing={s.dismissing} /> {/if} {/each} ``` ### Mount — stores In a store-based app, keep the declaration shape above but import `layerOptions` from `@stainless-code/svelte-layers/store`; importing the default runes entry requires Svelte 5.7+. ```svelte <script lang="ts"> import { setLayerClient, useStack, callFor } from "@stainless-code/svelte-layers/store"; import { confirm } from "./layers/confirm"; const client = setLayerClient(); const stack = useStack({ stack: "confirm" }); </script> {#each $stack as s (s.id)} {@const call = callFor(client, "confirm", s)} {#if call} <svelte:component this={confirm.component} {call} payload={s.payload} data={s.data} error={s.error} phase={s.phase} transition={s.transition} actionStatus={s.actionStatus} dismissing={s.dismissing} /> {/if} {/each} ``` The runes outlet uses the Svelte 5 dynamic-component idiom (`{@const Comp = confirm.component}` then `<Comp />`) and its stack handle's `.callFor`. The stores outlet may use `<svelte:component>` for Svelte 3/4 compatibility and the standalone `callFor`. The component and call snippets use Svelte 5 syntax. With Svelte 3/4, use legacy `export let` props and `on:click` events; see the [package README](https://github.com/stainless-code/layers/tree/main/packages/svelte#readme) for a complete example. ### Call ```svelte <script lang="ts"> import { createLayer } from "@stainless-code/svelte-layers"; import { confirm } from "./layers/confirm"; const c = createLayer(confirm); async function handleRemove() { const ok = await c.open({ title: "Remove?" }); if (!ok) return; deleteItem(); } </script> <button type="button" onclick={() => void handleRemove()}>Remove</button> ``` Low-level bag-form: `useLayerClient()` + `client.open({ ...confirm, payload })`. When `P` admits `undefined`, `payload` is optional; omit it instead of passing an empty or `undefined` value. With the default `void` response, a layer can also be fire-and-forget. Mount its `"notice"` stack with the runes outlet pattern above. ```ts // layers/notice.ts import { layerOptions } from "@stainless-code/svelte-layers"; import Notice from "./Notice.svelte"; export const notice = layerOptions<void>({ stack: "notice", key: ["notice"], component: Notice, }); ``` ```svelte <!-- Notice.svelte --> <script lang="ts"> import type { LayerComponentProps } from "@stainless-code/svelte-layers"; let { call }: LayerComponentProps<void> = $props(); </script> <p>Changes saved.</p> <button type="button" onclick={() => void call.dismiss()}>Dismiss</button> ``` ```svelte <!-- NoticeButton.svelte --> <script lang="ts"> import { createLayer } from "@stainless-code/svelte-layers"; import { notice } from "./layers/notice"; const c = createLayer(notice); </script> <button type="button" onclick={() => void c.open()}> Show notice </button> ``` ## The `call` context Call `setLayerClient()` or `setLayerClient(existingClient)` in a parent layout/root component. Descendants use `useLayerClient()` to read that `LayerClient`; it throws when no client exists in context. Each layer component receives: - `call`: `end`, `dismiss`, `update`, `setRunning`, `settle`, `ended`, `index`, `stackSize`, `root`, `stackId`, `layerId`, and `addBlocker` - state props: `payload`, `data`, `error`, `phase`, `transition`, `actionStatus`, and `dismissing` `await call.end(response)` resolves the caller's `await` and dismisses the layer. It returns `Promise<boolean>`; `false` means a blocker vetoed dismissal. `key` is logical identity for operations such as `find`, `upsert`, and `gcTime`; every mount gets a unique `id`. Key `{#each}` blocks with `s.id`, because parallel stacks may contain multiple same-key layers. ### Transitions Key CSS on `transition`; the configured delay or `call.settle()` completes the transition, whichever happens first: ```svelte <div role="dialog" data-transition={transition} ontransitionend={() => call.settle()} > <h2>{payload.title}</h2> </div> ``` Set `enteringDelay` / `exitingDelay` on `layerOptions`. For spring or variable-duration exits, combine a generous `exitingDelay` cap with `ontransitionend`. ### Rendering location The adapter has no built-in outlet or portal host. Render active layers inline in a layout with `useStack().current` and `callFor`, or move them with the app's portal pattern; nested child stacks from `useLayerGroup` follow the same render path. ## Wired handle: createLayer ```ts const c = createLayer(confirm); await c.open({ title: "Remove?" }); ``` Core headless factory is `createLayerHandle`. ## Subscribing to stacks Options-bag + optional trailing `client`; `select` (not `selector`). ### useStack / createQueuedStack Runes return a `SvelteStack` handle: read `.current` in a reactive context. ```svelte <script lang="ts"> import { useStack, createQueuedStack } from "@stainless-code/svelte-layers"; const stack = useStack({ stack: "confirm" }); const count = useStack({ stack: "confirm", select: (s) => s.length }); const queued = createQueuedStack({ stack: "confirm" }); </script> <p>{stack.current.length} open</p> ``` ### createLayerState / createLayerQueuedState Observe-only; `.current` is `LayerState[]` for all same-key instances: ```svelte <script lang="ts"> import { createLayerState } from "@stainless-code/svelte-layers"; import { confirm } from "./layers/confirm"; const mounted = createLayerState({ key: confirm.key, stack: "confirm" }); </script> {#if mounted.current.at(-1)} <span>{mounted.current.at(-1)!.payload.title}</span> {/if} ``` Stores entry: same names (`createLayer`, `createLayerState`, `createQueuedStack`, `createLayerQueuedState`); `useStack({ })` returns `Readable<T>` — auto-subscribe with `$stack`. ## Mutation flow `useMutationFlow(call)` coordinates a layer's `actionStatus: "running"` with async work. `run(fn).orEnd(response)` ends the layer on success; on failure it stays open and the error rethrows. ### Runes `pending` is a reactive `readonly boolean` — read `flow.pending` in markup. ```svelte <script lang="ts"> import { type LayerComponentProps, useMutationFlow } from "@stainless-code/svelte-layers"; let { call, payload }: LayerComponentProps<{ title: string }, boolean> = $props(); const flow = useMutationFlow(call); </script> <div role="dialog" aria-label={payload.title}> <button type="button" disabled={flow.pending} onclick={() => void flow.run(() => save()).orEnd(true)} > Save </button> </div> ``` ### Stores `pending` is a `Readable<boolean>` — destructure from the hook and subscribe with `$pending`. ```svelte <script lang="ts"> import { type LayerComponentProps, useMutationFlow } from "@stainless-code/svelte-layers/store"; let { call, payload }: LayerComponentProps<{ title: string }, boolean> = $props(); const { pending, run } = useMutationFlow(call); </script> <div role="dialog" aria-label={payload.title}> <button type="button" disabled={$pending} onclick={() => void run(() => save()).orEnd(true)}> Save </button> </div> ``` ## Nested stacks `useLayerGroup(call, options?)` creates a child stack scoped to the calling layer's lifetime. Returns `{ open, dismissAll, stack, stackId }`. On `onDestroy`, the group `dispose`s and `cancelAll`s child layers (`LayerCancelledError`). Render child layers inline with the group's stack handle. ### Runes `stack` is a `SvelteStack` — `{#each group.stack.current as s}` + `group.stack.callFor(s)`. ```svelte <script lang="ts"> import { type LayerComponentProps, useLayerGroup } from "@stainless-code/svelte-layers"; import { childLayer } from "./layers/child"; let { call, payload }: LayerComponentProps<{ title: string }, boolean> = $props(); const group = useLayerGroup(call); </script> <div role="dialog" aria-label={payload.title}> <button type="button" onclick={() => void group.open({ ...childLayer, payload: { label: "Child" } })} > Open child </button> {#each group.stack.current as s (s.id)} {@const childCall = group.stack.callFor(s)} {#if childCall} {@const Comp = childLayer.component} <Comp call={childCall} payload={s.payload} phase={s.phase} actionStatus={s.actionStatus} /> {/if} {/each} </div> ``` ### Stores `stack` is a `Readable<LayerState[]>` — `{#each $stack}` + `callFor(client, group.stackId, s)`. ```svelte <script lang="ts"> import { callFor, type LayerComponentProps, useLayerClient, useLayerGroup, } from "@stainless-code/svelte-layers/store"; import { childLayer } from "./layers/child"; let { call, payload }: LayerComponentProps<{ title: string }, boolean> = $props(); const client = useLayerClient(); const group = useLayerGroup(call); const stack = group.stack; </script> <div role="dialog" aria-label={payload.title}> <button type="button" onclick={() => void group.open({ ...childLayer, payload: { label: "Child" } })} > Open child </button> {#each $stack as s (s.id)} {@const childCall = callFor(client, group.stackId, s)} {#if childCall} <svelte:component this={childLayer.component} call={childCall} payload={s.payload} phase={s.phase} actionStatus={s.actionStatus} /> {/if} {/each} </div> ``` ## Adapter API | API | Runes entry | Stores entry | Role | | ------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | `setLayerClient` | `(client?) => LayerClient` | Same | Put a provided or new client in Svelte context. | | `useLayerClient` | `() => LayerClient` | Same | Read the nearest client; throws when absent. | | `createLayer` | `({ }, client?) => SvelteStack` wired handle + state/queued/top | Same | Drive + observe; `open(payload)` — no spread. | | `createLayerState` | `({ key, }, client?) => SvelteStack<LayerState[]>` | `Readable<LayerState[]>` | Observe-only, mounted. | | `createLayerQueuedState` | `({ key, }, client?) => SvelteStack<LayerState[]>` | `Readable<LayerState[]>` | Observe-only, queued. | | `useStack` | `({ stack?, select?, compare? }, client?) => SvelteStack` | `Readable<T>` | Whole-stack mounted. | | `createQueuedStack` | `({ stack?, select?, compare? }, client?) => SvelteStack` | `Readable<T>` | Whole-stack queued. | | `useMutationFlow` | `(call) => { readonly pending: boolean, run }` | `(call) => { pending: Readable<boolean>, run }` | Drive `actionStatus: "running"`; `run(fn).orEnd(response)` ends on success. | | `useLayerGroup` | `(call, options?) => { open, dismissAll, stack: SvelteStack, stackId }` | `(call, options?) => { open, dismissAll, stack: Readable<LayerState[]>, stackId }` | Child stack scoped to the calling layer; cleaned up on `onDestroy`. | | `SvelteStack` | `{ readonly current: T; callFor(state, rootProps?) }` || Reactive runes handle. | | `callFor` || `(client, stackId, state, rootProps?) => LayerCallContext \| null` | Build a call context; returns `null` if the layer is gone. | | `MutationFlow<R>` | `{ readonly pending: boolean, run }` | `{ pending: Readable<boolean>, run }` | `pending` mirrors `actionStatus: "running"`. | | `MutationRun<R>` | `{ orEnd(response) => Promise<void> }` | Same | Ends on success; on failure leaves the layer open and rethrows. | | `LayerGroup` | `{ open, dismissAll, stack: SvelteStack, stackId }` | `{ open, dismissAll, stack: Readable<LayerState[]>, stackId }` | `open` is stack-pre-bound; `dismissAll(response?)` clears the child stack. | ## Core re-exports (same import path) Both entries use `export * from "@stainless-code/layers"` — includes `createLayer`, `LayerHandle`, `ValidatedLayerHandle`, plus adapter types (`LayerComponentProps`, `SvelteStack`, `MutationFlow<R>`, ). Common core patterns: - **Key inference:** `layerOptions` / `layerKey` branding makes `await client.open(...)` infer its response. - **Singleton and live updates:** set `upsert: true` on `open`; update with `client.getStack(id).update(layer, patch)`. - **Serial scope:** configure `new LayerClient({ defaultStackOptions: { confirm: { scope: { strategy: "serial" } } } })`. - **Validation:** set `validate` on `layerOptions` or `open`; narrow `PayloadValidationError` with `isPayloadValidationError`. - **Blockers:** use `call.addBlocker` / `stack.addBlocker`, `dismissing`, and `dismissAll` modes. - **Nested stacks:** `useLayerGroup(call, options?)` wraps core `createLayerGroup` with Svelte lifetime cleanup; `childStackId` remains available for manual wiring. See the [`layers` core skill](https://github.com/stainless-code/layers/blob/main/packages/core/skills/layers/SKILL.md) for full engine coverage, the [architecture guide](https://github.com/stainless-code/layers/blob/main/docs/architecture.md) for package boundaries, and the [repository README](https://github.com/stainless-code/layers#readme) for project-level setup.