Skip to content

feat: add Lit adapter at full matrix parity#7

Merged
SutuSebastian merged 17 commits into
mainfrom
feat/lit-adapter
Jul 17, 2026
Merged

feat: add Lit adapter at full matrix parity#7
SutuSebastian merged 17 commits into
mainfrom
feat/lit-adapter

Conversation

@SutuSebastian

@SutuSebastian SutuSebastian commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Ship @stainless-code/lit-layers — Reactive Controllers + @lit/context, light-DOM <stack-outlet> / <stack-subscribe> / <app-host>, shadow <stack-provider>, explicit defineStackElements(), full matrix (useLayer, useLayerGroup.outlet(), createStackHook, etc.)
  • Docs + recipes + skill at seven-adapter parity; plan retired (delete + lift into architecture.md); changeset for the new package
  • Shared lazy context resolve so useLayer / useLayerGroup / useAppStack / useStackHandles stay on one client across provider swaps

Test plan

  • bun test packages/lit/src/index.test.ts (24)
  • bun run --filter @stainless-code/lit-layers test:dom (20)
  • bun run --filter @stainless-code/lit-layers typecheck
  • CI green on this PR
  • Post-merge (manual): GitHub About description + lit topic per retired plan closeout

Summary by CodeRabbit

  • New Features

    • Added a full Lit adapter for overlay layers, including Reactive Controllers, context-based client access, stack outlets, subscriptions, nested layers, and typed layer workflows.
    • Added Lit custom elements and light-DOM rendering support.
    • Added Lit installation guidance, examples, and API documentation.
  • Documentation

    • Updated framework support references, comparison tables, guides, and examples to include Lit.
  • Tests

    • Added comprehensive Lit unit, DOM, and type-inference coverage.

Ship @stainless-code/lit-layers with controllers, @lit/context, light-DOM
CEs, docs, skill, and recipes so Lit/WC apps match other adapters.
Reactive CE props, shadow stack-provider, createStackHook DOM tests, and
docs parity so the seventh adapter matches Lit idioms and the public site.
Landing parity/hero counts include Lit; StackOutlet rebinds on context
client swap; unit test covers validated useLayer parsed payload.
Lit ships as a first-class adapter; keep the comparison matrices honest.
LayerController deferred StackControllers until a single resolveClientLazy
callback, cutting three ContextConsumers to one for lazy useLayer hosts.
Defer #states until one resolveClientLazy callback, bindClient + requestUpdate
so outlet() no longer depends on StackController coincidentally re-rendering.
Core skill, glossary, CONTRIBUTING peers, and architecture DOM-harness
notes were still six-adapter shaped after the Lit package landed.
StackController ignored later context pushes once bound; share one
resolve for useAppStack, cover upsert/void/id dismiss, and sync Lit
into issue templates and architecture language.
Pass explicit createStackHook clients into StackController so open and
states cannot diverge; share resolve on useStackHandles; add headless
DOM coverage and close remaining Lit docs roster gaps.
Pin StackOutlet instance stability across actionStatus updates and add
Lit to headless-rendering / nested-overlays guide surfaces.
Delete+lift remaining Lit locks into architecture, drop roadmap pending
rows, and record the new @stainless-code/lit-layers package for release.
@changeset-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9aae20e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@stainless-code/lit-layers Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@SutuSebastian, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 56a1b475-0a1a-424a-97bd-da7922464fd5

📥 Commits

Reviewing files that changed from the base of the PR and between d5515f7 and 9aae20e.

📒 Files selected for processing (14)
  • .agents/skills/harden-pr/LEDGER.md
  • .size-limit.json
  • apps/docs/components/blume/Footer.astro
  • apps/docs/content/adapters/lit.mdx
  • apps/docs/content/concepts/glossary.mdx
  • apps/docs/content/guides/headless-rendering.mdx
  • apps/docs/content/guides/nested-overlays.mdx
  • apps/docs/content/reference/adapter-hooks.mdx
  • apps/docs/pages/_home/FrameworkTabs.astro
  • apps/docs/pages/_home/Hero.astro
  • apps/docs/pages/_home/Stability.astro
  • apps/docs/pages/_home/adapter-snippets.ts
  • apps/docs/recipes/confirm-dialog/lit.ts
  • packages/lit/src/index.ts
