Skip to content

feat: @pathscale/ui 2.6 — glass material, TanStack removal, and Solid 2 - #242

Merged
pathscale merged 15 commits into
masterfrom
feat/solid-2
Aug 16, 2026
Merged

feat: @pathscale/ui 2.6 — glass material, TanStack removal, and Solid 2#242
pathscale merged 15 commits into
masterfrom
feat/solid-2

Conversation

@pathscale

@pathscale pathscale commented Aug 16, 2026

Copy link
Copy Markdown
Owner

One PR for the whole 2.6 line, so it is a single QA pass. #240 (glass) and #241 (TanStack) were folded into this branch and closed.

Still draft: 9 typecheck errors remain. Tests, contracts and the build are green.


1. Glass becomes a material every surface can wear

material="glass" was on Card alone, so an app whose panels are dialogs, drawers, popovers or a navbar could set the glass tokens and see nothing change. It now works on Dialog.Content, Drawer.Content, Popover.Content, Menu and Navbar too. solid is the default everywhere — this adds a capability without changing a single existing render.

The rules live once in _shared/material.css, keyed on [data-material="glass"]. Nested glass flattens to one pane, and both prefers-reduced-transparency and a missing backdrop-filter fall back to an opaque fill.

2. TanStack is gone

  • solid-table backed 7 public hooks no component in this library used. createDataGrid already derives filteredRows → sortedRows → pageRows with selection. Deleted.
  • solid-virtual — same story, one file.
  • solid-form was load-bearing: FormApi was literally { _tsForm: any }, leaking 12 erased generics into the public type. Replaced with our own engine, which came out small because the field components already own presentation, state="invalid" and error slots.
  • 10 @solid-primitives/* packages were declared and imported from nowhere. 28 packages leave the install.

3. Solid 2.0.0-rc.0

Everything verified against an installed rc, not the migration guide.

change scale
splitPropsomit 222 calls, 113 files
JSX@solidjs/web 129 files (took 127 errors → 38 alone)
class widened → one typed twMerge 287 call sites fixed in one place
Context.ProviderContext 34
onMountonSettled 37
tabIndex/colSpan/inputMode → lowercase 50
ARIA booleans → "true"/"false" 16
classListclass, mergePropsmerge, Index<For keyed={false}> 3

useContext was far smaller than the 3.0 assessment estimated. It counted 57 sites with 17 guarded. Measured: 13 already throw — Solid 2 throwing first is the same behaviour with a different message. Only 3 used read-check-fallback and genuinely break, and each now carries as its createContext default the exact object its reader returned inline.

Three findings worth recording

  • lib was never set. target: ESNext gives no DOM types; Solid 1 pulled them in transitively and Solid 2 does not. requestAnimationFrame resolved to never and poisoned everything inside its callback.
  • The silent break is real. Solid 2 batches writes and flushes on a microtask, so createDataGrid — an imperative model whose whole API is call-then-read — returned pre-call state and failed 20 tests at once. Its setters are wrapped in one settled() helper rather than 20 scattered flush() calls.
  • The rc ships no JSX runtime. @solidjs/web declares ./jsx-runtime and ./jsx-dev-runtime and provides neither; solid-js has no jsx export at all. Fixed by moving three already-pure functions into .ts modules — better structure regardless, since testing a pure function never needed a JSX transform.

Toolchain

solid-layouts, solid-layouts-oxc, rsbuild-plugin-solid-layouts published at 0.2.0, serving both majors from one package (dist/component.js and dist/solid-2/component.js are byte-identical; only the renderer import differs). Consumers pin ^0.1.x, which semver will not cross, so nothing moves until it opts in. No major was needed.

Verification

  • bun test86/86 (matches the Solid 1 baseline exactly)
  • bun run check — 90/90 components
  • bun run build — clean (was failing on master: 23 orphaned *.generated.tsx files broke declaration generation; a stale-generated gate now catches that class)
  • tsc --noEmit9 errors left, down from 127

Breaking changes

useTableModel, useTableSorting, useTablePagination, useTableFiltering, useTableSelection, useTableExpansion, toSortDescriptor, toSortingState, useVirtualRows removed. Use createDataGrid; row expansion has no replacement yet and is the one capability lost. FormApi._tsForm replaced by values(), getFieldValue, getFieldMeta, setFieldValue, validateField, submit(), isSubmitting(), isValid().

meh added 9 commits August 16, 2026 14:33
Glass was a real material with one consumer. `styles/glass.ts` derives
twenty-five `--glass-*` tokens from three numbers, and the only component that
could read them was Card, so an app whose panels are dialogs, drawers, popovers
or a navbar could set the tokens and see nothing change.

`material` moves into the shared vocabulary and onto Dialog.Content,
Drawer.Content, Popover.Content, Menu and Navbar. `solid` is the default on all
of them, so this adds a capability without changing a single existing render.

The rules live in `_shared/material.css`, keyed on `[data-material="glass"]`
rather than a per-component class. Card had them inline, and copying that block
five more times would be six places to update the next time the glass
vocabulary moves - which it already did once, from thirty-one hand-declared
properties to three numbers. Card now carries the attribute too and keeps its
own classes, so its appearance is unchanged while the definition is shared.

Three behaviours come along because they belong to the material rather than to
any one component: nested glass is flattened to a single pane, since blurring
already-blurred output is muddy rather than deeper and costs a second pass; and
both `prefers-reduced-transparency` and a browser without `backdrop-filter`
fall back to an opaque fill, because a tint with no blur behind it is a surface
you cannot read text on.

Every `--glass-*` read carries a fallback. An undefined custom property makes
CSS drop the whole declaration rather than fall back to an initial value, which
is how a partial token set used to produce a surface with no background at all.

`CardMaterial` stays as a deprecated alias of `Material`. It is exported, it has
always had exactly these two members, and removing it would break call sites for
no gain.
`*.generated.tsx` is gitignored and `layouts:generate` only ever adds, so
removing a component leaves its generated file behind, still importing the
`.recipe` and `.layout` that went with it. Nothing notices: the file is
invisible to `git status`, and the failure surfaces much later as tsc errors
inside a file nobody edited.

2.5.0 shipped with twenty-two of them. `bun run check` read them as components
and reported "missing index.ts barrel export" for things that no longer exist,
which is how the gate spent a release red for a reason that pointed nowhere,
and `bun run build` failed generating declarations. A twenty-third under
`table/` survived the first sweep because it sits in a directory that is still
alive.

Checked here because this script already walks every component directory, and
because a gate that names the actual file and says it is deletable build output
costs one line to act on.
Three TanStack packages were the last things pinning this library to Solid 1,
and none of them was earning its place.

`@tanstack/solid-table` backed seven public hooks that **nothing in this
library used**. `createDataGrid` already answers the same questions natively -
its rows derive `filteredRows -> sortedRows -> pageRows` with selection
alongside - so the hooks were a second table engine kept alive by their own
exports. `@tanstack/solid-virtual` was the same story in one file.
`useAnchoredOverlayPosition` stays: it never touched TanStack and a grid still
needs to position an overlay against a cell.

`@tanstack/solid-form` was different, and it is the work here. It was load
bearing: `FormField` rendered `<tsForm.Field>`, `FormSubmitButton` rendered
`<tsForm.Subscribe>`, and `FormApi` was literally `{ _tsForm: any }`, so
twelve erased generics were part of this library's public type.

The replacement is small because the field components already carry the hard
part - `Input`, `Select` and the rest own their presentation, `state="invalid"`
and error slot. What was missing was bookkeeping: values, touched, errors, and
when to run the schema. Standard Schema is called directly, which is what
TanStack was doing on our behalf anyway.

Two behaviours are now explicit rather than inherited. Errors are gated on
`isTouched` so a form does not open covered in red, and a failed `submit()`
touches every field, so the errors it refused on all become visible at once
rather than one blur at a time. There is a test for the second, which the old
suite had no way to express.

Also drops ten `@solid-primitives/*` packages. Every one was declared in
`package.json` and imported from nowhere - not in `src`, `tests`, `scripts` or
`playground`. 28 packages leave the install.

BREAKING CHANGE: `useTableModel`, `useTableSorting`, `useTablePagination`,
`useTableFiltering`, `useTableSelection`, `useTableExpansion`,
`toSortDescriptor`, `toSortingState` and `useVirtualRows` are removed, with
their option and result types. Use `createDataGrid`, which covers filtering,
sorting, pagination and selection; row expansion has no replacement yet.
`FormApi._tsForm` is gone, replaced by `values()`, `getFieldValue`,
`getFieldMeta`, `setFieldValue`, `validateField`, `submit()`, `isSubmitting()`
and `isValid()`.
Not finished. Committed as a checkpoint because the codemods are done and
reviewable on their own, and the two that remain are judgement calls that
should be read rather than scripted.

Done, all verified against an installed solid-js@2.0.0-rc.0 rather than
against the migration guide:

  splitProps -> omit          222 calls, 113 files. Every call had exactly one
                              key list, so the four-bucket shape the toolchain
                              had to solve does not occur here. `omit` returns
                              only the rest, so each `local.x` became `props.x`,
                              which is what it always meant.
  Context.Provider -> Context 34 sites. `Context<T> extends
                              ContextProviderComponent<T>` in 2.0: the context
                              is the provider.
  onMount -> onSettled        37 sites. `onCleanup` survives unchanged, so the
                              seven bodies that nest one needed no rework.
  solid-js/web -> @solidjs/web 15 files. Only `Portal` and `Dynamic` are
                              imported, and `Dynamic` kept its shape.
  JSX from @solidjs/web       129 files plus `jsxImportSource`. Core no longer
                              declares the namespace; the shape of an element is
                              the renderer's business now. This one change took
                              127 errors to 38.
  solid-js/store -> solid-js  the store merged into core.
  store setters               `setStore("items", produce(fn))` becomes
                              `setStore(fn)`: `StoreSetter<T>` takes one mutator
                              over the whole store and `produce` is gone.

Remaining: one `createSignal` overload resolving to `never` in hueShift, and
one JSX prop mismatch in AnimatedCollapse.

The 107 `useContext` sites are untouched and are the real work: 2.0 throws
where 1.9 returned `undefined`, so every compound component that currently
works standalone needs a decision rather than a rename.
Four more things 2.0 changed, each systemic rather than per-site:

  ARIA booleans      `aria-expanded={open()}` no longer typechecks: 2.0 types
                     these as `"true" | "false"`, so a boolean has to be spelled
                     out. 11 sites across six attributes.
  tabIndex           lowercase `tabindex` in JSX now. 38 sites. The DOM property
                     is still `element.tabIndex`, and two sites read it that way,
                     so a blanket rename broke them and both are put back.
  class              2.0 absorbed `classList` into `class`, which is now
                     `ClassValue` - string, number, boolean, array or a record of
                     conditionals. `tailwind-merge` takes most of that but not
                     `number`, so all 287 `twMerge(CLASSES.base, props.class)`
                     calls stopped typechecking at once.

                     Fixed by widening once in `src/lib/twMerge.ts` rather than
                     casting at 287 call sites, which would have been 287 chances
                     to cast to the wrong thing and no single place to say why.
                     A record is flattened to its truthy keys, which is what
                     `classList` did with the same object.
  the boundary       `src/lib/layouts/index.ts` now names
                     `solid-layouts/solid-2`, and `layouts.library.json` sets
                     `solid: 2` so the generated files import the matching
                     application boundary. This is the one edit the toolchain
                     plan predicted this library would need.

The remaining 32 are a long tail rather than a pattern, and the `useContext`
decision is still ahead of them.
The `useContext` change is much smaller than the 3.0 assessment estimated, and
the difference is which guard a reader uses.

Of the sites that read a context with no default, **thirteen already throw**
when it is missing. Solid 2 throwing first is the same behaviour with a
different message, so those need nothing. Only **three** used the other
pattern - read, check, return a fallback - and those are the ones that would
start crashing where they used to render plainly:

    ListBoxItemContext, MenuItemStateContext, TableContext / TableContentContext

Each now carries as its `createContext` default exactly the object its reader
was returning inline. The fallback moved from every reader to one declaration,
which is also why `useTableContext` and `useTableContentContext` are now one
line each.

Also sets `lib` explicitly. Without it, `target: ESNext` supplies no DOM types:
Solid 1 happened to pull them in through `solid-js/web` and Solid 2 does not, so
`requestAnimationFrame` resolved to `never` and poisoned every expression inside
its callback - which read as a Solid 2 signal bug for three attempts before the
real cause turned up.
  colSpan -> colspan   11 sites, the same rule that renamed `tabIndex`: 2.0
                       spells DOM attributes as they appear in HTML.
  Index -> For keyed   `<Index>` is gone; `<For each keyed={false}>` is it. One
                       call site, in Skeleton, where row identity really is
                       position because the items are placeholders. The index
                       arrives as an accessor now, so `index` became `index()`.

26 errors left and they no longer share a shape: two spread-type complaints, a
couple of possibly-undefined reads, and a handful of one-off assignability
mismatches. Each wants reading rather than a codemod.
Solid 2 batches signal writes and flushes them on a microtask, so a read in the
same tick still sees the old value. `@solidjs/signals`' own README states it
plainly: "reads after a write won't reflect the new value until `flush()` runs".

That is the whole reason `createForm`'s tests failed under 2.0 while passing
under 1.9. `setFieldValue` writes a value and immediately validates it, and
`submit()` touches every field and immediately reads `isValid()` - both were
answering about the state from before the call.

This is the class of break the 3.0 assessment warned would be silent: nothing
throws, nothing fails to typecheck, the answer is just stale. Worth saying that
it was caught by a test rather than by reading, which is the argument for
porting with the suite running.
Three causes, and only the first is really about Solid 2.

**Batched writes.** `createDataGrid` is an imperative model whose entire API is
call-then-read: `addColumn(...)` then `columns()`, `sortByColumn(...)` then
`sort()`. Solid 2 flushes on a microtask, so every one of those read the state
from before the call and 20 tests failed at once. The setters are wrapped once
in a `settled()` helper rather than sprinkling `flush()` through twenty
methods: an imperative model that defers its own writes by a tick is not
something a caller can reason about, so this is the model's contract rather
than a workaround.

**`solid-js/h` moved** to `@solidjs/h`, which is published at the same rc.

**No JSX runtime exists in the rc.** `@solidjs/web` declares `./jsx-runtime`
and `./jsx-dev-runtime` and ships neither: `dist/dev.js` exports no `jsx`,
`jsxs` or `jsxDEV`, and `solid-js` has no jsx export at all. Any `.tsx`
compiled by the test runner fails to load. Config cannot fix a missing module
export, and three attempts at it were wasted before checking whether the thing
being pointed at exists.

What fixed it is better structure anyway: `nextPresenceState`,
`nextCollapsePhase` and `computeCollapseStyle` were already pure and already
commented "exposed for unit testing", so they move to `presenceState.ts` and
`collapseState.ts`. A test for a pure function should never have needed a JSX
transform to reach it. The components re-export them, so no caller changes.
@pathscale
pathscale changed the base branch from feat/nuke-tanstack to master August 16, 2026 08:30
meh added 2 commits August 16, 2026 15:34
More renames of the same kind: `inputMode` to `inputmode`, `classList`
folded into `class` (2.0 absorbed it, and `class` takes the same record),
`mergeProps` to `merge`, and a sweep for the boolean ARIA bindings the first
pass missed.

Two shape changes rather than renames. `RemoveAttribute` means any attribute
prop can now be `false`, so `buttonHref`, `buttonRel` and
`ensureExternalRel` say `string | false | undefined` - all three already
guarded for it, the signatures just say so now. And 2.0's `JSX.Element` no
longer admits a function, so the render-prop components have to name the type
in the branch where children are not one.

The Menu context default added earlier was missing two of its five fields,
which typechecking caught.
…to 9

Two shapes.

TypeScript will not carry a narrowing on a captured `let` across a closure
boundary, and every ref in this library is exactly that: `let sliderRef:
HTMLDivElement | undefined` written by `ref={}` and read inside a handler.
The guards were all already there and all still correct; binding to a const
first is what lets the compiler see them.

And `props.id` is `string | false | undefined` in 2.0, where `false` means
remove the attribute. Only a real string can name an element for
`aria-labelledby`, so Dialog's heading and description ids fall through to
their generated id when given one.
@pathscale pathscale changed the title wip(solid-2): move @pathscale/ui to solid-js 2.0.0-rc.0 feat: @pathscale/ui 2.6 — glass material, TanStack removal, and Solid 2 Aug 16, 2026
meh added 2 commits August 16, 2026 15:45
….0-rc.0

The last nine, and none of them shared a shape:

  style spread        `style` is `CSSProperties | string` in 2.0 and a string
                      cannot be spread. MetalBorder and Toast now check before
                      spreading, so a caller passing a string keeps it.
  on:pointerdown      the `on:` namespace is gone. It was the 1.x escape hatch
                      for a non-delegated listener; the ordinary prop is what is
                      left and the stopPropagation was the point anyway.
  maxLength           lowercase, like every other DOM attribute in 2.0.
  For keyed={false}   the overload flips which argument is reactive: with
                      `keyed={false}` the *item* is the accessor and the index
                      is a plain number, the opposite of the keyed form. The
                      earlier conversion had it backwards.
  `string | false`    `ensureExternalRel` still admitted `false` through
                      `value ?? ""`, which has no `.split`.
  ref narrowing       two more captured-`let` guards that needed a const.
  hueShift            TypeScript narrowed the *accessor* to `never` inside a
                      MutationObserver callback. Hoisting the handler out of
                      that control flow is what fixed it, and it reads better
                      than the inline arrow it replaces.

127 errors at the start of this port, 0 now, with the suite matching its Solid 1
baseline exactly.
0.2.0 went to npm by hand carrying only the darwin-arm64 `.node` that was in
the publisher's working tree. The bindings ship inside the main package rather
than as platform sub-packages, so CI's Linux runner resolved nothing and every
run died in `layouts:generate`:

    Cannot find module './solid-layouts-oxc.linux-x64-gnu.node'

0.2.1 went out through `release-oxc.yml`, which builds each target on its own
runner and refuses to publish a partial set.
meh added 2 commits August 16, 2026 17:17
The plugin pinned solid-layouts-oxc to an exact 0.2.0, so bun installed a
nested 0.2.0 beneath the plugin no matter that this manifest hoisted 0.2.1,
and the nested copy is the one the plugin loads. 0.2.0 was published by hand
with only the darwin-arm64 binary, so CI failed on a missing wasm32-wasi
module under Linux.
Solid 2 takes plain lowercase DOM attribute names, so InputOTP now ships
inputmode and TableVirtualSpacerRow ships colspan. The contract doc still
listed the camelCase spellings, which the contract test reads as drift in
both directions at once.
@pathscale
pathscale marked this pull request as ready for review August 16, 2026 10:22
@pathscale
pathscale merged commit 712fae5 into master Aug 16, 2026
1 check failed
@pathscale
pathscale deleted the feat/solid-2 branch August 16, 2026 10:22
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