Skip to content

feat(ui): migrate PromptSyntax to UI 3.2 - #18

Merged
pathscale merged 22 commits into
masterfrom
feat/ui-3-migration
Sep 13, 2026
Merged

pathscale merged 22 commits into
masterfrom
feat/ui-3-migration

Conversation

@pathscale

@pathscale pathscale commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Moves PromptSyntax onto the published @pathscale/ui@3.2.3 release while preserving the site's storage-blocked-browser handling, complete build output, icons, and route behavior.

The final branch replaces invalid nested link/button combinations with native UI Button links, so accessible and routing semantics match the rendered controls. Its full-root QA profile covers both documents, generated tables of contents, all six audience lenses, route round trips, theme state, geometry, and contrast.

Local verification:

  • TypeScript and production build pass
  • full native ps-qa suite: 131/131
  • strict full-root inventory: 93/93 classified, comprising 77 outcome-declared controls and 16 intentional external/manual links; zero missing, unstable, duplicate, or unverified controls
  • published UI 3.2.3 focused home/theme smoke: 19/19
  • source and built output contain no Google Fonts URLs

The production deployment intentionally has no enforced CSP. This PR does not create or enable one. No deployment is included.

meh added 6 commits September 7, 2026 21:08
The library moved to 3.x. Most of the 3.0 breaking changes were already
satisfied here: this site was written against the 2.11 vocabulary, so it
already passes `class`, `flavor` and `state` rather than `className`,
`color` and the `isDisabled`/`isLoading` booleans, and it uses none of the
eleven renamed components. Three things did need changing.

Checkbox now reports its new value. As of 3.1 `Switch`, `Checkbox` and
`Radio` hand `onChange` the checked boolean instead of the native `Event`,
matching what `Slider`, `RadioGroup` and `CheckboxGroup` always did. The
consent box on the study page was the only call site. It is the change
worth being careful about, because a handler written for an `Event` can
still compile in a loose position and then read `undefined` at run time,
which for this box would silently refuse consent.

The Layout compiler now runs before Babel. `@pathscale/ui` is a compiled
Layout bundle, and the application compiler resolves `<Button>` against the
package manifest. Once the Solid transform has run there is no `<Button>`
left to resolve, only a `_$createComponent` call, so plugin order is
load-bearing rather than cosmetic.

Babel emits `@solidjs/web`. Solid 2 dropped the `solid-js/web` subpath, so
the transform has to name the package the runtime actually lives in. The
SWC pass below was already pointed at it; the two now agree.

`bun run lint` matches the base branch and `rsbuild build` succeeds. The
single remaining type error is the Checkbox handler above: `^3.0.0`
resolves to 3.0.0, which predates that change, and no 3.1 was ever
published to the 3.x line.
The migration commit pinned `^3.0.0`, which predates every breaking change it
converts to. Verified against the published tarballs rather than the registry
metadata: 3.0.0 has no `onNativeChange` anywhere in its types and its `Input`
still defaults to `md`; 4.0.0 has both.

This is not a cosmetic version bump. With 3.0.0 resolved, a converted
`onChange={(checked) => ...}` receives an Event, an Event is always truthy, and
the toggle latches on and never releases. It typechecks, because 3.0.0 takes
`onChange` from `InputHTMLAttributes` where a one-argument callback is
assignable, so nothing catches it.
The lockfile still pinned 3.0.0 from the install made before the version was
corrected. Re-resolved and typechecked: `tsc --noEmit` is clean against 4.0.0,
where against 3.0.0 it reported the converted handler as the error it should
have been.
4.0.0 was published in error and has been unpublished from the registry, so a
`^4.0.0` range now resolves to nothing at all. The value-change contract these
conversions target ships as 3.1.0.

The source is unchanged: it was written against this API either way. Only the
number the range asks for is different.
`typeof window === "undefined"` guarded the `localStorage` access, and that is
not the same question. A renderer can have a window and no storage API, in
which case the guard passes and the next line throws a `ReferenceError` out of
module evaluation, before a single component renders. The page is blank and the
log has one line naming the entry chunk.

Observed under the QA inspection host, but it is not specific to it: a private
window with site data blocked, or a browser where storage is blocked by policy,
throws on the same line. `typeof localStorage` is itself inside the `try`,
because reading the binding is what throws in the policy case.

The theme still applies for the session when it cannot be remembered. Losing
the preference is a smaller failure than losing the page.

Also locks `@pathscale/ui` at the published 3.1.0.
A `localStorage` read outside a try killed the whole page where storage is
unavailable: a private window, a browser with site data blocked, an embedded
webview. The read happens during startup, so it threw before anything
rendered and the symptom was a blank page.

"The page renders at all" is therefore the guard, and it is not a trivial one:
it is the assertion the defect failed. The rest of the group is the shape of
the page around it, so a partial render cannot pass as a whole one.
@pathscale

Copy link
Copy Markdown
Owner Author

Checks added, and wired into CI.

This site now has ps-qa checks under tests/ps-qa/, driven against its built dist through chuzz-headless — the browser with no window, loading through the same engine and the same web-API shim a tab uses. .github/workflows/qa.yml runs them on every pull request. The hand-written verification table in this description is no longer the only thing standing between a regression and a release.

Every check in this repository passes locally against the built site. What each one does not cover is written in its own file rather than implied by its absence.

Depends on three engine changes, all open:

meh added 10 commits September 8, 2026 19:08
Every check here only asked whether the page painted, and a page that
paints can still be dead. Solid 2 halts the reactive system permanently
when an error escapes every boundary: the page renders once and then
answers nothing, with no visible difference. Two sites in this fleet
were shipping in exactly that state and every render-only check passed
over them.

The theme control is the cheapest detector there is. Its own label is
derived from state, so pressing it and reading the label back asks
whether state still moves, without knowing what the page is for.
The group asked whether the landing page rendered and whether the theme
toggle flips. Nothing pressed a navigation control, so the specification and
the syntax reference were never reached. The router sends every unmatched
path to the home page, which is exactly the arrangement that hides a broken
link: a route that no longer resolves looks like a landing page, not an error.

Five checks: both documents are reached and shown to have a body rather than a
heading over an empty shell, the header's own name goes back, and the hero
button is driven separately from the navigation link beside it because it is a
Button rather than an anchor the router owns.

They live in the existing group rather than a new one. A group runs in
sequence against one host and the first check absorbs the bundle boot; as
their own group these spent fourteen seconds looking for a navigation control
that had not been drawn yet, and reported it missing.

The profile gains its surfaces, so `inventory` has pages to walk, and the
links that leave for github.com are declared exceptions. One of them was worth
catching: a check that pressed "Read the transport profile" and asserted the
landing heading was gone passed while sitting on github.com, which is a check
passing for the wrong reason.
A recorded version keeps satisfying its caret range, so nothing is ever
reconsidered and no build reports that it is behind. --locked and
--frozen-lockfile mean nothing without a committed lock, and the bun form
fails the job outright.
solid-js 2.0.0-rc.4 depends on `@solidjs/signals: ^2.0.0-rc.4`, and a later
rc satisfies that caret while dropping exports rc.4 re-exports, so the build
fails with `export 'patchableRaw' was not found in '@solidjs/signals'`. The
removed lockfile had been holding rc.4 in place.
`on_surface` answers "are we already here" by looking for a visible node with
a non-zero box whose name contains the marker. The composite action that
supplies the browser builds it with `--no-default-features`, which drops
`system-fonts` along with the window, and on that host a heading is laid out
with a height of zero. Both document markers were headings, so `on_surface`
could never be true in CI: the navigation step would press the opener, never
see itself arrive, and report a control that was already on screen as missing.

The new markers are nodes whose box comes from padding -- the lens tab strip
for home, and each document's first sidebar entry, which are anchors with
`padding: 3px 0` and measure 243x6 with no font at all. The two documents'
first sections differ, so the same marker cannot answer for both pages.

Also records what the harness cannot reach here and why: `Download PDF` joins
the manual controls, since it leaves the site for a PDF and both document
pages draw it under the same label, and `Read the full specification` leaves,
since it is an ordinary route to /spec that the checks now press. `/vignette`
and `/study` are routed and deliberately unlinked, and ps-qa has no way to
open a URL, so they are named as uncovered rather than quietly missing.
`page` was one group asking eleven questions about four unrelated things. It
becomes two, and the split is the point: `chrome` never leaves the landing
page and `navigation` is the only group that does.

