Skip to content

Migrate to UI 3 and verify all showcase routes with native QA - #53

Merged
pathscale merged 32 commits into
masterfrom
feat/ui-3-migration
Sep 11, 2026
Merged

Migrate to UI 3 and verify all showcase routes with native QA#53
pathscale merged 32 commits into
masterfrom
feat/ui-3-migration

Conversation

@pathscale

@pathscale pathscale commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Migrate the showcase to the published @pathscale/ui 3.2 release and verify the built application through ps-qa's font-enabled native host.

The theme editor now uses UI controls for radius, size, effect, and export options. Every control has a usable name, keyboard behavior is covered, the generated-CSS dialog uses the supported Dialog structure and can be closed, and in-page section links work correctly under the hash router. Documentation and version claims now describe the UI 3.x stack.

CI runs each of the 12 self-contained groups in a fresh native host so retained route, scroll, and pointer state cannot leak between groups. This still drives every declared rendered outcome.

Local release validation on the committed registry build:

  • clean dependency install with @pathscale/ui 3.2.0
  • TypeScript check passes
  • production build passes
  • 330/330 native end-to-end checks pass across actions, calendar, data display, docs, feedback, input, layout, navigation, showcase, surfaces, theming, and unlisted routes

The previous red run failed while building the older chuzz checkout before any js.software checks ran. The current workflow consumes the repaired chuzz action.

The owner reviews this PR before deployment.

meh added 7 commits September 8, 2026 01:11
4.0.0, not 3.0.0. 3.0.0 predates the value-change callback contract and
still defaults Input to md, so a converted onChange={(checked) => ...}
would receive an Event there. An Event is always truthy, which latches
every toggle on permanently, and it typechecks, so nothing catches it.

The resolved peer set is the corroboration: no @tanstack/solid-form and
no @tanstack/solid-table.
solid-layouts ships one build arm per Solid major and its bare entry is
the 1.9 one, whose renderer calls splitProps. Solid 2 replaced that with
omit, and the bundler links both arms of the package's runtime check, so
the default entry fails at link time on a missing createComponent export.

The site documented these aliases on its own installation page without
applying them to its own config. Matches support.cafe, which is the
known-good reference.
Switch, Checkbox and Radio report their new value from onChange rather
than the native Event, and each grows a controlled demo so the contract
is visible rather than only described. The event is still reachable as
onNativeChange, which is where preventDefault belongs. ThemeEditor's five
switches are the real call sites and move with them.

Also:

- Input, Select and Textarea documented a width prop taking the Width
  vocabulary. They take fullWidth, a boolean; width="full" is a Button
  prop and is silently dropped on a field.
- Button's flavor row advertised tertiary, which the recipe does not
  style. Flavor is an open union, so it typechecks and renders unstyled.
  Shown as accent, which is the flavor that actually fills that slot.
- The state axis replaces isDisabled, isLoading and isInvalid, which
  could previously disagree with each other.
- className is gone from UIBaseProps; the props tables say so once
  instead of listing class twice.
The conventions section still taught the 2.x HeroUI surface: is* props,
a colour list ending in error|ghost, and "both class and className are
accepted everywhere". None of that is true. It now describes the state
axis, the flavor axis and its openness, variant as the separate shape
axis, and class alone.

The forms section claimed the engine is TanStack Form and offered
form._tsForm as an escape hatch, while the peer list three sections
earlier already said TanStack was gone. The engine is the library's own;
_tsForm went with it, so the FormApi surface is documented instead.
FormSubmitButton disables on isValid(), not canSubmit.

The table section taught useTableModel, useTableSorting and four more
hooks that no longer ship. Table is the presentational compound now, and
the model moved to createDataGrid. useAnchoredOverlayPosition is the only
survivor of that group.
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.
The range was repinned without re-resolving, so the lockfile still named a
version that is not on the registry. `bun install` here is the resolution.
This site is the library's kitchen sink, so what these prove is that the
published `@pathscale/ui` renders in a real engine at all. A version that
fails to boot takes the whole showcase with it, and the symptom is a blank
page rather than an error anyone reads.

The heading checks are `Present` rather than `Paints` on purpose. Text is
exactly as wide as its line and as tall as its shaped glyphs, so on a host
with no font catalogue -- which a Linux runner is -- it lays out flat and a
box assertion fails for a reason that says nothing about the site.
@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 22 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.

The subject is a category rather than the trigger, because the trigger
is there either way. `Data display` and `Navigation` exist only once
the menu has opened.
The group asked whether the landing page rendered and whether the component
menu opens. No navigation control was pressed, so the four pages this site
publishes -- the theme editor, the documentation, the layouts page and the
showcases -- were never reached. A route that stopped resolving would have
looked exactly like a landing page.

Five checks, each naming its destination by a heading that page owns. The
header's names are on every page and prove nothing about having arrived
anywhere, which is also why the profile's markers are headings rather than the
navigation labels.

