Skip to content

feat(ui): Add machine library for authoring flows#8919

Merged
alexcarpenter merged 31 commits into
mainfrom
carp/mosaic-state-machine
Jun 26, 2026
Merged

feat(ui): Add machine library for authoring flows#8919
alexcarpenter merged 31 commits into
mainfrom
carp/mosaic-state-machine

Conversation

@alexcarpenter

@alexcarpenter alexcarpenter commented Jun 18, 2026

Copy link
Copy Markdown
Member

Description

We have several approaches to handling flows in component code, domain modeling and state syncing spread across useEffects which makes it difficult to without rendering a component. This adds a lightweight state machine library with the goal of standardizing flow patterns.

Why not XState

XState is the obvious reference, and the API design follows XState v5 closely but ~9x smaller.

Minified Gzipped
This library (useMachine + setup, full runtime) ~3.6 KB ~1.6 KB
XState v5 (bundlephobia, v5.32.1) 44.7 KB 14.1 KB

What's in this PR

  • New library in packages/ui/src/mosaic/machine/
  • delete-organization and leave-organization Mosaic component sections migrated as a proof of concept
  • signInMachine and firstFactorMachine modelled but not wired to components yet
  • patterns.test.ts documents how real Clerk patterns map to machine primitives

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • New Features
    • Added a new Mosaic state-machine library for step-based flows with async invoke handling.
    • Introduced React hooks for running machines, selecting state, and logging transitions.
    • Refreshed delete/leave organization flows with machine-driven typed confirmation and submit gating.
  • Documentation
    • Added new guides and README content for building, adopting, and testing Mosaic state machines.
  • Tests
    • Expanded coverage for machine behavior, React hook integration, and destructive confirmation flows.

- Tighten Actor.send and Actor.can to accept TEvent only, removing the
  AnyEventObject escape hatch that silently swallowed typos
- Add context init option to CreateActorOptions so runtime dependencies
  can be injected at actor-creation time without module-level closures
- Add types.test-d.ts with @ts-expect-error guards and expectTypeOf
  assertions covering send/can rejection, snapshot types, and assign
- Extract delete-organization-machine and leave-organization-machine as
  module-level factories; migrate both sections to useMachine with the
  split-component pattern (loader + ready) so the machine is always
  instantiated with a non-null organization/membership
…mounts

In StrictMode (on by default in Next.js dev), React runs effects twice:
start → stop → start. The second start() hit the `if (started) return`
guard and bailed out, leaving status = 'stopped'. Every subsequent send()
then short-circuited, so dialogs driven by useMachine never opened.

Fix: stop() resets `started = false` to allow a clean restart, and start()
sets status = 'active' before entering state so a restart-after-stop works.
…-fire

After stop() + start() (StrictMode, Suspense remount), the actor was
re-entering whatever state it was stopped in. If that state had an invoke
(e.g. deleting), the async function would fire a second time.

Fix: start() now resets context and value to their initial values before
entering state, so a restart always begins from idle rather than continuing
a half-completed flow.

Adds a regression test for the double-invoke case.
…factories

useMachine creates the actor once, so any function passed inline to the
machine factory is captured from the first render only. If membership or
organization changed under the same mount, the old .destroy() would fire.

Fix: pin a ref to the prop on every render; the machine receives a stable
wrapper that reads from the ref at call time.

Adds a regression test that re-renders with a fresh destroy prop before
triggering the flow and asserts the fresh function is invoked.
…Effect

Replaces the ref-wrapper workaround in section components with the XState
pattern: actor.setContext() silently patches context each render so invoke
sources always call the latest prop-derived function without stale closures.
Section machine factories are now module-level singletons; the destroy/leave
function is injected via useMachine options.context instead of a closure.
This reverts commit a521f2e.
TransitionConfig, Transition, InvokeConfig, StateConfig, and MachineConfig
now accept an optional TStates extends string = string generic. When callers
supply it, initial and transition targets are constrained to that literal union;
the default of string preserves the existing behaviour for untyped machines.