📝 Walkthrough

Walkthrough

This PR adds @stainless-code/lit-layers, including Lit Reactive Controllers, context-based client provisioning, stack custom elements, rendering and mutation APIs, tests, package configuration, documentation, and repository-wide adapter coverage updates.

Changes

Lit adapter integration

Layer / File(s) Summary
Runtime controllers and custom elements
packages/lit/src/index.ts, packages/lit/package.json, packages/lit/*.config.ts
Adds client context, reactive stack and layer controllers, headless rendering, mutation flows, nested groups, StackProvider, StackOutlet, StackSubscribe, app-level stack hooks, and idempotent custom-element registration.
Behavioral and type validation
packages/lit/src/index.test.ts, packages/lit/tests-dom/*
Tests lifecycle, context resolution and rebinding, rendering, controlled layers, headless flows, mutation handling, nested groups, registration, and generic inference.
Lit documentation and examples
packages/lit/README.md, packages/lit/skills/*, apps/docs/content/adapters/lit.mdx, apps/docs/recipes/confirm-dialog/lit.ts, apps/docs/content/guides/*
Documents installation, client provisioning, rendering elements, controllers, ergonomic APIs, and confirm-dialog, headless, and nested-overlay examples.
Repository adapter coverage
README.md, .github/*, docs/*, apps/docs/*, .agents/*, packages/core/skills/*, knip.json, .size-limit.json, .changeset/*
Adds Lit to package listings, adapter matrices, issue guidance, architecture and glossary content, workspace discovery, size checks, release metadata, and framework coverage counts.

Estimated code review effort: 5 (Critical) | ~120 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a Lit adapter with full matrix parity.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/lit-adapter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Split Lit out of the React-shaped adapter-hooks matrix, fix nested
group init, restore verbatim experimental copy, and slim StackController
JSDoc so the design note lives on the API surface.
Lift the layerClientContext export rationale into JSDoc and keep the
harden ledger as a one-line suppress.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (3)
packages/lit/tests-dom/layer-handles.test.ts (2)

149-167: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Move custom element definition to the module scope.

Defining custom elements inside an it block risks triggering a DOMException (registry collisions) if the test suite is ever re-run in watch mode without a complete JavaScript environment teardown.

Move the StackProbe class and the customElements.define call outside the describe block, alongside ConfirmDialog and LayerHost.

♻️ Proposed refactor
-    class StackProbe extends LitElement {
-      `#stack` = useStack(this, {
-        stack: "confirm",
-        select: (s) => s.length,
-      });
-
-      createRenderRoot(): this {
-        return this;
-      }
-
-      get count(): number {
-        return this.#stack.current;
-      }
-
-      render() {
-        return html`<span>${this.#stack.current}</span>`;
-      }
-    }
-    customElements.define("test-stack-probe", StackProbe);

Note: Place the extracted class definition at the root module scope of this file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit/tests-dom/layer-handles.test.ts` around lines 149 - 167, Move
the StackProbe class and its customElements.define call from the test block to
the module scope, alongside ConfirmDialog and LayerHost. Keep its useStack
configuration, createRenderRoot, count getter, and render behavior unchanged,
and ensure the tests reference the module-scoped custom element.

89-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use afterEach for robust DOM cleanup.

If a test assertion fails, execution immediately exits before reaching the manual document.body.removeChild(...) cleanup step. This can leave orphaned elements in the DOM, polluting subsequent tests.

Consider importing afterEach from vitest and using it to reliably clear the DOM, which allows you to safely remove the manual cleanup lines at the end of every test.

♻️ Proposed refactor
-describe("Lit adapter — useLayer lazy client", () => {
+describe("Lit adapter — useLayer lazy client", () => {
+  afterEach(() => {
+    document.body.innerHTML = "";
+  });
+
   it("dispatches one layer-client context-request on the useLayer host", async () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit/tests-dom/layer-handles.test.ts` around lines 89 - 90, Use
Vitest’s afterEach in the “Lit adapter — useLayer lazy client” test suite to
reliably clean up DOM elements even when assertions fail. Move the shared
document.body cleanup from each test’s manual teardown into the afterEach hook,
then remove the redundant per-test cleanup lines.
docs/architecture.md (1)

39-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reorder table columns to match the canonical framework list.

As per coding guidelines, the adapter list order for every multi-adapter listing should be: core/vanilla → react → preact → solid → angular → vue → lit → svelte. The current matrix has Vue preceding Solid and Angular.

♻️ Proposed refactor
-| Capability                                             | React | Preact | Vue | Solid | Angular | Lit | Svelte (runes) | Svelte (store) |
-| ------------------------------------------------------ | :---: | :----: | :-: | :---: | :-----: | :-: | :------------: | :------------: |
-| Primitives + core re-export                            |  ✅   |   ✅   | ✅  |  ✅   |   ✅    | ✅  |       ✅       |       ✅       |
-| `StackOutlet` (built-in host)                          |  ✅   |   ✅   | ✅  |  ✅   |   ✅¹   | ✅  |       —⁴       |       —⁴       |
-| `StackSubscribe` (render-prop / scoped slot)           |  ✅   |   ✅   | ✅  |  ✅   |   —²    | ✅  |       —⁴       |       —⁴       |
-| `useStackHandles` (headless `{ states, getCall }`)     |  ✅   |   ✅   | ✅  |  ✅   |   ✅    | ✅  |       —⁵       |       —⁵       |
-| `useMutationFlow` (async-action helper)                |  ✅   |   ✅   | ✅  |  ✅   |   ✅    | ✅  |       ✅       |       ✅       |
-| `useLayerGroup` (nested-stack hook)                    |  ✅   |   ✅   | ✅  |  ✅   |   ✅³   | ✅  |      ✅⁶       |      ✅⁶       |
-| `createStackHook` (`useAppStack`/`AppHost`/`AppLayer`) |  ✅   |   ✅   | ✅  |  ✅   |   ✅³   | ✅  |       —⁴       |       —⁴       |
+| Capability                                             | React | Preact | Solid | Angular | Vue | Lit | Svelte (runes) | Svelte (store) |
+| ------------------------------------------------------ | :---: | :----: | :---: | :-----: | :-: | :-: | :------------: | :------------: |
+| Primitives + core re-export                            |  ✅   |   ✅   |  ✅   |   ✅    | ✅  | ✅  |       ✅       |       ✅       |
+| `StackOutlet` (built-in host)                          |  ✅   |   ✅   |  ✅   |   ✅¹   | ✅  | ✅  |       —⁴       |       —⁴       |
+| `StackSubscribe` (render-prop / scoped slot)           |  ✅   |   ✅   |  ✅   |   —²    | ✅  | ✅  |       —⁴       |       —⁴       |
+| `useStackHandles` (headless `{ states, getCall }`)     |  ✅   |   ✅   |  ✅   |   ✅    | ✅  | ✅  |       —⁵       |       —⁵       |
+| `useMutationFlow` (async-action helper)                |  ✅   |   ✅   |  ✅   |   ✅    | ✅  | ✅  |       ✅       |       ✅       |
+| `useLayerGroup` (nested-stack hook)                    |  ✅   |   ✅   |  ✅   |   ✅³   | ✅  | ✅  |      ✅⁶       |      ✅⁶       |
+| `createStackHook` (`useAppStack`/`AppHost`/`AppLayer`) |  ✅   |   ✅   |  ✅   |   ✅³   | ✅  | ✅  |       —⁴       |       —⁴       |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/architecture.md` around lines 39 - 49, Reorder the capability matrix
columns in the architecture table to the canonical sequence: core/vanilla,
React, Preact, Solid, Angular, Vue, Lit, Svelte. Update the header, separator,
every capability row, and associated footnote alignment without changing any
capability values or footnote text.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/docs/content/adapters/lit.mdx`:
- Around line 31-40: The Lit examples create duplicate ContextProvider
controllers on reconnect by calling provideLayerClient in connectedCallback. In
apps/docs/content/adapters/lit.mdx lines 31-40, move provideLayerClient(this) to
a class field on MyShell and remove the connectedCallback initialization. In
apps/docs/recipes/confirm-dialog/lit.ts lines 89-93, likewise initialize the
provider as a class field and lift defineStackElements() to module scope.

In `@packages/lit/src/index.test.ts`:
- Around line 377-396: Update the useMutationFlow success test to await the
promise returned by run(...).orEnd("ok") instead of tracking the locally
assigned resolved flag. After it completes, assert flow.pending is false and
verify the host layer was removed, using the existing host/layer inspection API.

In `@packages/lit/src/index.ts`:
- Around line 1192-1245: Update the client/context handling in `#init`, updated,
and connectedCallback to track the latest context client separately from the
explicit client. Ignore context callbacks while .client is set, preserve
explicit-client precedence during provider swaps, and when .client is cleared
rebind to the most recent context client.
- Around line 1620-1624: Update defineStackElements to validate existing
registrations: when customElements.get(tag) returns a constructor, compare it
with the expected ctor and throw a descriptive integration error if they differ;
retain definition for unregistered tags and accept registrations already owned
by the same ctor.
- Around line 804-817: Update the MutationRun implementation returned by run so
concurrent orEnd invocations cannot prematurely clear shared state or call
this.#call.end while another operation remains active. Either reject a new
invocation while one is in flight, or track active runs with a count and only
reset `#pending`, update the host, and finalize the call after the last run
completes; preserve cleanup for rejected or failed operations.
- Around line 1520-1533: Update the open flow in the method containing `#open`,
`#opened`, and `#pendingOpen` so synchronous throws from this.#client.open and
asynchronous promise rejections both reset `#opened` to false and `#pendingOpen` to
null, preserving retryability. Attach rejection handling to the pending promise
so failures do not become unhandled rejections, while retaining the existing
successful-response guard and onResolved behavior.
- Around line 1440-1458: Update AppLayerController so assignments to payload,
options, and onResolved synchronize the currently mounted layer whenever it is
open, using accessors or an explicit update method rather than plain fields.
Ensure options.key changes do not alter the key used to dismiss the
already-owned layer; retain and use the original mounted-layer identity for
later dismissal.
- Around line 1473-1477: Update the controller’s layer lifecycle around the
client-resolution branch and the close/disconnect paths to retain the exact
client and unique layer identity returned when this controller opens its layer.
When the context client changes, dismiss the previously owned layer before
replacing `#client`, then reopen or resynchronize on the new client as
appropriate. Replace stack.find(key)-based dismissal with operations targeting
only the stored layer instance, and clear the stored ownership state when the
layer is closed or disconnected.
- Around line 1551-1555: Update StackHook.AppLayer and BoundAppLayer so their
constructors/classes remain generic over payload and response types instead of
collapsing them to unknown; ensure new AppLayer(this, { payload, onResolved })
preserves typed P/R inference through the returned hook, and add the
corresponding assertion in inference.test-d.ts.

In `@packages/lit/tests-dom/stack-outlet.test.ts`:
- Around line 209-230: Strengthen the “stops updating after disconnect” test
around provider.remove() by opening the “After disconnect” layer, waiting for
any relevant update completion, and asserting the detached outlet does not
render that content. Keep the existing no-throw assertion, but verify the
outlet’s rendered output remains unchanged after disconnection.

---

Nitpick comments:
In `@docs/architecture.md`:
- Around line 39-49: Reorder the capability matrix columns in the architecture
table to the canonical sequence: core/vanilla, React, Preact, Solid, Angular,
Vue, Lit, Svelte. Update the header, separator, every capability row, and
associated footnote alignment without changing any capability values or footnote
text.

In `@packages/lit/tests-dom/layer-handles.test.ts`:
- Around line 149-167: Move the StackProbe class and its customElements.define
call from the test block to the module scope, alongside ConfirmDialog and
LayerHost. Keep its useStack configuration, createRenderRoot, count getter, and
render behavior unchanged, and ensure the tests reference the module-scoped
custom element.
- Around line 89-90: Use Vitest’s afterEach in the “Lit adapter — useLayer lazy
client” test suite to reliably clean up DOM elements even when assertions fail.
Move the shared document.body cleanup from each test’s manual teardown into the
afterEach hook, then remove the redundant per-test cleanup lines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a09dc40f-f653-4873-9164-4f30b67ac192

📥 Commits

Reviewing files that changed from the base of the PR and between 1d9e864 and d5515f7.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (53)
  • .agents/skills/docs-voice/SKILL.md
  • .agents/skills/harden-pr/LEDGER.md
  • .agents/skills/improve-codebase-architecture/LANGUAGE.md
  • .changeset/lit-adapter.md
  • .github/CONTRIBUTING.md
  • .github/ISSUE_TEMPLATE/1_bug.yml
  • .github/ISSUE_TEMPLATE/2_feature_adapter.yml
  • .size-limit.json
  • README.md
  • apps/docs/blume.config.ts
  • apps/docs/content/adapters/index.mdx
  • apps/docs/content/adapters/lit.mdx
  • apps/docs/content/adapters/meta.ts
  • apps/docs/content/concepts/comparison.mdx
  • apps/docs/content/concepts/glossary.mdx
  • apps/docs/content/concepts/overview.mdx
  • apps/docs/content/concepts/stability.mdx
  • apps/docs/content/concepts/when-to-use.mdx
  • apps/docs/content/examples/confirm-dialog.mdx
  • apps/docs/content/guides/headless-rendering.mdx
  • apps/docs/content/guides/install.mdx
  • apps/docs/content/guides/nested-overlays.mdx
  • apps/docs/content/reference/adapter-hooks.mdx
  • apps/docs/content/reference/core-api.mdx
  • apps/docs/content/reference/index.mdx
  • apps/docs/content/reference/migration.mdx
  • apps/docs/content/reference/roadmap.mdx
  • apps/docs/package.json
  • apps/docs/pages/_home/FrameworkTabs.astro
  • apps/docs/pages/_home/Hero.astro
  • apps/docs/pages/_home/UseCases.astro
  • apps/docs/pages/_home/adapter-snippets.ts
  • apps/docs/recipes/confirm-dialog/lit.ts
  • apps/docs/tsconfig.recipes-lit.json
  • docs/architecture.md
  • docs/glossary.md
  • docs/plans/lit-adapter.md
  • docs/roadmap.md
  • knip.json
  • packages/core/skills/layers/SKILL.md
  • packages/lit/README.md
  • packages/lit/package.json
  • packages/lit/skills/lit-layers/SKILL.md
  • packages/lit/src/index.test.ts
  • packages/lit/src/index.ts
  • packages/lit/tests-dom/create-stack-hook.test.ts
  • packages/lit/tests-dom/headless.test.ts
  • packages/lit/tests-dom/inference.test-d.ts
  • packages/lit/tests-dom/layer-handles.test.ts
  • packages/lit/tests-dom/stack-outlet.test.ts
  • packages/lit/tsconfig.json
  • packages/lit/tsdown.config.ts
  • packages/lit/vitest.config.ts
💤 Files with no reviewable changes (3)
  • docs/plans/lit-adapter.md
  • apps/docs/content/reference/roadmap.mdx
  • docs/roadmap.md

Comment thread apps/docs/content/adapters/lit.mdx
Comment on lines +377 to +396
describe("useMutationFlow (lit)", () => {
it("drives pending and ends the layer on success", async () => {
const client = new LayerClient();
const { host } = createHost();
const call = makeCall(client);
const flow = useMutationFlow<unknown, unknown>(host, call);
expect(flow.pending).toBe(false);
let resolved = false;
void flow
.run(async () => {
await Promise.resolve();
})
.orEnd("ok");
// pending flips true synchronously, then the layer ends
expect(flow.pending).toBe(true);
await Promise.resolve();
await Promise.resolve();
resolved = true;
expect(resolved).toBe(true);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the mutation-flow result instead of a local flag.

resolved is assigned by the test itself, so this passes even if the layer never ends or pending never resets. Await orEnd(), then assert flow.pending === false and that the layer was removed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit/src/index.test.ts` around lines 377 - 396, Update the
useMutationFlow success test to await the promise returned by
run(...).orEnd("ok") instead of tracking the locally assigned resolved flag.
After it completes, assert flow.pending is false and verify the host layer was
removed, using the existing host/layer inspection API.

Comment thread packages/lit/src/index.ts
Comment on lines +804 to +817
run = (fn: () => Promise<void> | void): MutationRun<R> => ({
orEnd: async (response: R) => {
this.#pending = true;
this.#host.requestUpdate();
this.#call.setRunning(true);
try {
await fn();
this.#call.end(response);
} finally {
this.#call.setRunning(false);
this.#pending = false;
this.#host.requestUpdate();
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prevent overlapping mutation runs.

Two concurrent orEnd() calls share one boolean. The first completion resets pending/actionStatus and ends the layer while the second operation is still running. Reject overlapping runs or track an in-flight count.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit/src/index.ts` around lines 804 - 817, Update the MutationRun
implementation returned by run so concurrent orEnd invocations cannot
prematurely clear shared state or call this.#call.end while another operation
remains active. Either reject a new invocation while one is in flight, or track
active runs with a count and only reset `#pending`, update the host, and finalize
the call after the last run completes; preserve cleanup for rejected or failed
operations.

Comment thread packages/lit/src/index.ts
Comment on lines +1192 to +1245
#init(client: LayerClient): void {
if (this.#states === null) {
this.#clientRef = client;
this.#stack = client.getStack(this.stack);
this.#states = new StackController(
this,
{ stack: this.stack, client },
undefined,
false,
);
this.requestUpdate();
return;
}
// Context provider may push a new client via setValue — rebind.
if (client === this.#clientRef) return;
this.#clientRef = client;
this.#states.reconfigure({ stack: this.stack }, client);
this.#stack = client.getStack(this.stack);
this.requestUpdate();
}

updated(changed: PropertyValues<this>): void {
if (this.#stack === null || this.#states === null) return;

if (
changed.has("client") &&
this.client &&
this.client !== this.#clientRef
) {
this.#clientRef = this.client;
this.#states.reconfigure({ stack: this.stack }, this.client);
this.#stack = this.client.getStack(this.stack);
this.requestUpdate();
return;
}

if (changed.has("stack") && this.#clientRef) {
this.#states.reconfigure({ stack: this.stack }, this.#clientRef);
this.#stack = this.#clientRef.getStack(this.stack);
this.requestUpdate();
}
}

connectedCallback(): void {
super.connectedCallback();
if (this.client) {
this.#init(this.client);
} else if (this.#stack === null) {
new ContextConsumer(this, {
context: layerClientContext,
subscribe: true,
callback: (c: LayerClient) => this.#init(c),
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve explicit .client precedence over context updates.

After initialization from context, assigning .client switches correctly, but the retained context consumer can later call #init() and overwrite that explicit client during a provider swap. Track the latest context client separately, ignore context pushes while .client is set, and fall back to context when it is cleared.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit/src/index.ts` around lines 1192 - 1245, Update the
client/context handling in `#init`, updated, and connectedCallback to track the
latest context client separately from the explicit client. Ignore context
callbacks while .client is set, preserve explicit-client precedence during
provider swaps, and when .client is cleared rebind to the most recent context
client.

Comment thread packages/lit/src/index.ts
Comment on lines +1440 to +1458
/**
* Reactive controller for a controlled layer bound to one stack. Set `.open` to
* `true` to open and `false` to dismiss; `.payload` / `.options` / `.onResolved`
* update the live layer. Dismisses automatically when the host disconnects.
*/
export class AppLayerController<
P = unknown,
R = unknown,
> implements ReactiveController {
#client: LayerClient | undefined;
#stackId: string;
#opened = false;
#connected = false;
#pendingOpen: Promise<R> | null = null;

options: OmitKeyof<LayerOptions<P, R>, "stack">;
#open: boolean;
payload: P;
onResolved?: (response: R) => void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Implement the documented live updates for payload, options, and onResolved.

These are plain fields, so assignments while the layer is open never update the mounted layer. Changing options.key also makes later dismissal search for a different key. Add setters or an explicit update method that synchronizes the owned layer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit/src/index.ts` around lines 1440 - 1458, Update
AppLayerController so assignments to payload, options, and onResolved
synchronize the currently mounted layer whenever it is open, using accessors or
an explicit update method rather than plain fields. Ensure options.key changes
do not alter the key used to dismiss the already-owned layer; retain and use the
original mounted-layer identity for later dismissal.

Comment thread packages/lit/src/index.ts
Comment on lines +1473 to +1477
} else if (isElementHost(host)) {
resolveClientLazy(host, undefined, (c) => {
this.#client = c;
if (this.#connected) this.#sync();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Track the exact layer instance and client owned by this controller.

A context swap replaces #client without dismissing/reopening the layer on the old client. Close and disconnect also use stack.find(key), which can dismiss another controller’s same-key layer. Store the opened layer’s client and unique identity, then operate only on that instance.

Also applies to: 1495-1515, 1534-1539

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit/src/index.ts` around lines 1473 - 1477, Update the controller’s
layer lifecycle around the client-resolution branch and the close/disconnect
paths to retain the exact client and unique layer identity returned when this
controller opens its layer. When the context client changes, dismiss the
previously owned layer before replacing `#client`, then reopen or resynchronize on
the new client as appropriate. Replace stack.find(key)-based dismissal with
operations targeting only the stored layer instance, and clear the stored
ownership state when the layer is closed or disconnected.

Comment thread packages/lit/src/index.ts
Comment on lines +1520 to +1533
if (this.#open && !this.#opened) {
this.#opened = true;
const pending = this.#client.open({
...this.options,
stack: this.#stackId,
payload: this.payload,
} as OpenLayerOptions<P, R>) as Promise<R>;
this.#pendingOpen = pending;
void pending.then((response) => {
if (this.#pendingOpen !== pending) return;
this.#opened = false;
this.#pendingOpen = null;
this.onResolved?.(response);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reset controller state when open() fails.

A synchronous throw leaves #opened = true, while a rejected promise leaves both state fields stuck and creates an unhandled rejection from the ignored .then(). Handle both failure paths and restore retryable state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit/src/index.ts` around lines 1520 - 1533, Update the open flow in
the method containing `#open`, `#opened`, and `#pendingOpen` so synchronous throws
from this.#client.open and asynchronous promise rejections both reset `#opened` to
false and `#pendingOpen` to null, preserving retryability. Attach rejection
handling to the pending promise so failures do not become unhandled rejections,
while retaining the existing successful-response guard and onResolved behavior.

Comment thread packages/lit/src/index.ts
Comment on lines +1551 to +1555
/** Controlled-layer controller constructor bound to the hook's stack. */
AppLayer: new (
host: ReactiveControllerHost,
props: AppLayerProps<unknown, unknown>,
) => AppLayerController;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant type and nearby tests.
rg -n "AppLayer: new|AppLayerProps|AppLayerController|BoundAppLayer|StackHook" packages/lit/src packages/lit/test -S

# Show the surrounding source for the reported lines.
sed -n '1510,1615p' packages/lit/src/index.ts

# Show the type-level tests if present.
fd -a "inference.test-d.ts|*.test-d.ts" packages/lit -t f

Repository: stainless-code/layers

Length of output: 1313


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the full relevant source sections with line numbers.
cat -n packages/lit/src/index.ts | sed -n '1436,1620p'

# Find type-level tests anywhere in the repo.
fd -a "inference.test.d.ts|*.test-d.ts|*.test.ts" . -t f

# Search for usages of createStackHook/AppLayer in tests or examples.
rg -n "createStackHook\\(|useAppStack|AppLayer\\b" . -g '!**/dist/**' -g '!**/build/**' -S

Repository: stainless-code/layers

Length of output: 6965


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List declaration/type tests and examples in the repo.
fd -a -t f '.*(test|spec).*\.(d\.ts|ts|tsx)$' .
fd -a -t f '.*\.d\.ts$' .

# Narrow search for StackHook/AppLayer usages in tests/examples.
rg -n "createStackHook\\(|useAppStack|AppLayerProps<|AppLayerController<" . \
  -g '!**/dist/**' -g '!**/build/**' -g '!**/coverage/**' -S

Repository: stainless-code/layers

Length of output: 18761


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lit inference test =="
cat -n packages/lit/tests-dom/inference.test-d.ts | sed -n '210,260p'

echo "== lit create-stack-hook test =="
cat -n packages/lit/tests-dom/create-stack-hook.test.ts | sed -n '105,240p'

Repository: stainless-code/layers

Length of output: 6026


Preserve AppLayer generics on the bound constructor. StackHook.AppLayer and BoundAppLayer still collapse P/R to unknown, so new AppLayer(this, { payload, onResolved }) loses typed payload/response flow. Make the constructor and bound class generic, and add a packages/lit/tests-dom/inference.test-d.ts assertion for the returned hook.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit/src/index.ts` around lines 1551 - 1555, Update
StackHook.AppLayer and BoundAppLayer so their constructors/classes remain
generic over payload and response types instead of collapsing them to unknown;
ensure new AppLayer(this, { payload, onResolved }) preserves typed P/R inference
through the returned hook, and add the corresponding assertion in
inference.test-d.ts.

Comment thread packages/lit/src/index.ts
Comment on lines +1620 to +1624
export function defineStackElements(): void {
if (typeof customElements === "undefined") return;
for (const [tag, ctor] of STACK_ELEMENTS) {
if (!customElements.get(tag)) customElements.define(tag, ctor);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fail loudly when a custom-element tag is already owned by another constructor.

The current check silently accepts collisions, after which document.createElement("stack-outlet") may return an unrelated element. Verify that an existing registration equals ctor; otherwise throw a descriptive integration error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit/src/index.ts` around lines 1620 - 1624, Update
defineStackElements to validate existing registrations: when
customElements.get(tag) returns a constructor, compare it with the expected ctor
and throw a descriptive integration error if they differ; retain definition for
unregistered tags and accept registrations already owned by the same ctor.

Comment on lines +209 to +230
it("stops updating after disconnect", async () => {
const client = new LayerClient();
const { provider, outlet } = await mountProvider(client);

void client.open({
...confirmOptions,
payload: { title: "Mounted" },
});
await (outlet as unknown as { updateComplete: Promise<unknown> })
.updateComplete;
await Promise.resolve();

provider.remove();
await Promise.resolve();

expect(() => {
void client.open({
...confirmOptions,
payload: { title: "After disconnect" },
});
}).not.toThrow();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Verify that disconnect actually stops rendering updates.

LayerClient.open() not throwing does not prove unsubscription. After removal, open another layer and assert that the detached outlet does not render "After disconnect".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit/tests-dom/stack-outlet.test.ts` around lines 209 - 230,
Strengthen the “stops updating after disconnect” test around provider.remove()
by opening the “After disconnect” layer, waiting for any relevant update
completion, and asserting the detached outlet does not render that content. Keep
the existing no-throw assertion, but verify the outlet’s rendered output remains
unchanged after disconnection.

Add Lit to the footer adapters column, restore the verbatim experimental
strip, and put Angular → Vue → Lit column order in adapter-hooks and guides.
Keep the package-binds-via-mechanism lead used by React/Vue/Solid/Angular
instead of a one-off pain-first opener on the Lit page alone.
Declare reactive layer props, register CEs at module scope, use useLayer
+ provideLayerClient correctly, and match official light-DOM createRenderRoot.
Full-matrix Lit adapter (controllers + stack CEs) lands at 4.01 kB gzip
ignored peers — 9 B over the React-matched 4 KB cap.
@SutuSebastian
SutuSebastian merged commit a886aa9 into main Jul 17, 2026
17 checks passed
@SutuSebastian
SutuSebastian deleted the feat/lit-adapter branch July 17, 2026 12:53
@github-actions github-actions Bot mentioned this pull request Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant