Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/prefer-wired-handles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@stainless-code/vue-layers": patch
"@stainless-code/svelte-layers": patch
---

Prefer wired handles (`useLayer` / `createLayer`) in Confirm docs and fire-and-forget skill examples; bag-form `client.open` remains a valid escape hatch.
6 changes: 3 additions & 3 deletions apps/docs/content/adapters/angular.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import {
layerOptions,
provideLayerClient,
renderStack,
useLayerClient,
injectLayer,
type LayerComponentProps,
} from "@stainless-code/angular-layers";

Expand Down Expand Up @@ -98,15 +98,15 @@ const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({
template: `<button (click)="remove()">Remove</button><ng-container #outlet />`,
})
export class AppComponent {
private client = useLayerClient();
private c = injectLayer(confirm);
private vcr = inject(ViewContainerRef);

constructor() {
renderStack(this.vcr, "confirm");
}

async remove() {
await this.client.open({ ...confirm, payload: { title: "Remove item?" } });
await this.c.open({ title: "Remove item?" });
}
}
```
Expand Down
10 changes: 3 additions & 7 deletions apps/docs/content/adapters/preact.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ function App() {
```tsx
import {
layerOptions,
StackProvider,
StackOutlet,
useLayerClient,
useLayer,
type LayerComponentProps,
} from "@stainless-code/preact-layers";

Expand All @@ -74,12 +72,10 @@ const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({
});

function RemoveButton() {
const client = useLayerClient();
const c = useLayer(confirm);
return (
<button
onClick={() =>
void client.open({ ...confirm, payload: { title: "Remove item?" } })
}
onClick={() => void c.open({ title: "Remove item?" })}
>
Remove
</button>
Expand Down
10 changes: 3 additions & 7 deletions apps/docs/content/adapters/react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@ Same pattern across adapters — declare, mount, open:
```tsx
import {
layerOptions,
StackOutlet,
StackProvider,
useLayerClient,
useLayer,
type LayerComponentProps,
} from "@stainless-code/react-layers";

Expand All @@ -73,12 +71,10 @@ const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({
});

function RemoveButton() {
const client = useLayerClient();
const c = useLayer(confirm);
return (
<button
onClick={() =>
void client.open({ ...confirm, payload: { title: "Remove item?" } })
}
onClick={() => void c.open({ title: "Remove item?" })}
>
Remove
</button>
Expand Down
8 changes: 3 additions & 5 deletions apps/docs/content/adapters/solid.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
LayerClientContext,
LayerClient,
StackOutlet,
useLayerClient,
useLayer,
type LayerComponentProps,
} from "@stainless-code/solid-layers";

Expand All @@ -80,12 +80,10 @@ const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({
const client = new LayerClient();

function RemoveButton() {
const c = useLayerClient();
const c = useLayer(confirm);
return (
<button
onClick={() =>
void c.open({ ...confirm, payload: { title: "Remove item?" } })
}
onClick={() => void c.open({ title: "Remove item?" })}
>
Remove
</button>
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/content/adapters/svelte/runes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,26 +65,26 @@ Import client/stack APIs from **one** entry only. Mixing this package with `@sta
```svelte
<script lang="ts">
import {
createLayer,
layerOptions,
setLayerClient,
useLayerClient,
useStack,
} from "@stainless-code/svelte-layers";

type ConfirmPayload = { title: string };
type ConfirmResponse = boolean;

setLayerClient();
const client = useLayerClient();
const confirmStack = useStack({ stack: "confirm" });

const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({
stack: "confirm",
key: ["confirm", "remove"],
});
const c = createLayer(confirm);

async function remove() {
await client.open({ ...confirm, payload: { title: "Remove item?" } });
await c.open({ title: "Remove item?" });
}
</script>

Expand Down
4 changes: 3 additions & 1 deletion apps/docs/content/adapters/svelte/store.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Import client/stack APIs from **one** entry only. Mixing `@stainless-code/svelte
<script lang="ts">
import {
callFor,
createLayer,
layerOptions,
setLayerClient,
useLayerClient,
Expand All @@ -86,9 +87,10 @@ Import client/stack APIs from **one** entry only. Mixing `@stainless-code/svelte
stack: "confirm",
key: ["confirm", "remove"],
});
const c = createLayer(confirm);

async function remove() {
await client.open({ ...confirm, payload: { title: "Remove item?" } });
await c.open({ title: "Remove item?" });
}
</script>

Expand Down
6 changes: 3 additions & 3 deletions apps/docs/content/adapters/vue.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {
layerOptions,
provideLayerClient,
StackOutlet,
useLayerClient,
useLayer,
type LayerComponentProps,
} from "@stainless-code/vue-layers";

Expand Down Expand Up @@ -79,10 +79,10 @@ const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({
});

provideLayerClient();
const client = useLayerClient();
const c = useLayer(confirm);

async function remove() {
await client.open({ ...confirm, payload: { title: "Remove item?" } });
await c.open({ title: "Remove item?" });
}
</script>

Expand Down
18 changes: 12 additions & 6 deletions apps/docs/content/concepts/identity-and-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ search:
tags: ["concept"]
---

Two opens with the same `key` share **logical identity**; each still gets a unique instance id (**physical identity**). `DataTag` branding carries the response type through `await client.open(...)`, so no explicit generic is needed.
Two opens with the same `key` share **logical identity**; each still gets a unique instance id (**physical identity**). `DataTag` branding carries the response type through `await confirm.open(...)` (wired handle or bag-form), so no explicit generic is needed.

## Key vs instance id

Expand All @@ -20,7 +20,7 @@ Two opens with the same `key` share **logical identity**; each still gets a uniq

## Wired handle path

Besides `client.open({ …layerOptions, payload })`, bind a declaration with `createLayer(options, client)` (headless) or `useLayer(options)` (adapter). Payload-only `open`/`upsert` infer `R` from the same `DataTag` on `options.key`:
Bind a declaration with `useLayer(options)` (adapter) or `createLayer(options, client)` (headless). Payload-only `open`/`upsert` infer `R` from the same `DataTag` on `options.key`:

```tsx
import { useLayer } from "@stainless-code/react-layers";
Expand All @@ -32,12 +32,14 @@ const ok = await confirmLayer.open({ title: "Remove?" });

Pass `{ id: state.id }` on `dismiss`/`update`/`cancelQueued` when targeting a specific instance — `current` on the handle is the caller's last `open`, not an implicit default for stack ops.

Bag-form `client.open({ …layerOptions, payload })` stays valid — see [Migration](/reference/migration).

## DataTag — typed `await open`

`layerOptions` brands its `key` with a `DataTag`, so `LayerClient.open` infers the response type with no explicit generic:
`layerOptions` brands its `key` with a `DataTag`, so handle `open` and `LayerClient.open` infer the response type with no explicit generic:

```ts
import { LayerClient, layerOptions } from "@stainless-code/layers";
import { LayerClient, createLayer, layerOptions } from "@stainless-code/layers";

type ConfirmPayload = { title: string };
type ConfirmResponse = boolean;
Expand All @@ -49,7 +51,8 @@ const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({
key: ["confirm", "remove"],
});

const ok = await client.open({ ...confirm, payload: { title: "Remove?" } });
const c = createLayer(confirm, client);
const ok = await c.open({ title: "Remove?" });
// ^? boolean
```

Expand All @@ -72,12 +75,15 @@ There is **no** global key→response registry. Colocated `layerOptions` / `laye
The response type `R` defaults to `void` (fire-and-forget), and `payload` is omittable when the layer needs no input:

```ts
import { createLayer } from "@stainless-code/layers";

const about = layerOptions({
stack: "modal",
key: ["about"],
});

void client.open(about); // fire-and-forget
const c = createLayer(about, client);
void c.open(); // fire-and-forget
```

## Register and DefaultLayerError
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/content/concepts/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ search:
tags: ["concept"]
---

A confirm, toast, or drawer rarely stays where it started — prop-drilled `isOpen` and threaded callbacks don't scale. Layers moves that into a **headless client**: declare once, mount an outlet, `await client.open(...)` (or a wired handle) from any call site. Zero-dep core; thin adapters bind each framework's reactivity.
A confirm, toast, or drawer rarely stays where it started — prop-drilled `isOpen` and threaded callbacks don't scale. Layers moves that into a **headless client**: declare once, mount an outlet, `await confirm.open(...)` from any call site (wired handle or bag-form). Zero-dep core; thin adapters bind each framework's reactivity.

## Engine model

Expand All @@ -16,7 +16,7 @@ LayerClient ──┬── LayerStack (named, ordered) ── Layer[]

1. **Declare** — `layerOptions<P, R>({ stack, key, ... })` brands the <Tooltip tip="The logical identity of a layer; find/upsert/gcTime operate on its signature.">key</Tooltip> with a `DataTag` so `open()` infers the response type.
2. **Observe** — subscribe to a stack snapshot; adapters bind `LayerStack.subscribe` / `getSnapshot` to UI reactivity.
3. **Call** — `await client.open({ ...options, payload })` or a wired handle's `open(payload)`; resolution happens when something calls `call.end(response)` or `call.dismiss(response)`.
3. **Call** — a wired handle's `open(payload)` (or bag-form `client.open({ ...options, payload })`); resolution happens when something calls `call.end(response)` or `call.dismiss(response)`.

## Package boundary

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/concepts/when-to-use.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ search:

Overlay UI is cross-cutting, but component models push its state local. You end up prop-drilling `isOpen`, lifting state, threading `onConfirm`/`onCancel`, duplicating modal boilerplate, and hand-rolling z-order and one-at-a-time logic. Layers moves overlay state into a headless `LayerClient` you invoke imperatively: declare a layer once, open it from anywhere.

Awaiting a result is **optional** — `void client.open({ ...toast, payload })` is a complete fire-and-forget call; the typed response is a bonus axis, not a requirement.
Awaiting a result is **optional** — `void toast.open(payload)` on a wired handle is a complete fire-and-forget call; the typed response is a bonus axis, not a requirement.

| Use case | What it involves | Fit |
| --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
Expand Down
31 changes: 24 additions & 7 deletions apps/docs/content/guides/awaiting-results.mdx
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
---
title: Awaiting results
description: Await typed responses from client.open, infer types via DataTag, or fire-and-forget with void.
description: Await typed responses from a wired handle (or bag-form open), infer types via DataTag, or fire-and-forget with void.
search:
tags: ["guide"]
---

Need a boolean back from a confirm? `await client.open(...)` returns `Promise<R>`, and the response type flows from your layer definition with no explicit generic. Awaiting is optional fire-and-forget is equally first-class.
Need a boolean back from a confirm? Wire the declaration, then `await confirm.open(...)` `Promise<R>` with no call-site generic. Awaiting is optional; fire-and-forget is equally first-class.

## Typed await

Brand the contract once with `layerOptions<P, R>()`. The `key` carries a `DataTag`, so `await client.open({ ...confirm, payload })` infers `R`:
Brand the contract once with `layerOptions<P, R>()`. The `key` carries a `DataTag`, so payload-only `open` infers `R`:

```tsx lineNumbers
import { createLayer } from "@stainless-code/layers";

const confirm = layerOptions<ConfirmPayload, boolean>({
stack: "confirm",
key: ["confirm", "remove"],
component: ConfirmDialog,
});

const ok = await client.open({ ...confirm, payload: { title: "Remove?" } });
const c = createLayer(confirm, client);
const ok = await c.open({ title: "Remove?" });
// ^? boolean
```

You can also brand a key alone with `layerKey<R>()(key)` when options are inlined at the call site. See [Identity and types](/concepts/identity-and-types) for how `DataTag` inference works.
Adapters bind the same handle with `useLayer` / `injectLayer` / `createLayer` (see tabs below). You can also brand a key alone with `layerKey<R>()(key)` when options are inlined at the call site. See [Identity and types](/concepts/identity-and-types).

## Open from your framework

`client.open({ …layerOptions, payload })` works everywhere. Adapters also ship a **wired handle** — `useLayer(options)` (Svelte: `createLayer`; Angular: `injectLayer`) — for payload-only `open` plus reactive `state`/`queued`/`top`:
Adapters ship a **wired handle** — `useLayer(options)` (Svelte: `createLayer`; Angular: `injectLayer`) — for payload-only `open` plus reactive `state`/`queued`/`top`:

<Tabs>
<Tab title="React" icon="/icons/react.svg">
Expand Down Expand Up @@ -184,18 +187,32 @@ async function remove() {
</Tab>
</Tabs>

## Bag-form (escape hatch)

`client.open({ …layerOptions, payload })` stays valid — headless call sites, dynamic keys, or when you already hold a `LayerClient`:

```ts lineNumbers
const ok = await client.open({ ...confirm, payload: { title: "Remove?" } });
// ^? boolean
```

See [Migration](/reference/migration) — bag-form remains first-class alongside Drive handles.

## Fire-and-forget

When a layer needs no return value, omit the response generic (defaults to `void`) and skip `await`:

```tsx
import { createLayer } from "@stainless-code/layers";

const about = layerOptions({
stack: "modal",
key: ["about"],
component: About,
});

void client.open(about);
const c = createLayer(about, client);
void c.open();
```

The layer dismisses itself — typically via `call.dismiss()` inside the component. Use `void` to signal intentional fire-and-forget when your linter flags floating promises.
Expand Down
Loading
Loading