The profile gains its surfaces, so `inventory` has pages to walk instead of
reporting a site with no controls at all.
A recorded version keeps satisfying its caret range, so nothing is ever
reconsidered and no build reports that it is behind. --frozen-lockfile means
nothing without a committed lock and 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.
The showcase group proves each of these is reachable from the header. It
does not ask what is on them, and a page that routes correctly and renders
an empty article passes every navigation check there is.

Two of the four are in no navigation bar. The installation guide is reached
by the header's Get Started call to action and the usage cheatsheet only by
a link inside another page, so both are covered the way a person arrives at
them rather than by naming a URL.
The theme editor is the only page here that is an application rather than
a document: the left column writes custom properties and the right column
is a live preview built from the components those properties style. It is
also the only place on the site where a control changes anything, so it is
where a halted reactive system shows up first.

The colour popover is opened and closed and no colour is committed, so the
group is re-runnable.
Button, Dropdown, Menu, Dialog and Switch, reached the way a person reaches
them: the component pages are behind a two-press disclosure that the router
clears whenever the route becomes /, /docs or /showcases, so every component
group lands on a clearing route, opens the menu, then selects its category.

The reset page is the documentation index rather than the showcase index.
/showcases names every card after its component plus its description, so
Button, Actions and Menu all match something there and the preamble would
pass on the wrong node.
Avatar, Badge, Card, Chat Bubble, Data Grid, Empty, Kbd and Table.

The Data Grid is why this group is worth more than a page walk. It is the
only component on the site that owns state, so its pager can be driven and
read back rather than counted: paging forward is what unlocks paging back,
and the group pages back before it leaves.
Alert, Progress, Radial Progress, Skeleton, Spinner, Toast and Tooltip.

These components exist to tell a person something, so most of what is
checked is their roles rather than their boxes. status and alert decide
whether a screen reader announces politely or interrupts, and the Alert
page is the one place both are rendered side by side.
Checkbox, File Input, Form, Input, Radio Group, Select, Slider and Textarea,
plus the two pages in this category the suite reaches but never opens.

/calendar dies on 'Intl is not defined' and /color-picker on a ColorWheelFlower
rendered outside its provider. Both errors escape every boundary, Solid 2
halts its reactive system for good when that happens, and every group runs
against one shared browser, so opening either would end the run and report
every later group as a failure of its own. The check file's header says so
in full.
Accordion, Collapsible, Drawer, Flex, Grid, Join and Separator. A layout
component has no state and no label, so this group asks where things are
rather than what they are called: a joined group has to sit in a row and
share a centre line, and a separator has to span its container.

Drawer runs last because what it opens cannot be closed. It offers no close
control, its trigger does not toggle, and nothing inside accepts keyboard
focus so Escape cannot be delivered. The last check is that navigating away
still works, which is the question that matters.
Breadcrumb, Dock, Link, Navbar, Pagination and Tabs. These are the
components whose accessible structure carries the most weight, so the
assertions are about roles: a breadcrumb is a named navigation landmark,
a dock is a toolbar, a tab strip is a tablist whose panel is replaced
rather than restyled when a tab is chosen.
Glass Panel, Glow Card and Noise Background are backgrounds with no roles
and nothing to press, so what is asserted is that each parameter section
produced its own example.

Complete Coverage is the opposite and is why this group is worth running:
one page carrying every public family that has no showcase of its own, so
a family that stops building leaves a hole here and nowhere else.
Fieldset, Fieldset Actions, Footer, Icon, Radio and Video Preview are
routed and linked from the showcase index, which lists every route rather
than only the ones navigationData.ts knows about. This group reaches them
the way a person does: through the index, back to the index, on to the
next. Navigating straight to a URL would assert that a page renders while
saying nothing about whether anyone can get to it.

If a route is added and this group starts failing, the route table and the
component menu have gone out of step, which is worth being told.
The existing checks in this group are all about the header, which is the
same on every route. These are the four calls to action the page exists to
offer, the two rows of cards below them, and the fact that the primary one
goes where it says.

Every check here now names Home as its opener. The driver buckets checks by
the surface each one opens, computed across every check file at once, and a
check that names none inherits whatever the file before it left behind: with
more than one file present these six were being ordered after the theme
editor and asserting the landing page against it.
Eleven groups rather than one, listed explicitly rather than globbed so the
workflow says what it covers. One invocation per group is load-bearing and
not only for readable logs: the driver orders the checks it selects by which
surface each opens, across all files at once, so selecting everything
interleaves the groups instead of running each as written.
Every toast on the page removes itself after four seconds. One check
counted the whole status family and took its baseline from the toast the
previous check had raised, so on a slower run that toast had already
expired, the count went 1 to 0, and a working component was reported
broken by a timer.

Each toast check now names the toast its own press produced and carries a
deadline shorter than the toast's life.
The limited-toast example picks its severity at random from four values,
and two of them are severe enough that the toast takes the alert role
rather than status. A role-qualified subject therefore passed or failed on
a coin toss. Naming the message without a role is the assertion that was
meant, and it held over eight consecutive runs.
The page cannot be opened today: `useCalendarState` builds three
`Intl.DateTimeFormat` instances during the first render, the engine has
no `Intl` by policy, and Solid 2 answers the escaped ReferenceError by
halting its reactive system for good.