StateConfig.on keys are now Partial<Record<TEvent['type'], ...>> so event type
typos (e.g. 'CANEEL' instead of 'CANCEL') are caught at the createMachine call
site rather than silently accepted. Type tests cover both constraints.
@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jun 26, 2026 1:04am
swingset Ready Ready Preview, Comment Jun 26, 2026 1:04am

Request Review

@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e1fd1ba

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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 Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: f2c15693-b5fc-41f5-8cf6-998934aebc7a

📥 Commits

Reviewing files that changed from the base of the PR and between 380bd93 and e1fd1ba.

📒 Files selected for processing (10)
  • .claude/skills/mosaic-machine/SKILL.md
  • packages/ui/src/mosaic/block/destructive.tsx
  • packages/ui/src/mosaic/machine/ADOPTION.md
  • packages/ui/src/mosaic/machine/README.md
  • packages/ui/src/mosaic/machine/__tests__/patterns.test.ts
  • packages/ui/src/mosaic/machine/createActor.ts
  • packages/ui/src/mosaic/sections/__tests__/delete-organization-machine.test.ts
  • packages/ui/src/mosaic/sections/__tests__/leave-organization-machine.test.ts
  • packages/ui/src/mosaic/sections/delete-organization-machine.ts
  • packages/ui/src/mosaic/sections/leave-organization-machine.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/ui/src/mosaic/machine/README.md
  • .claude/skills/mosaic-machine/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/ui/src/mosaic/sections/tests/delete-organization-machine.test.ts
  • packages/ui/src/mosaic/sections/leave-organization-machine.ts
  • packages/ui/src/mosaic/sections/delete-organization-machine.ts
  • packages/ui/src/mosaic/sections/tests/leave-organization-machine.test.ts
  • packages/ui/src/mosaic/machine/tests/patterns.test.ts
  • packages/ui/src/mosaic/machine/createActor.ts
  • packages/ui/src/mosaic/block/destructive.tsx
  • packages/ui/src/mosaic/machine/ADOPTION.md

📝 Walkthrough

Walkthrough

The PR adds a typed Mosaic state-machine runtime, React hooks, and machine examples. It also refactors delete/leave organization flows into controller/view slices, updates the destructive confirmation component, and adds documentation plus alias configuration for the new hook path.

Changes

Mosaic state-machine rollout