That is a constraint of the site rather than a preference. The one control
that returns to the landing page is the header logo, and it is a `<span>` of
text, so on the fontless host CI builds it has no box and cannot be pressed.
Home is reachable only where the application already is -- at the start of a
run -- so every other group stays there and `navigation` sorts last among the
files that share the home bucket.

`chrome` covers the header's four controls, the footer, the theme toggle and
its size, and a contrast audit of every link. `navigation` walks home to the
specification by the hero button, the specification to the syntax reference
and back by the header, and carries the theme across a route change, which on
this site is a whole document load: written on one document and read back on
the next is the honest end-to-end test of the storage whose unguarded read
once killed the page.

The contrast audit runs in dark only, and that is a finding rather than a
choice. In light, six named nodes fall under the 4.5:1 floor: the four footer
links and the inline TOON link at `text-base-content/55` and `/60`, and the
receipt heading in `text-warning` over `bg-warning/5` at 3.39:1. Those are
token decisions, so the check names them in a comment instead of pinning them.
…itcher

The landing page makes its argument across six sections and an audience
switcher, and a partial render passes "the heading is drawn" trivially. `home`
names every section, both hero actions, the receipt block, both halves of the
before-and-after, and the three buttons the later sections end in, then reads
the authored order off the boxes the renderer produced.

Order is asserted between controls rather than between headings on purpose. A
heading is as tall as its shaped glyphs, and the CI host has no font stack, so
a heading there is laid out with a height of zero and `Above` has no painted
box to compare. The buttons each section ends in have padding and measure the
same either way.

`lenses` is the only stateful control the public site has. Each switch is
proved twice: `SelectionChanges` follows the exact tab that was pressed, and
the panels are asked about as a family -- six mounted, exactly one painted,
that one under the strip, and the six tabs at six distinct positions inside
it.

Which panel is showing is not asserted, and cannot be from here. Everything
inside a panel is either anonymous, as the list and its items are, or is the
panel heading, which has no box on a host with no fonts. The tab strip is
addressable only because the component library supplies `data-slot`; this site
supplies no identifiers of its own, and one on each panel would close the gap.
These two pages are what the site exists for, and they reach the page by a
route nothing else uses: `src/content/*.html` is imported with `?raw`, which
is a build rule rather than a language feature. Lose the `resourceQuery` rule
in rsbuild.config.ts and the import still resolves, still typechecks and still
builds -- it yields a URL string, and the page renders a path where the
document should be. Only driving the built bundle sees that.

So the assertions are about sections deep inside each document. Sixteen for
the specification and thirteen for the reference, one check each, because a
conversion that drops a section in the middle keeps the opening and the ending
and a first-and-last pair cannot see it. `covers` would have been shorter and
would have been a lie: the harness uses it to credit a repeated family in
`inventory`, not to assert anything.

The sidebar gets its own attention because it is not authored. A
`createEffect` walks `h2[id], h3[id]` out of the rendered article and builds
the entries from what it finds; Solid 2 takes two functions there, and an
effect written the old way throws at run time while typechecking clean. A
sidebar that exists and is empty is what that looks like. The checks ask for
its first entry, its last, one in the middle, a second-level one, and that the
entries are in document order.

The entries are then pressed. Where the document ends up is not assertable --
`Paints` asks for a box, not for a viewport -- but what a press must not do is
throw, and an error escaping the handler halts Solid 2's reactive system
permanently while the page carries on painting. So after the presses the theme
toggle is pressed, because its own label is derived from state. If the sidebar
killed the application, that is where it shows.

The reference group leads with the absence of the specification's first
section: both pages are the same component with a different prop, and a wiring
mistake there produces a page that passes every check written about the other.
@pathscale pathscale changed the title feat(ui): migrate to @pathscale/ui 3.1.0 feat(ui): migrate PromptSyntax to UI 3.2 Sep 11, 2026
@pathscale
pathscale merged commit bbc565f into master Sep 13, 2026
4 checks passed
@pathscale
pathscale deleted the feat/ui-3-migration branch September 13, 2026 01:59
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