So these checks fail, on purpose, and say what the fix has to produce
rather than merely that it stopped throwing. A calendar rendering every
cell as `Invalid Date` throws nothing, and a shim answering
`String(value)` renders `Sun Jun 15 2025 12:00:00 GMT+0700` where the
page meant `Sunday, June 15, 2025`. Both are rejected: every assertion
names the exact string a correct en-US `Intl` produces, on the one
calendar of the five whose dates are pinned rather than today's.

Its own group and its own file, because every group shares one browser
and a page that halts the engine ends the run where it halts. Not added
to the workflow's group list; `input.ron` still records the page as
reached and not opened, and should keep doing so until this is green.

Measured: 4 of 14 pass. Two of the three formatters cannot be asserted
at all, because a `columnheader` and a bare `div` report no accessible
name and `aria-labelledby` is not resolved. Only the day button's
`aria-label` is reachable, which is the strongest of the three to be
left with. The header says what a library change would unlock.
Every one of the five markers was a heading, and `on_surface` accepts a
marker only when the node is onscreen, which means a box with width and
height. A host built without `system-fonts` -- which is what CI runs, and
what any Linux runner is -- shapes text to no glyphs, so every heading on
the site lays out at zero height and no surface is ever recognised as
being in front.

The symptom is not one heading check failing. Arrival is what the marker
decides, so every check in the suite failed at `open`: 47 of 302 passed
against a fontless host, and the first message was "could not open Home"
on a page whose Home link is right there, visible and enabled.

Each marker is now a padded control instead. A link or a button with
padding has a box whether or not its label shapes, and the five chosen
here measure 38x44, 148x36, 48x26, 26x36 and 26x36 with no font at all.
`open` navigates and then confirms the arrival before the check is
judged. A component page is not one of the surfaces in `ps-qa.ron`, so
the harness has no marker for it and confirms arrival by waiting for the
check's own subject to paint. Every one of those subjects is the page's
heading, and a heading has no height on a host with no font catalogue.

So the arrival timed out on all 51 of them, the harness fell back to its
recovery path, pressed the home opener, and left the group on the
landing page with the component menu closed. The first page of a group
failed and took every later check in that group with it: the seven
category groups scored between 3 and 6 out of 26 to 40.

`click` presses the same menu entry and skips the arrival gate, which is
the only part that could not work. Subjects and expectations are
unchanged, and they are what proves the page is in front: `heading:Button`
exists on the Button page and nowhere else. The menu entry is a padded
control, so the press itself was never in doubt.
`Paints`, `Enabled` and every geometric expectation filter to nodes with
a non-zero box. A control whose whole size is its label has no such box
on a host with no font catalogue, so eleven checks failed while saying
nothing about the site: a bare inline anchor, a file input's own label,
a disclosure summary, a toolbar whose height is its contents'.

Where the claim survives the change it is made with `Present`, which
still proves the node exists with the right role and the right
accessible name. Three checks had to give something up, and each says so
beside itself:

  - `button:Browse` loses its enabled state; nothing in the vocabulary
    reads that without geometry.
  - the Glow Card grid no longer asserts that its cards sit side by side.
    Nothing on that page paints without fonts, so no expectation can
    read the offsets that are there.
  - the breadcrumb asserts distinct positions rather than left-to-right
    order, because the page draws the same trail three times and the
    crumb names repeat across all of them.

`link:GitHub Discussions` was replaced rather than weakened: an anchor
inside a sentence is not laid out at all without fonts, so its `bounds`
is `None` and even `Present` rejects it. The closing call to action a few
lines below it is two padded links, and that is what is asserted now.

The landing page's feature card keeps its `Measures` assertion with a
40px height floor instead of 100px. 100 was the wrapped text; 40 is the
padding, and a card that shrank onto its heading is still 0.
meh added 2 commits September 9, 2026 20:08
The QA host is built without `system-fonts`, so text shapes to no glyphs
and a control whose whole size is its label lays out at zero height. It
is in the tree, enabled, with a position, and it is rejected by the
geometry gate that every coordinate-driven press needs.

`PS_QA_HEADLESS` targets those controls by node id instead, which is what
the protocol's `Click` takes anyway. One check needs it: the Collapsible
toggle is pressed twice, and its trigger is a disclosure summary with no
padding of its own.

It buys the press and nothing else. It does not make a visual assertion
meaningful here, which is why the checks say `Present` wherever the
subject is text-shaped rather than a padded control.
@pathscale pathscale changed the title feat(ui): migrate the showcase to @pathscale/ui 3.1.0 Migrate to UI 3 and verify all showcase routes with native QA Sep 11, 2026
@pathscale
pathscale merged commit c59f291 into master Sep 11, 2026
1 check passed
@pathscale
pathscale deleted the feat/ui-3-migration branch September 11, 2026 21:34
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