Skip to content

feat(svelte): Svelte 5 integration - #34

Open
ivanbanov wants to merge 37 commits into
mainfrom
svelte
Open

ivanbanov wants to merge 37 commits into
mainfrom
svelte

Conversation

@ivanbanov

@ivanbanov ivanbanov commented Jun 23, 2026 •

Copy link
Copy Markdown
Member

Svelte 5 target: @dunky.dev/svelte-state-machine

A first-class Svelte 5 bridge for the state-machine engine, written in runes. The same machine config and connect() run unchanged on React, Solid, Svelte, React Native, and OpenTUI.

API

  • useMachine(createConfig, connect, effects, () => props) → { api, machine }
    • Builds the machine and connector once and reads props through a getter. Props compare shallowly, as on every target.
    • view.api is read lazily: connect() runs once a send settles, only when read. Inside a view.machine.send, a read gets the snapshot the previous send settled to, and Svelte hears of the change only when the outermost send ends (so flushSync() in a callback renders the pre-send UI; await tick() to focus). Transitions that start elsewhere (after timers, an action's own async send) aren't guarded: selectors and callbacks should read the machine.
    • Starts the machine after mount. Stops it on destroy, before child components tear down (React's order).
    • Each ComponentEffect runs as its own $effect. It re-runs only when a named dep's value changes (Object.is, like React's dep array), and its body is untracked.
    • Reactions and their callbacks never become dependencies of the $effect that changed the machine, whatever notified: a send, a context change, a timer.
  • useSelector(machine | () => machine, selector, isEqual?) → { current }
    • One subscription per machine and one dedup baseline for both machine and prop changes.
    • Renders live on the server.
    • A selector that throws mid-send is retried in the next flush instead of throwing out of send().
  • normalize(bindings): maps the vocabulary to Svelte's lowercase DOM event props (onclick, oninput, ondblclick), tabindex, and aria-*. Reuses @dunky.dev/state-machine-dom. Symbol-keyed attachments pass through.
  • mergeProps(consumer, library): one pass.
    • Handlers chain consumer-first through composeHandlers, with the defaultPrevented veto.
    • class/style strings join; other class shapes merge as [consumer, library].
    • A nullish library class/style keeps the consumer's. Attachments from both sides are kept.

Packaging

  • @sveltejs/package 3 builds dist with .svelte.js runes modules and a .d.ts per module. The consumer's Svelte compiler compiles them.
  • exports uses only the types and svelte conditions.
  • The root pnpm build runs it after tsdown, and publint checks the packed manifest.
  • A package-local typescript@^6 is required, because svelte-package and svelte-check need the TypeScript JS API that TypeScript 7 does not ship.
  • Vitest needs only @sveltejs/vite-plugin-svelte, unless a test harness reaches the adapter only through other packages' sources. Then: test.server.deps.inline: [/@dunky\.dev\/svelte-/].
  • Peer range: svelte >=5.16.0 <5.33.5 || ^5.34.5, measured across every Svelte 5 minor. 5.16 added class clsx; 5.33.5–5.34.4 kept stale spread handlers (fix: keep spread non-delegated event handlers up to date sveltejs/svelte#16180).

Tests

  • 64 tests: pure, jsdom, and svelte/server SSR, plus the shared vocabulary-accounting suite.
  • Regression tests cover: props flow, proxied api, hidden dependencies through every notification path, reaction loops, mid-transition reads, teardown order, server snapshots, Object.is deps, same-machine re-subscribes, throwing selectors, and attachments.
  • pnpm typecheck runs svelte-check over the .svelte harnesses.
  • Every doc sample passes svelte-check --fail-on-warnings under strict TypeScript.

Also

  • sandbox/svelte uses the shared cmdk machine and stylesheet on vite 8.3.2.
  • Docs: package README, website page, Integrations sidebar, and the target lists in AGENTS, ARCHITECTURE, and ACCESSIBILITY.
  • Changeset: minor bump for the new package.

🤖 Generated with Claude Code

ivanbanov and others added 2 commits June 23, 2026 22:22
Add @dunky.dev/state-machine-svelte, a thin runes-based edge layer
mirroring the React package:

- useMachine: build-once bridge + lifecycle + prop-scoped substrate
  effects, returning a reactive { api, machine }. Takes props as a
  getter (() => props) since Svelte props are reactive bindings.
- useSelector: fine-grained leaf subscription returning { current }.
- normalize/mergeProps: Svelte DOM idiom (lowercase on* event props,
  class/style string merge).

The package ships its src uncompiled (svelte export condition) so the
consumer's Svelte compiler processes the .svelte.ts runes modules; it's
excluded from the tsdown bundle for that reason. Vitest gains a scoped
svelte project (svelte plugin + svelteTesting + jsdom) so runes tests
run without disturbing the other packages.

Adds docs (libs/svelte.mdx + sidebar entry) and a changeset.

Co-authored-by: Claude <noreply@anthropic.com>
A fourth renderer for the shared @sandbox/cmdk-core command palette,
driven by the Svelte binding. Unlike the React/OpenTUI/Native apps —
which all render through a React reconciler and share React's useMachine
— the Svelte app brings its own runes-based useMachine from
@dunky.dev/state-machine-svelte, yet runs the exact same machine and
connect() unchanged. Same behavior, different framework.

- Vite + vite-plugin-svelte, aliasing every @dunky.dev/* package (and the
  svelte binding's .svelte.ts runes source) to src for a live, no-build
  workspace run.
- app.svelte / command-palette.svelte port the React demo to Svelte 5:
  useMachine over the shared machine, normalize → lowercase DOM props,
  the ⌘K shortcut as a ComponentEffect, input-focus as an $effect.
- Updates sandbox/README.md to four substrates.

Co-authored-by: Claude <noreply@anthropic.com>
@ivanbanov ivanbanov added the 🚧 WIP This will not be worked on label Jun 23, 2026
@vercel

vercel Bot commented Jun 23, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
dunky-state-machine Ready Ready Preview Oct 8, 2026 11:38am UTC
state-machine-website Ready Ready Preview Oct 8, 2026 11:38am UTC

Co-authored-by: Claude <noreply@anthropic.com>
ivanbanov and others added 12 commits October 4, 2026 20:59
Brings the branch up to main (renamed targets, per-package tsconfigs,
state-machine-dom, TypeScript 7, the opentui target, the website
redesign). Conflicts resolved by taking main's side and re-adding the
svelte entries: the tsconfig path, the svelte vitest project next to the
solid one, the Integrations sidebar entry after Solid, and the sandbox
README lines. The lockfile is main's, regenerated by pnpm install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… conventions

Renames the package to @dunky.dev/svelte-state-machine (the #43
convention) across the manifest, tsconfig path, sandbox, docs, and
changeset, and conforms the manifest to main: exact workspace:* pins for
internal deps (#59) and a header-only CHANGELOG, as the solid target was
introduced (#32).

Drops the ComponentEffects alias (#49): useMachine takes a plain
ComponentEffect[] like every other target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirrors packages/solid: the svelte project (vite-plugin-svelte 7.3.1 +
svelteTesting() from @testing-library/svelte 5.4.2) lives with its
package and is referenced from the root config, so the root carries no
Svelte dependencies. node by default; DOM tests opt into jsdom per file.
The package joins tsconfig.build.json, so `pnpm typecheck` covers its
TypeScript.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
normalize kept its own copies of the aria- projection, the payload
adapters, and AnyEvent, and its adapters handed out the event's
preventDefault detached, so calling payload.preventDefault() threw
Illegal invocation (main fixed this once, in state-machine-dom). It now
reuses the shared maps and adapters and keeps only what is Svelte's own.

Svelte 5 event props are the DOM's handler attribute names, lowercase
and case-sensitive, which is exactly the shared React-cased map
lowercased, so the map is derived rather than restated; onValueChange ->
oninput and onDoublePress -> ondblclick are the two named keys. The maps
are vocabulary-typed (HandlerTargets/AttrTargets) and run through the
shared accounting fixture, and a jsdom suite fires every mapped handler
from the real DOM event a user action produces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mergeProps reimplemented the handler composition and dropped the base's
camelCase composition. It now runs the agnostic mergeProps and composes
Svelte's lowercase event props (onclick, onkeydown) with the shared
composeHandlers, so the defaultPrevented veto is written once. Extending
the composition on the Svelte side keeps the published base untouched
for every other target.

class merges as [consumer, library] whatever its shape: Svelte 5.16+
resolves strings, arrays, and objects through clsx, so the consumer's
array or object class is no longer dropped. Overlapping style strings
join with the library last, so its declarations win. Typed like the
react and solid mergeProps, so Svelte's element prop types pass in and
out cast-free.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ine faithfully

useMachine:
- the documented `() => props` usage never reached the snapshot: the
  connector stored the live props proxy and the setProps effect read no
  keys, so it never re-ran and Object.is short-circuited anyway. It now
  seeds a plain copy and spreads the props inside the effect, so every
  key is tracked and setProps value-dedups.
- the api is the connector's own memoized object ($state.raw), not a
  deep-proxied copy with a new identity.
- the connector subscription is a pre-effect: it runs synchronously at
  init (never on the server), so a send from a child's mount effect -
  which runs before the parent's - reaches the api instead of leaving it
  stale.
- a ComponentEffect re-runs only when a named dep's value changes: one
  $derived per dep dedups it whatever the props getter reads (including
  getters that build their own object), and the body runs untracked so
  a prop it merely reads is never a hidden dependency.
- lifecycle matches React's measured order: start, then the component
  effects, after mount; stop, then their cleanups, on destroy. Effects
  never run under svelte/server, pinned by an SSR suite.

useSelector:
- the selected value is held raw, never proxied, with the same
  init-time subscription; the props its selector closes over re-seed
  the value and the dedup baseline, and notify-time evaluations run
  untracked so a send inside an effect adds no dependencies to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… with svelte-check

The PR shipped raw TypeScript src behind a svelte export condition, so a
consumer's tsc and svelte-check type-checked our sources under their own
compiler options. tsdown can't take over: bundling folds the .svelte.ts
runes modules into a plain index.js the Svelte compiler never sees, and
a runes-free build has no public effect primitive that tracks props.

@sveltejs/package 3 is the official convention: it strips the types,
keeps each runes module as a .svelte.js file for the consumer's
compiler, and emits a .d.ts per module. The published exports resolve
through the types and svelte conditions only, so a tool that would load
uncompiled runes fails loudly instead. The root build runs it after
tsdown (so changeset:publish builds it too) and publint checks the
packed manifest. Module specifiers are explicit .js: the output ships
unbundled. state-machine-bindings becomes a dependency because the
per-module declarations reference its types.

svelte-package 3 and svelte-check 4.7 need the classic TypeScript JS
API, which TypeScript 7 no longer ships, so the package pins a local
typescript@^6. pnpm typecheck now runs svelte-check after tsc -b, which
type-checks the .svelte test harnesses tsc can only see as a shim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The demo now renders through the stylesheet the React and Solid apps
share (same classes, same copy, the Palette typo fixed), on the latest
toolchain: svelte 5.57.1, vite-plugin-svelte 7.3.1, vite 8.3.2. It
dedupes svelte like the solid sandbox dedupes solid-js, since the
aliased binding would otherwise load its own runtime, and drops the
svelte.config.js that configured nothing. The active row uses Svelte's
class array, and the two pointer-only backdrop handlers say why they
skip the a11y key-handler rule rather than carrying no-op handlers.

lint-staged now runs oxlint on .svelte files, which lints their script
blocks; oxfmt has no .svelte support yet, so they are formatted by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The peer range was `^5`. mergeProps hands Svelte a `[consumer, library]`
class, which renders only since 5.16 resolved class arrays and objects
through clsx; 5.33.5-5.34.4 kept a spread's first non-delegated event
handler (sveltejs/svelte#15961, fixed in #16180), so a re-rendered part's
onpointerenter/onfocus would call a stale connect() closure. Every other
API used ($state.raw, $effect.pre, $derived, untrack) is 5.0.

The range is the measured one: this suite passes on 5.16.0, 5.33.4,
5.34.5, and the last patch of every minor from 5.16 to 5.57, and fails
on 5.15 (class) and 5.33.5-5.34.4 (stale handler).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The package README now mirrors the solid one: a quick start, the bridge
reference (why props go in as a getter, the raw snapshot, the effect
order), the measured version support, and what a consumer's build needs
to compile the shipped runes modules. The website page follows the
current react/solid structure with Svelte 5 runes examples.

AGENTS.md, ARCHITECTURE.md, the ACCESSIBILITY.md hidden mapping tree,
the effects page, CONTRIBUTING's sandbox commands (solid was missing
too), and the React sandbox's stylesheet note now name svelte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The demo resolves state-machine-dom and -utils through its Vite aliases
into the binding's source, so listing them only added two knip findings.
CONTRIBUTING's typecheck row now mentions the svelte-check pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ivanbanov and others added 16 commits October 5, 2026 16:43
The pure suite re-asserted the shared aria- projection value by value,
which state-machine-dom pins once and the vocabulary-accounting suite
already accounts for here. It now covers only Svelte's own translation
(lowercase event props, oninput/ondblclick, tabindex, the adapter
wrapping) and the loop's pass-through rules; the DOM suite still pins
how ARIA booleans render.

Also scopes the PR to the svelte target: the React sandbox's stylesheet
note and the Solid line in CONTRIBUTING are left to their own targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… children

Core notifies synchronously inside send(), so everything that woke on
it ran in the tracking scope of whichever effect sent. A reaction
callback that read or wrote state (onOpenChange counting, logging) made
that effect invalidate itself until effect_update_depth_exceeded, and
connect()'s reads made a child's mount effect re-send on unrelated
state. The machine's send is now wrapped in untrack before the
connector captures it.

The api is a $derived over a write-only version signal and the
connector's lazy snapshot: connect() runs after a transition settles
(it used to run mid-transition and could throw out of send()), once per
flush and only when read - 4 -> 1 calls for a 4-patch transition, 11 ->
1 for a same-tick burst - and the server renders the snapshot a send
during script init produced.

The stop teardown moved to the pre-effect created before the template,
so the machine stops before child components tear down, as React's
unmount does; a child's cleanup send no longer fires reactions. The
props getter is read through one $derived (6 -> 1 calls at mount).

useSelector keeps one subscription and one dedup baseline for machine
and prop changes, so isEqual now also gates a prop-driven re-seed
(equal object selections no longer wake readers), with 3 -> 2
evaluations at mount and no re-subscribe on a prop change. It also
takes the machine through a getter, re-subscribing when the machine
changes and avoiding Svelte's state_referenced_locally warning.

The suites drop three tests implied by others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…handler map

mergeProps ran the shared base and then a second, regex-gated pass for
Svelte's lowercase event props. It is one pass now (3.5 -> 6.7 M ops/s
on a typical part), composing every on* key through the shared
composeHandlers so the veto stays written once; extending the published
base instead would leave the packed package depending on a utils
release that doesn't exist yet. Overlapping string classes join into a
string again, since a consumer component interpolating `class` would
render an array as `a,b`; other shapes still merge as [consumer,
library]. A nullish library class or style no longer erases the
consumer's.

normalize and mergeProps carry symbol keys, so a Svelte attachment on
either side still attaches. Non-vocabulary keys still pass through
untouched, a camelCase onMouseDown included: only the vocabulary is
translated, and that record lives in the maps.

HANDLER_MAP lists all seventeen handlers as a Record<HandlerKey,
string>: greppable, and a new vocabulary handler fails to compile until
Svelte maps it. That also leaves state-machine-dom untouched, so its
comments no longer need a casing contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the tree

The focus effect read view.api.open directly, and view.api is replaced
on every machine change, so it re-focused the input on each keystroke,
arrow, and hover (5 calls across 5 keystrokes and an arrow, now 0). It
reads a $derived slice that changes only on open/close.

The sandbox tree used Unicode arrows; AGENTS.md asks for plain ASCII in
diagrams, so the whole tree now uses ->.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The root typecheck hard-coded the svelte package; it now runs tsc -b and
then any package's own typecheck script (only svelte's svelte-check
today), so the package scripts are the contract. The svelte tsconfig's
types override was redundant: its modules import svelte, which brings
the rune and *.svelte declarations along.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rules

The README, the website page, and the changeset now say what the bridge
does after the review fixes: connect() runs lazily once a transition
settles, sends never lend dependencies to the effect that sent, the
machine stops before children tear down, and props compare shallowly
(pass a new reference). They retract the claim that view.api changes
only on real changes - it is a fresh object per machine change - and
teach the two effect rules: read $derived slices, and act through
untrack or view.machine.send. The samples spread `rest` rather than the
whole props onto an element, and pass a prop machine to useSelector as
a getter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nit sends

Before Svelte 5.52, a server-compiled $derived in a module captured its
value when declared, so a send during script init (`if (autoOpen)
view.machine.send(...)`) server-rendered the stale snapshot and
mismatched hydration on 5.16-5.51. The $derived bought nothing: the
connector already memoizes and every notify yields a new snapshot. The
api getter now reads the version signal and returns the connector's
snapshot directly - just as lazy on the client, always current on the
server, on every supported version.

Also pins the shallow-props contract the docs describe: a replaced prop
reaches the api, an in-place mutation does not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
svelte-check flagged rerender({ box }) against a prop the harness never
declared; the untyped mount helper had hidden it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ject.is deps

Not every notification passes through the wrapped send: a core effect's
setContext and an action's own send (captured before the wrap) reached
reaction callbacks inside the consumer effect that caused them, so
their reads became that effect's dependencies. Each reaction's selector
and callback now runs untracked where the connector consumes it,
whatever notified; the send wrapper stays for guards, actions, and the
consumer's own machine listeners.

A read of view.api from inside a send - a reaction's callback, a
selector over the api - ran connect() after setState but before the
entry actions, so a connect relying on an entry-action invariant threw
out of send() and left the transition half-applied. While a send is in
flight the getter returns the last settled snapshot; a selector over
the api catches up in the next flush because the getter tracks the
version signal. after() timers enqueue without the wrapper, so a core
"settled" hook remains the complete fix.

ComponentEffect deps now compare with Object.is, as React's dep array
does ($derived uses ===): a NaN dep no longer re-runs on unrelated prop
changes, and 0 -> -0 re-runs. The init copy of the props moved into
untrack(), which drops the state_referenced_locally warning every
compile of the module printed.

The connect-reads test passed with or without the send wrapper since
connect went lazy; it now pins the wrapper through a consumer's own
machine listener.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er machine, no throws into send

On the server pre-effects never run, so `current` stayed the value
captured at creation: a send later in the script showed in view.api but
not in the selection, and SSR rendered the wrong branch (silently
replaced on hydration). Until the client subscription is live, the
getter reads the selector.

A machine getter re-subscribed on every unrelated prop change when the
props share one signal (a parent's {...spread}); the machine now goes
through a $derived, so only a different machine re-subscribes.

A reader its parent drops on this very change still hears the change's
notification, and a selector reading an index that is gone threw out of
the sender's send(), aborting the notification and leaving the machine
half-applied. The notification-time evaluation now catches and retries
in the next flush: a destroyed reader is skipped there, a mounted one
surfaces the error in Svelte's flush instead of in send().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ame merge rules as they are

The pure suite asserted each handler's prop name, which the DOM suite
proves on a real element from an independent table of DOM event types;
it keeps what the DOM can't see (unwrapped handlers, tabindex, the
pass-through rules) and the vocabulary accounting. The merge-props
tests no longer say "inherits from the agnostic base", which mergeProps
stopped calling, and pin the documented rule that a nullish library
handler wins while a nullish class or style keeps the consumer's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… must inline

Every Svelte sample in the README and on the website now compiles under
strict TypeScript with svelte-check --fail-on-warnings against the packed
package: typed connects, setup.as<Context, Event>() (setup.infer() pins
the event to never), Record parts that normalize accepts, a ./dialog
module that exports what the effect and component samples import, and
an Escape check that respects the config's closeOnEscape default.

The effect rules say what holds: an effect that reads view.api must not
also act (view.machine.send doesn't help once it has read the api), and
selectors and reaction callbacks read the machine. The docs also cover
the settled api mid-send, Object.is deps, the retry for a selector that
throws mid-send, the nullish-handler rule, the missing `this` on composed
handlers, and that the attachment test needs Svelte 5.29.

"Vitest needs no server.deps.inline" held only while the adapter is in
the test root's dependency graph. A harness that reaches it through
other packages' sources (the UI repo's layout) loads the runes
uncompiled (rune_outside_svelte, reproduced) unless it inlines the
adapter; the README, the website, and the changeset now show the snippet.
SSR builds need nothing extra: with no Node-resolvable export condition,
Vite bundles the adapter instead of externalizing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The version signal was bumped mid-send while reads during the send got
the settled snapshot, so anything re-evaluated mid-send - a $derived
slice a callback reads, a parent's slice feeding a child selector's
prop, $inspect, a flushSync() in a callback - cached the pre-send value
against the already-bumped version and was never invalidated: a parent
deriving `view.api.open` stayed false after the machine opened. The
bump now waits for the outermost send to end (in `finally`, so a
throwing send still releases it), and a later read refreshes.

`settled` was refreshed only by a read outside a send, so with two
sends in one tick a callback in the second saw the snapshot from before
the first; the outermost send now refreshes it first when something
changed. Transitions that don't start at view.machine.send (after
timers, an action's own async send) remain unguarded, now said where
the guard lives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he new state

The notification-time catch covered only the selector: a dropped row
with an object selection and a custom isEqual still threw out of the
sender's send(), half-applying the transition. isEqual now runs inside
the same try.

Teardown reset `live`, so every later `.current` read ran the selector
against the new state - a dropped row's effect cleanup unregistering
itself re-selected and the zombie error resurfaced from the flush. The
subscription teardown no longer touches `live` (it only needs to start
false, for the server), so a torn-down reader keeps its last value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ded transitions

The README, the website, and the changeset now say that reads inside a
view.machine.send see the snapshot the previous send settled to and
refresh once the send ends, that flushSync() in a callback therefore
renders the pre-send UI (await tick() to focus newly opened content),
and that transitions starting elsewhere - after timers, an action's own
async send, a watcher fed by a core effect's setContext - aren't
guarded, so selectors and callbacks should read the machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ivanbanov ivanbanov changed the title feat: Svelte 5 integration feat(svelte): Svelte 5 integration Oct 8, 2026
@ivanbanov ivanbanov removed the 🚧 WIP This will not be worked on label Oct 8, 2026

This branch was successfully deployed

1 active deployment
Preview — 81142c5d Deployed Oct 8, 2026 by vercel[bot]
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