feat(solid): the Solid substrate with the Dialog primitive - #44
feat(solid): the Solid substrate with the Dialog primitive#44ivanbanov wants to merge 5 commits into
Conversation
packages/solid targets Solid 2.0 (solid-js + @solidjs/web at ^2.0.0-rc.0): @dunky.dev/solid-dialog plus the lifecycle wrappers @dunky.dev/solid-use-focus-trap and @dunky.dev/solid-use-scroll-lock, the @dunky-dev/solid storybook harness (port 6008), scaffold templates, and the workspace wiring (vitest project split, own tsconfig project with jsxImportSource @solidjs/web, per-package tsdown babel config compiling the dist with babel-preset-solid). Solid-specific decisions the code encodes: - Bindings merge inside the JSX spread to stay reactive; children never ride that spread — a re-evaluated spread re-creates them, and a child that writes to the machine on mount (Title's presence) would loop forever. - Two-phase effects compute eagerly, before refs fill: useFocusTrap re-reads its target at apply time, and the shared backdrop handle is a plain box. - The dialog context uses a null default so the root's parent-depth lookup stays non-throwing (a default-less context throws on unprovided reads). The lockfile is deliberately not committed: @dunky.dev/solid-state-machine isn't on npm yet (state-machine PR #32); local dev uses a pnpm override pointing at a packed tarball of that branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Solid 2.0 renders a boolean attribute as presence/absence, so
aria-expanded={false} disappeared and aria-modal={true} rendered empty.
ARIA states are literal "true"/"false" tokens — serialize them explicitly.
Found by the solid-dialog binding in dunky-dev/ui#44.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Solid 2.0 renders a boolean attribute as presence/absence, so
aria-expanded={false} disappeared and aria-modal={true} rendered empty.
ARIA states are literal "true"/"false" tokens — serialize them explicitly.
Found by the solid-dialog binding in dunky-dev/ui#44.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rc.1 fixes the reconcile store corruption the adapter worked around — the matching adapter build drops the detour, so the substrate rides the plain reconcile path. Peer ranges move to ^2.0.0-rc.1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cut the comments down to the constraint each one protects — one or two lines, no narration. Comment-only change on top of the Solid substrate (#44); no code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
refactor(solid): simplify the code comments
|
Status sync (2026-08-21) — this PR has a hard upstream dependency: dunky-dev/state-machine#32.
The lockfile stays uncommitted on purpose — it cannot resolve a package that does not exist on the registry. CI goes green only after #32 merges and publishes; nothing in this branch can fix it before then. Local dev now links the adapter straight from the state-machine overrides:
"@dunky.dev/solid-state-machine": "link:<path-to>/state-machine/packages/solid"Everything passes against that link: build, lint, format, typecheck, Unblock checklist, once #32 publishes: drop the override, |
* feat(solid): add @dunky.dev/state-machine-solid integration A first-class Solid bindings target (not a React re-export): useMachine mirrors the connector snapshot into a createStore via reconcile for fine-grained updates, runs the lifecycle through onMount/onCleanup, keeps props fresh with a tracked setProps effect, and runs each ComponentEffect as its own dep-tracked createEffect. useSelector returns a Solid accessor. normalize maps the agnostic bindings to Solid DOM props (onInput, onDblClick, tabindex) and mergeProps applies Solid's class concat + single-object style merge. Also split the tsconfig setup into a tsconfig/ folder (base/react/solid/all) so JSX is a per-project concern, since the repo now has both React and Solid JSX. Wires Solid into tsdown, the vitest solid project, docs, and a changeset. Co-authored-by: Claude <noreply@anthropic.com> * feat(sandbox): add sandbox/solid cmdk demo A Solid renderer for the shared ⌘K command-palette machine, mirroring sandbox/react. Drives the same @sandbox/cmdk-core machine + connect through the Solid bridge: api is a fine-grained store read directly in JSX, Show/For for control flow, a createEffect for focus-on-open, and the same cmdkShortcut ComponentEffect tuple the React sandbox uses. Vite + vite-plugin-solid, aliasing the workspace TS sources. Co-authored-by: Claude <noreply@anthropic.com> * refactor(solid): adopt the substrate-prefix name and manifest conventions Renames the package to @dunky.dev/solid-state-machine (the convention from #43) across the manifest, changelog, tsconfig path alias, sandbox, and changeset. Conforms the manifest to main: ship src/ in the published files (#53), pin internal workspace deps exact (#59), and add the bindings devDep the translation contract needs. The changeset now also states the version policy: solid-js ^1.6 today; Solid 2.0 lands as a separate major once stable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(solid): adopt the bindings translation contract and current target APIs Brings the Solid target up to main's contract changes: - normalize maps are vocabulary-typed (HandlerTargets/AttrTargets) and exported, with keyed lookups — a typo or unknown key is now a compile error (#57/#60). The header carries the non-mechanical translation rationale ACCESSIBILITY.md asks for (focusable -> tabindex 0/-1, disabled -> aria-disabled per APG). - the shared describeVocabularyAccounting fixture runs against the solid maps — the last target missing the conformance suite. - ComponentEffects is folded into ComponentEffect; the effects param is a plain ComponentEffect[] (#49). - mergeProps is generic over the consumer's props, cast-free at call sites, with the expectTypeOf regression test (#51). - tests import the published entry (@dunky.dev/solid-state-machine) instead of ../src, matching every other target. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sandbox): harden the solid demo and align the demo copy - dedupe solid-js in the solid sandbox's vite config: the package alias points inside packages/solid, which carries its own solid-js devDep, so version skew would load two runtimes and silently kill reactivity. - clear the input ref on the Show branch's disposal — the closed palette kept a detached <input> alive until the next open. - sandbox README now tells the four-substrates story: solid in the tree and run instructions, and the lifecycle-hook claim rewritten (three targets share the React hook; Solid brings its own bridge). - react demo copy mentions the Solid version, its page title disambiguates (cmdk - React), and a user-visible Palette typo is fixed in both apps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: add solid to every target enumeration and fix the solid docs page Solid existed in no doc a contributor or consumer reads first: - root README target diagram gains the Solid box; AGENTS.md, ARCHITECTURE.md, and the ACCESSIBILITY.md hidden fan-out example now enumerate all four targets. - the solid docs page examples were silently broken: string-shorthand transitions the core no-ops, and a bare config object instead of setup.infer().createMachine — both now mirror the react page. Also: Show instead of the React && idiom, the mergeProps import line, the stale mapping table replaced with the source link (the 521440e convention), cross-target links, and the solid-js version-support note. - api/effects.mdx no longer states the React-only hooks rule as universal and points at the Solid bridge alongside React Native. - the package README gains the Quick start, the flow diagram, the current heading conventions, and the Solid version support section (^1.6 now; 2.0 as a separate major once stable, with the migration mapped). - drop a trailing comma in the root tsconfig left by the merge resolution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(react,solid): bind the adapted payloads' preventDefault to its event normalize() copied the native event's preventDefault onto the ChangePayload/WheelPayload detached, so calling it threw Illegal invocation. The payload now carries a closure bound to the event. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(solid): run ComponentEffect bodies untracked A prop the effect body merely read became a hidden dependency and re-ran the effect. The authored deps list is now the whole re-run contract, matching the React target's dep array. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(solid): target Solid 2.0 as the first-class peer Port the bridge to the 2.0 primitives: root-level createStore/reconcile, two-phase createEffect(compute, apply), onSettled for the lifecycle, and @solidjs/web for JSX. Peer range is ^2.0.0-rc.0; 1.x is unsupported (2.0 removed the surface the bridge stands on, so the majors are version-split like the rest of the Solid ecosystem). Work around a solid-js 2.0.0-rc.0 bug: reconcile corrupts a store node when it replaces a function-valued property, halting reactivity on the next tracked read. The bridge reconciles a view holding the previous function identities and writes the fresh ones through plain draft assignments, all in one commit. Regression-tested; remove once fixed upstream. The solid vitest project and tsconfig now live in packages/solid so the root workspace carries no Solid dependencies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Update app.tsx Signed-off-by: Ivan Banov <ivanbanov@gmail.com> * Update app.tsx Signed-off-by: Ivan Banov <ivanbanov@gmail.com> * Update README.md Signed-off-by: Ivan Banov <ivanbanov@gmail.com> * refactor(sandbox): share one stylesheet between the React and Solid demos Both apps rendered the same command palette with duplicated inline style objects. Move the shared look into sandbox/shared/styles.css and swap style={} for class/className — each app now supplies only markup, so a future third DOM sandbox has one file to add, not another copy of the same styles. Trims the Solid docs page of comments that just restated the adjacent code. * style(sandbox): rejoin the lead line oxfmt wraps Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(sandbox): move the shared stylesheet into shared/src Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(solid): stringify boolean aria-* values in normalize Solid 2.0 renders a boolean attribute as presence/absence, so aria-expanded={false} disappeared and aria-modal={true} rendered empty. ARIA states are literal "true"/"false" tokens — serialize them explicitly. Found by the solid-dialog binding in dunky-dev/ui#44. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(solid): shorten the reconcile-workaround comment * refactor(solid): rebind preventDefault with bind Same behavior as the arrow-closure wrapper, one line — mirrors the react normalizer on #64. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(solid): target solid-js 2.0.0-rc.1 and drop the reconcile workaround rc.1 fixes the store corruption when reconcile replaces a function-valued property, so the function-leaf detour (stableFunctionView / restoreFunctionLeaves) is gone — the bridge reconciles the snapshot directly. Peer range moves to ^2.0.0-rc.1; the regression test that guarded the detour now passes against the plain path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(solid): close the bridge's behavioral gaps found in review - useSelector: pin the compute-form seed (a selected function comes back by identity, never invoked — the createSignal function-arg hazard) and that a disposed owner stops evaluating its selector (onCleanup(off)). - useMachine: pin the stop half of the lifecycle (reactions stop firing after unmount), that a ComponentEffect's cleanup runs BEFORE its re-run on a dep change, and the negative half of the fine-grained claim (an unrelated field change does not wake a reader). - mergeProps: pin the undefined-consumer early return and the documented string-style fall-through to library-wins. Both regression guards were mutation-verified: reverting the seed to the value form and dropping onCleanup(off) each fail exactly their new test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(dom): extract the shared DOM translation into @dunky.dev/state-machine-dom The aria-* attribute projection and the payload adapters were byte-identical in the React and Solid normalizers, and had already drifted once (the preventDefault bind fix landed in one but not the other). They now live once in packages/dom; each target keeps only what genuinely differs — its handler prop names (onChange/onDoubleClick vs onInput/onDblClick), the focusable → tabindex casing, and its value serialization (React passes ARIA booleans through, Solid stringifies them). Payload construction is pinned once in the dom package's tests; the target suites keep a wiring proof each (normalize wraps the handler with its adapter) plus the vocabulary accounting. No consumer-facing API change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Signed-off-by: Ivan Banov <ivanbanov@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
…der (#63) * chore: give each package its own tsconfig, drop the root tsconfig folder tsconfig/react.json actually typechecked everything except solid, while the solid project already lived in its package — an inconsistent split. Now the root tsconfig.json is the shared base (it also covers the root config files), tsconfig.build.json is the tsc -b solution list, and every package owns its project config. Only react and solid set a JSX flavor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Update tsconfig.json Signed-off-by: Ivan Banov <ivanbanov@gmail.com> * Update tsconfig.build.json Signed-off-by: Ivan Banov <ivanbanov@gmail.com> * Update tsconfig.json Signed-off-by: Ivan Banov <ivanbanov@gmail.com> * fix(solid): stringify boolean aria-* values in normalize Solid 2.0 renders a boolean attribute as presence/absence, so aria-expanded={false} disappeared and aria-modal={true} rendered empty. ARIA states are literal "true"/"false" tokens — serialize them explicitly. Found by the solid-dialog binding in dunky-dev/ui#44. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Signed-off-by: Ivan Banov <ivanbanov@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
What
The Solid substrate, targeting Solid 2.0 (
solid-js+@solidjs/webat^2.0.0-rc.1):@dunky.dev/solid-dialog— the compound Dialog (Trigger, Portal, Backdrop, Viewport, Content, Title, Description, Close), same anatomy and behavior contract as the React binding, with the connected api as a fine-grained store.@dunky.dev/solid-use-focus-trap/@dunky.dev/solid-use-scroll-lock— the Solid lifecycle wrappers over the framework-free DOM utils.@dunky-dev/solidstorybook harness (pnpm dev:solid, port 6008,storybook-solidjs-vite), with the story set mirroring the React one.scripts/templates/packages/solid—pnpm scaffold <name>now stamps Solid too.jsxImportSource: "@solidjs/web"), and a per-package tsdown babel config so the dist is compiled bybabel-preset-solid.Solid-specific decisions
childrennever rides that spread: a re-evaluated spread re-creates the children it carries, and a child that writes to the machine on mount (Title's presence) loops machine -> spread -> remount -> machine. Every part strips it and renders{props.children}explicitly.useFocusTrapre-reads its target at apply time, and the backdrop handle shared between sibling parts is a plain mutable box, not a signal (the layer walk reads it in the same settle that mounts the backdrop).Content'sinitialFocusaccepts an element or an accessor resolved at open time — the Solid idiom for a ref variable that fills during render.nulldefault so the root's parent-depth lookup stays non-throwing (a Solid 2.0 default-less context throws on unprovided reads).Blocked on
@dunky.dev/solid-state-machinefirst (including the normalize fix that stringifies booleanaria-*values — Solid 2.0 renders boolean attributes as presence/absence). The lockfile is deliberately not committed until then; local dev uses an uncommittedpnpm-workspace.yamloverride that links@dunky.dev/solid-state-machinestraight to that branch checkout. The^0.4.0dependency ranges assume the fixed-group publish version — adjust if it differs.Verified
pnpm build(dist compiled to@solidjs/webtemplates, publint clean), and the storybook build.🤖 Generated with Claude Code