Layer / File(s) Summary
Machine contracts and factories
packages/ui/src/mosaic/machine/types.ts, packages/ui/src/mosaic/machine/assign.ts, packages/ui/src/mosaic/machine/createMachine.ts, packages/ui/src/mosaic/machine/setup.ts, packages/ui/src/mosaic/machine/types.test-d.ts
Defines the shared machine types, helper factories, and type-level tests for createMachine, assign, setup, and fromPromise.
Actor runtime and React hooks
packages/ui/src/mosaic/machine/createActor.ts, packages/ui/src/mosaic/machine/useMachine.ts, packages/ui/src/mosaic/machine/__tests__/machine.test.ts, packages/ui/src/mosaic/machine/__tests__/useMachine.test.tsx
Implements actor execution, subscriptions, invokes, timers, and React hook bindings, with runtime and hook behavior tests.
Sign-in and factor machines
packages/ui/src/mosaic/machines/firstFactorMachine.ts, packages/ui/src/mosaic/machines/signInMachine.ts, packages/ui/src/mosaic/machines/__tests__/test-utils.ts, packages/ui/src/mosaic/machines/__tests__/firstFactorMachine.test.ts, packages/ui/src/mosaic/machines/__tests__/signInMachine.test.ts
Adds the first-factor and sign-in machines, shared test helpers, and flow coverage for identifier, factor, reset-password, resend, and cooldown paths.
Pattern and migration tests
packages/ui/src/mosaic/machine/__tests__/patterns.test.ts, packages/ui/src/mosaic/machine/__tests__/wizard-migration.test.tsx
Adds machine pattern examples and a wizard migration parity suite covering guarded transitions, async routing, recheck(), and React integration.
Organization flows and destructive control
packages/ui/src/mosaic/sections/delete-organization*, packages/ui/src/mosaic/sections/leave-organization*, packages/ui/src/mosaic/sections/__tests__/*, packages/ui/src/mosaic/block/destructive.tsx, packages/swingset/next.config.mjs, packages/swingset/tsconfig.json, packages/swingset/src/stories/destructive.stories.tsx
Moves delete and leave organization flows into machine-backed controller/view slices and updates the destructive dialog, story, and path aliases for controlled confirmation input.
Machine docs and skill note
packages/ui/src/mosaic/machine/README.md, .claude/skills/mosaic-machine/SKILL.md, .changeset/mosaic-state-machine.md
Adds the machine README, authoring skill note, and changeset entry for the new Mosaic API.
Adoption and architecture guides
references/mosaic-architecture.md, packages/ui/src/mosaic/machine/ADOPTION.md
Adds the adoption guide and architecture reference describing the machine/controller/view split and migration walkthroughs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • clerk/javascript#8474: Introduces the useControllableState pattern that the updated destructive confirmation block uses for its controlled input.
  • clerk/javascript#8838: Modifies the same Mosaic destructive confirmation block, and this PR extends that API with external canSubmit, confirmationValue, and error handling.
  • clerk/javascript#8884: Updates the Mosaic dialog component API that the destructive flow now uses in the new controller/view wiring.

Suggested reviewers

  • kylemac
  • wobsoriano

Poem

A rabbit hops through states by night,
with guards that glow and contexts bright.
When carrots fit, the door swings wide,
and hidden bugs must hop aside.
🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 clearly summarizes the main change: adding a UI machine library for authoring flows.
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 docstrings
  • Create stacked PR
  • Commit on current branch

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 markdownlint-cli2 (0.22.1)
.claude/skills/mosaic-machine/SKILL.md

markdownlint-cli2 wrapper config was not available before execution

packages/ui/src/mosaic/machine/ADOPTION.md

markdownlint-cli2 wrapper config was not available before execution

packages/ui/src/mosaic/machine/README.md

markdownlint-cli2 wrapper config was not available before execution


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

@github-actions github-actions Bot added the ui label Jun 18, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jun 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8919

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8919

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8919

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8919

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@8919

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@8919

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@8919

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8919

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8919

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8919

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8919

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8919

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8919

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8919

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8919

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8919

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8919

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8919

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8919

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8919

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8919

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8919

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8919

commit: e1fd1ba

Adds a `UseMachineOptions` type that extends `CreateActorOptions` with
an optional `onDone` callback, fired once when the machine reaches a
final state. Auth flows can now declaratively hand off (navigate,
redirect) without a `useEffect` watching `snapshot.status` externally.
Resolves a promise with the first snapshot satisfying a predicate.
Rejects when the actor becomes non-active without the predicate passing,
or when an optional timeout elapses (WaitForTimeoutError). Eliminates
manual subscribe wiring in async tests and flow orchestration.
Adds setTimeout-based delayed transitions via a new `after` key on state
nodes — the idiomatic way to model OTP expiry and resend cooldowns without
leaking timers into components. Timers are cancelled on state exit and on
stop(), so they never outlive the state or the actor.
No auth flow or component pulls it in; deferred()+tick() covers test
needs. Can be added when something concrete requires it.
Machines already stored the API error in context on failure, but the
Destructive component had no error prop and neither section component
passed the error through. Users saw the dialog reopen with no feedback.

Adds an optional error prop to Destructive and surfaces snapshot.context.error
from both DeleteOrganizationReady and LeaveOrganizationReady.
- Guard send() and can() with the started flag so callers using createActor()
  directly cannot fire events before start() runs entry actions
- Add tests asserting send/can are no-ops before start()
- Add SAFETY comments (per CLAUDE.md) to all non-as-const casts:
  createActor.ts (event as never), createMachine.ts ({} as TContext),
  setup.ts (fromPromise src cast), firstFactorMachine.ts (three casts)
- Document in delete/leave-organization machines that the CONFIRM guard
  is intentionally delegated to the Destructive component
Replace Set with array-based listener tracking using bitwise NOT for safe removal.
Inline normalizeTransitions helper to eliminate function overhead. Use short-circuit
operators (&&) instead of if statements in hot paths. Collapse toArray into single
ternary expression.

Minified size: ~130-150 bytes saved (-1.5-2%). All 2316 tests pass. Zero API changes.

- Replace Set<SnapshotListener> with array + indexOf + splice pattern (~40 bytes)
- Inline normalizeTransitions via map expression (~60 bytes)
- Use reverse iteration (i--) in listener loops for safe removal
- Apply && operator in hot paths (useMachine, useMachineLogger) (~15 bytes)
- Collapse toArray to single-line ternary expression
Adds TransitionFn as a fourth arm of the Transition union. A function
returning { target?, context? } replaces the guard + assign combo for
the common case; returning undefined is treated as unhandled. Works for
both on-event and always transitions, including recheck() re-evaluation.
@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-06-26T01:05:04.146Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 0
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on e1fd1ba.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (4)
packages/ui/src/mosaic/block/destructive.tsx (1)

43-47: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Reset effect re-runs every render due to an unstable setter.

useControllableState's setter is memoized on [isControlled, onChange]. Consumers pass an inline onConfirmationValueChange, so onChange (and therefore setConfirmValue) gets a new identity each render, causing this effect to fire on every render. It's a no-op while open, but while closed it repeatedly invokes the change callback. Consider stabilizing the consumer handlers with useCallback, or resetting based on a ref to avoid the redundant calls.

🤖 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/ui/src/mosaic/block/destructive.tsx` around lines 43 - 47, The reset
effect in destructive.tsx is re-running every render because
`useControllableState` returns a setter that changes identity when
`onConfirmationValueChange` is passed inline. Stabilize the consumer callback
used by `useControllableState` in `Destructive` (or otherwise avoid depending on
the unstable setter in the `useEffect`) so the reset only runs when `open`
changes. Update the `useEffect` dependency pattern around
`confirmValue`/`setConfirmValue` to prevent repeated change callbacks while the
dialog is closed.
packages/ui/src/mosaic/machines/signInMachine.ts (1)

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

Add an explicit return type to the exported createSignInMachine.

Unlike createDeleteOrgMachine, this exported factory relies on inferred return typing. Annotating it (e.g. StateMachine<SignInContext, SignInEvent>) keeps the public machine surface stable and self-documenting.

As per coding guidelines ("Always define explicit return types for functions, especially public APIs") and based on learnings to enforce explicit return type annotations for exported functions and public APIs.

🤖 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/ui/src/mosaic/machines/signInMachine.ts` at line 42, Add an explicit
return type to the exported createSignInMachine factory so its public API is
stable and self-documenting. Update the createSignInMachine signature to
annotate the machine type explicitly, following the pattern used by
createDeleteOrgMachine and the other exported machine factories in this module,
rather than relying on inference.

Sources: Coding guidelines, Learnings

packages/ui/src/mosaic/machine/__tests__/delete-organization-machine.ts (1)

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

Optional: the event.type === 'TYPE' guard is redundant here.

This TYPE handler only ever receives the TYPE member at runtime, so the ternary fallback : {} is dead. Using the setup() factory's assign (which narrows e contextually) would let you write assign((_, e) => ({ confirmValue: e.value })) without the manual discriminant check. Not blocking since this is a fixture.

🤖 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/ui/src/mosaic/machine/__tests__/delete-organization-machine.ts`
around lines 49 - 53, The TYPE transition in delete-organization-machine is
over-defensive because the handler only receives the TYPE event, so the
`event.type === 'TYPE'` fallback is unnecessary. Update the `TYPE` action in the
`assign` for `DeleteOrgContext` to rely on the contextual event typing from
`setup()` and directly map `e.value` to `confirmValue`, removing the dead
ternary branch.
packages/ui/src/mosaic/machine/setup.ts (1)

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

Consider an explicit return type for the public setup factory.

setup is an exported public API but its return type is inferred. The inner factories are individually annotated, so an explicit interface here would make the public surface self-documenting and guard against accidental shape drift.

As per coding guidelines: "Always define explicit return types for functions, especially public APIs."

🤖 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/ui/src/mosaic/machine/setup.ts` at line 41, The exported setup
factory currently relies on inferred return type, so make its public API
explicit by adding a named return type/interface for setup and annotating the
setup<TContext, TEvent> function accordingly; use the existing inner factory
types in setup.ts to define the returned shape and ensure the exported surface
stays self-documenting and stable.

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 @.claude/skills/mosaic-machine/SKILL.md:
- Line 26: The wording around setup<TContext, TEvent>() is too broad:
fromPromise should be described as the typed helper only for promise-backed
invoke configurations, not all invokes. Update the SKILL.md guidance near
setup/createMachine/assign/fromPromise to state that fromPromise narrows the
resolved type to e.output in onDone.actions only when the invoke returns a
promise, and avoid implying it applies to every invoke configuration.

In `@packages/ui/src/mosaic/block/destructive.tsx`:
- Around line 64-75: The error message rendered by the destructive block is not
announced to assistive tech because it is only a plain paragraph. Update the Box
in destructive.tsx so the error container uses an accessible live announcement
such as role="alert" or aria-live="assertive" while keeping the existing error
rendering logic in place. Locate the conditional error block in the destructive
component and add the ARIA attribute directly on the Box/p element so screen
readers announce failed destructive actions immediately.

In `@packages/ui/src/mosaic/machine/__tests__/patterns.test.ts`:
- Around line 44-64: The submitting invoke in makeSignInMachine only uses
activeStrategy to choose between emailFn and socialFn, so the selected social
provider is lost. Update the social path in the invoke to pass the chosen
strategy through to socialFn, and make the machine context/actions in
makeSignInMachine preserve the CLICK_SOCIAL strategy value so provider-specific
behavior remains available when this pattern is copied.

In `@packages/ui/src/mosaic/machine/__tests__/wizard-migration.test.tsx`:
- Around line 482-508: The current Wizard AFTER test in
wizard-migration.test.tsx uses a no-op GOTO to the same step, which never
publishes a new snapshot and does not validate useSelector behavior. Update the
StepIndicator/useSelector scenario to trigger a real actor transition that emits
a new snapshot while keeping s.value unchanged, then assert renders does not
increment. Use the existing createWizardMachine, createActor, and useSelector
setup to add a context-only or otherwise unrelated transition that preserves the
selected value but still notifies subscribers.
- Around line 173-175: The test helper actorAt is returning an unstarted actor,
so lifecycle-dependent assertions in wizard-migration.test.tsx are not actually
exercising a running machine. Update actorAt to start the actor before returning
it, and make sure any direct recheck() usage in the affected tests also operates
on a started actor so send, can, and subscribe behavior is validated against the
real lifecycle.

In `@packages/ui/src/mosaic/machine/ADOPTION.md`:
- Around line 391-399: The submit invoke in the `submitting` state is erasing
the typed event by casting to `any`; update the `invoke.src` handler to use the
existing `SignInEvent` shape and carry `identifier` through the typed
event/context instead of `(event as any)`. Keep the example fully typed by
referencing the `submitting` state and `signIn.create` call, and remove the
`any` cast while preserving the same payload flow.
- Around line 124-159: The waitlist machine still closes over external async
dependencies instead of using the factory/context pattern. Update the
`waitlistMachine` example so `clerk`, `navigate`, and `afterJoinWaitlistUrl` are
provided through a machine factory or stored in machine context rather than read
from module scope. Use the existing `createMachine`-based `waitlistMachine` and
its `invoke`/`success.entry` logic as the places to thread those dependencies in
for easier testing and consistency with the authoring guidance.

In `@packages/ui/src/mosaic/machine/createActor.ts`:
- Around line 163-188: The invoke completion/error paths in createActor
currently call commit() unconditionally after takeTransition, unlike send,
startAfterTimers, and recheck. Update the onDone and onError handlers to check
the boolean returned by takeTransition before committing, so blocked transitions
do not publish a new snapshot or notify subscribers when the state does not
change.

In `@packages/ui/src/mosaic/machine/README.md`:
- Around line 48-55: The ASCII diagram block in the README is missing a fenced
language label, which triggers markdownlint and reduces highlighting. Update the
fence around the state machine diagram to include an appropriate tag, using the
diagram block near the FETCH/loading/success/failure section so the markdown
parser and tooling recognize it correctly.

In `@packages/ui/src/mosaic/sections/delete-organization-machine.ts`:
- Around line 49-51: The error handling in deleteOrganizationMachine is passing
a stringified exception directly to the UI, which can expose technical text to
end users. Update the assign handler in delete-organization-machine.ts to derive
a user-facing message from the event error (prefer the error’s message field,
with a friendly fallback when unavailable) instead of using String(event.error),
so delete-organization-view.tsx and Destructive receive a safe message. Apply
the same fix to the matching error assignment in leave-organization-machine.ts
to keep both flows consistent.

In `@packages/ui/src/mosaic/sections/leave-organization-machine.ts`:
- Around line 47-52: The `onError` handler in `leave-organization-machine` is
passing `String(event.error)` through to user-facing state, which can include an
unwanted “Error:” prefix. Update the `assign` logic in the `onError` action to
extract `event.error.message` when the rejection is an `Error`, and fall back to
a sensible string for non-Error values. Then adjust the
`leave-organization-machine.test` expectation to match the cleaned message
instead of the prefixed form.

---

Nitpick comments:
In `@packages/ui/src/mosaic/block/destructive.tsx`:
- Around line 43-47: The reset effect in destructive.tsx is re-running every
render because `useControllableState` returns a setter that changes identity
when `onConfirmationValueChange` is passed inline. Stabilize the consumer
callback used by `useControllableState` in `Destructive` (or otherwise avoid
depending on the unstable setter in the `useEffect`) so the reset only runs when
`open` changes. Update the `useEffect` dependency pattern around
`confirmValue`/`setConfirmValue` to prevent repeated change callbacks while the
dialog is closed.

In `@packages/ui/src/mosaic/machine/__tests__/delete-organization-machine.ts`:
- Around line 49-53: The TYPE transition in delete-organization-machine is
over-defensive because the handler only receives the TYPE event, so the
`event.type === 'TYPE'` fallback is unnecessary. Update the `TYPE` action in the
`assign` for `DeleteOrgContext` to rely on the contextual event typing from
`setup()` and directly map `e.value` to `confirmValue`, removing the dead
ternary branch.

In `@packages/ui/src/mosaic/machine/setup.ts`:
- Line 41: The exported setup factory currently relies on inferred return type,
so make its public API explicit by adding a named return type/interface for
setup and annotating the setup<TContext, TEvent> function accordingly; use the
existing inner factory types in setup.ts to define the returned shape and ensure
the exported surface stays self-documenting and stable.

In `@packages/ui/src/mosaic/machines/signInMachine.ts`:
- Line 42: Add an explicit return type to the exported createSignInMachine
factory so its public API is stable and self-documenting. Update the
createSignInMachine signature to annotate the machine type explicitly, following
the pattern used by createDeleteOrgMachine and the other exported machine
factories in this module, rather than relying on inference.
🪄 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: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: f49b6f3b-9b8a-4440-9f9b-e10800af6fd6

📥 Commits

Reviewing files that changed from the base of the PR and between 34c653a and 380bd93.

📒 Files selected for processing (38)
  • .changeset/mosaic-state-machine.md
  • .claude/skills/mosaic-machine/SKILL.md
  • packages/swingset/next.config.mjs
  • packages/swingset/src/stories/destructive.stories.tsx
  • packages/swingset/tsconfig.json
  • packages/ui/src/mosaic/block/destructive.tsx
  • packages/ui/src/mosaic/machine/ADOPTION.md
  • packages/ui/src/mosaic/machine/README.md
  • packages/ui/src/mosaic/machine/__tests__/delete-organization-machine.ts
  • packages/ui/src/mosaic/machine/__tests__/machine.test.ts
  • packages/ui/src/mosaic/machine/__tests__/patterns.test.ts
  • packages/ui/src/mosaic/machine/__tests__/useMachine.test.tsx
  • packages/ui/src/mosaic/machine/__tests__/wizard-migration.test.tsx
  • packages/ui/src/mosaic/machine/assign.ts
  • packages/ui/src/mosaic/machine/createActor.ts
  • packages/ui/src/mosaic/machine/createMachine.ts
  • packages/ui/src/mosaic/machine/setup.ts
  • packages/ui/src/mosaic/machine/types.test-d.ts
  • packages/ui/src/mosaic/machine/types.ts
  • packages/ui/src/mosaic/machine/useMachine.ts
  • packages/ui/src/mosaic/machines/__tests__/firstFactorMachine.test.ts
  • packages/ui/src/mosaic/machines/__tests__/signInMachine.test.ts
  • packages/ui/src/mosaic/machines/__tests__/test-utils.ts
  • packages/ui/src/mosaic/machines/firstFactorMachine.ts
  • packages/ui/src/mosaic/machines/signInMachine.ts
  • packages/ui/src/mosaic/sections/__tests__/delete-organization-machine.test.ts
  • packages/ui/src/mosaic/sections/__tests__/delete-organization-view.test.tsx
  • packages/ui/src/mosaic/sections/__tests__/leave-organization-machine.test.ts
  • packages/ui/src/mosaic/sections/__tests__/leave-organization-view.test.tsx
  • packages/ui/src/mosaic/sections/delete-organization-controller.tsx
  • packages/ui/src/mosaic/sections/delete-organization-machine.ts
  • packages/ui/src/mosaic/sections/delete-organization-view.tsx
  • packages/ui/src/mosaic/sections/delete-organization.tsx
  • packages/ui/src/mosaic/sections/leave-organization-controller.tsx
  • packages/ui/src/mosaic/sections/leave-organization-machine.ts
  • packages/ui/src/mosaic/sections/leave-organization-view.tsx
  • packages/ui/src/mosaic/sections/leave-organization.tsx
  • references/mosaic-architecture.md

Comment thread .claude/skills/mosaic-machine/SKILL.md Outdated
Comment thread packages/ui/src/mosaic/block/destructive.tsx
Comment thread packages/ui/src/mosaic/machine/__tests__/patterns.test.ts Outdated
Comment thread packages/ui/src/mosaic/machine/__tests__/wizard-migration.test.tsx
Comment on lines +482 to +508
describe('Wizard AFTER — useSelector scopes re-renders to the step slice', () => {
it('a stepper re-renders on navigation but not on an unrelated context change', () => {
const machine = createWizardMachine([{ id: 'intro' }, { id: 'details', guard: () => true }]);
// Seed at 'intro' (furthest-reachable would land on 'details' otherwise).
const actor = createActor(machine, { snapshot: { value: 'intro' } });
actor.start();

const renders = vi.fn();
function StepIndicator() {
const current = useSelector(actor, s => s.value);
renders();
return <output data-testid='current'>{current}</output>;
}

render(<StepIndicator />);
expect(renders).toHaveBeenCalledTimes(1);
expect(screen.getByTestId('current')).toHaveTextContent('intro');

// A GOTO to the current step is a no-op → no notify → no re-render.
act(() => actor.send({ type: 'GOTO', step: 'intro' }));
expect(renders).toHaveBeenCalledTimes(1);

// A real navigation changes the selected slice → exactly one re-render.
act(() => actor.send({ type: 'NEXT' }));
expect(renders).toHaveBeenCalledTimes(2);
expect(screen.getByTestId('current')).toHaveTextContent('details');
});

@coderabbitai coderabbitai Bot Jun 25, 2026

Copy link
Copy Markdown
Contributor

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

Make the useSelector test emit a snapshot whose selected value is unchanged.

The current “unrelated context change” assertion sends GOTO to the current step, which is a true no-op and does not notify subscribers. That does not prove useSelector suppresses re-renders when the actor publishes a new snapshot but s.value is unchanged.

Consider adding a context-only transition in this test and asserting renders remains unchanged after that event.

🤖 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/ui/src/mosaic/machine/__tests__/wizard-migration.test.tsx` around
lines 482 - 508, The current Wizard AFTER test in wizard-migration.test.tsx uses
a no-op GOTO to the same step, which never publishes a new snapshot and does not
validate useSelector behavior. Update the StepIndicator/useSelector scenario to
trigger a real actor transition that emits a new snapshot while keeping s.value
unchanged, then assert renders does not increment. Use the existing
createWizardMachine, createActor, and useSelector setup to add a context-only or
otherwise unrelated transition that preserves the selected value but still
notifies subscribers.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid point — the GOTO-to-same-step test doesn't prove useSelector actually filters a new snapshot where the selected slice is unchanged. Skipping for this PR: adding a context-only transition that emits a new snapshot without changing s.value would require a new event on the wizard machine, which is out of scope here. Filed as a follow-up.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@alexcarpenter Makes sense — thanks for confirming the gap and for filing the follow-up. Agreed that introducing a wizard-specific context-only event just to prove the useSelector behavior would broaden this test fixture beyond the scope of this PR, so I'm good to defer it.


🧠 Learnings used
Learnt from: alexcarpenter
Repo: clerk/javascript PR: 8593
File: packages/ui/src/elements/ClipboardInput.tsx:0-0
Timestamp: 2026-05-19T17:58:25.594Z
Learning: When styling native HTML `<input type="text">` (or other single-line text inputs), applying `textOverflow: 'ellipsis'` by itself is sufficient—native single-line input behavior provides implicit single-line/overflow handling, so you generally do not need to also set `overflow: 'hidden'` or `whiteSpace: 'nowrap'`. Conversely, if you’re applying ellipsis to non-input/block-level elements (e.g., `<div>`, `<span>`, or other elements), ensure `whiteSpace: 'nowrap'` and `overflow: 'hidden'` are included alongside `textOverflow: 'ellipsis'` so the ellipsis can render.

Learnt from: alexcarpenter
Repo: clerk/javascript PR: 8922
File: packages/ui/src/primitives/Input.tsx:30-33
Timestamp: 2026-06-18T21:53:41.119Z
Learning: When building SVG `data:` URIs, ensure any interpolated theme color token won’t introduce reserved characters. In particular, `theme.colors.$white` resolves to the CSS named color string `'white'` (no `#`), so interpolating it into an SVG `data:` URI does not create `#`-related URI breakage and does not require URL-encoding for that token specifically. If a token resolves to a hex color (e.g., `#fff`), then `#` must be treated as a reserved character and should be encoded/escaped accordingly.

Comment thread packages/ui/src/mosaic/machine/ADOPTION.md
Comment thread packages/ui/src/mosaic/machine/createActor.ts
Comment thread packages/ui/src/mosaic/machine/README.md Outdated
Comment thread packages/ui/src/mosaic/sections/delete-organization-machine.ts
Comment thread packages/ui/src/mosaic/sections/leave-organization-machine.ts

@wobsoriano wobsoriano left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's get it

- Gate commit() on takeTransition return in invoke onDone/onError handlers
  so blocked entry guards don't publish spurious snapshots
- Use error.message instead of String(error) in delete/leave org machines
  to avoid surfacing "Error: nope"-style technical text to users
- Add role='alert' to error box in Destructive component for screen readers
- Pass selected social strategy through to socialFn in patterns.test.ts
- Use factory pattern for waitlistMachine example in ADOPTION.md
- Remove any cast in signInStart machine example by storing identifier
  in context on SUBMIT transitions
- Add text language tag to ASCII diagram fence in README.md
- Narrow fromPromise description in SKILL.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants