8 Commits
Author SHA1 Message Date
Tommy D. Rossi 1032e257c0 feat: examples use Button and Dialog; popups draw no focus ring
- examples/chat.tsx and examples/solid/chat.tsx: every div + onClick
  control (IconButton, SidebarAction, ConversationRow, thread filter,
  WorkedFor, ModeToggle, GhostButton, search hits, cycle-overlay) is a
  Button, so the chat is fully keyboard reachable. OverlayCard is a Dialog:
  Escape, backdrop press, Tab trap, and focus in (search input via
  initialFocus) and back out to the opener. Overlays stay mounted with
  open={...} instead of conditional rendering.
- example-app/app.tsx: IconButton, SidebarRow, Checkbox, Add are Buttons;
  Add is disabled while the draft is empty.
- Select.Content and Dialog.Popup set focusVisible: {} so the default ring
  does not outline the whole menu or card; they are focused only to take
  keys.
- @gpuix/solid exports the HostElement type for refs.
- chat.test.tsx: Escape closes search and focus returns to its button.

Solid chat port done by a subagent; verified with chat.automation.ts
(6/6) and a throwaway test of open, Escape, close, and focus return.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:10:10 +02:00
Tommy D. Rossi 53b3a8920f docs: keep automated windows in background
Make every native example honor GPUIX_BACKGROUND and make every current real-window automation example pass that flag. Human launches still focus normally, while agent-driven launches avoid taking the user keyboard on macOS and Windows.

Explain why live input, clicks, and screenshots do not require activation. Correct stale guidance that claimed fill and press were unavailable, and document that Linux currently ignores the focus option.

Validated with the examples and example-app test suites, type checking, lintcn, a live screenshot, and a macOS foreground-process check.

Session: ses_fb208083cffewN8NfeygDAdslz
2026-08-29 16:59:30 +02:00
Tommy D. Rossi 6e75327cdc Add the GPUIX project scaffolder
Publish a new @gpuix/cli package with a goke-powered `gpuix new` command. The command downloads the official example app, replaces its workspace React dependency with the latest npm release, and installs the generated project with Bun.

Update the quickstart, example documentation, workspace metadata, release notes, and CI publish order. Add a real GitHub-backed integration test and shell completion support.

Session: ses_fb87b7e28fferSvDT71z3OadR1
2026-08-28 10:51:43 +02:00
Tommy D. Rossi 94725742c9 example-app: assert what every sidebar tab paints
The suite covered adding, deleting and starring a row, but nothing
proved a sidebar tab actually changes the screen. A view filter that
silently returned the same rows, or a header that kept the previous
title and count, would have passed.

The new case clicks Inbox, Starred, Done and Today and snapshots the
whole painted screen for each, so the header title, the header count
and the row set are all locked. It also returns to the tab the app
opened on and asserts the screen matches the first Today capture, so a
tab cannot be one-way.

Session: ses_fc0b7cd62ffe3aBw0pyYBOBDjf
2026-08-26 20:16:34 +02:00
Tommy D. Rossi 6c5b0b564f Centre the todo empty state and stop drawing the star twice
`EmptyState` was the single child of the `<virtual-list>`, so it could only
pad itself down from the top: a list row is sized to its content and never
fills the viewport. It is now rendered in place of the list, as a
`flexGrow: 1` box that centres on both axes.

    {visible.length === 0 ? (
      <EmptyState view={view} />
    ) : (
      <virtual-list estimatedItemHeight={48}>…</virtual-list>
    )}

A starred row also painted a static star next to the hover strip, which
already carries a star button tinted with the accent. The static marker now
steps aside while the row is hovered.

Verified with live-app screenshots through the automation client. The five
`example-app` tests and `tsc` still pass.

Session: ses_fc149768bffeA9Ig3eeIhN2APN
2026-08-26 17:54:39 +02:00
Tommy D. Rossi 691ca1fe26 Test the todo starter through the automation client
`example-app` had a `screenshot.ts` and nothing that asserted behaviour.
`app.test.tsx` mounts `TodoApp` on the GPU test renderer and drives it
with the same locator API, in-process, with no child window:

    const { render, renderer } = createTestRoot({ width: 940, height: 660 })
    render(<TodoApp />)
    const app = await connectTest(renderer)

    await app.getByTestId('view-inbox').click()
    await app.getByTestId('row-t5').hover()
    await app.getByTestId('delete-t5').click()

Five tests: the active view paints every row, delete removes the hovered
row, the composer prepends, a completed task moves between views, and a
task added past the viewport height stays on screen.

**`app.tsx` changes**

`render()` now sits behind an entry-point check, the same one `chat.tsx`
uses, so importing the file does not open a window.

Rows carry `row-<id>`, `toggle-<id>`, `star-<id>` and `delete-<id>`. The
trash button only exists while the row is hovered, so a test must
`hover()` before it can `click()`.

**Why `getPaintedText()` and not `getAllText()`**

Both would work here, since every row is a `<text>`. `getPaintedText()`
is the one that proves the row is actually on screen, which is the whole
point of the fifth test.

Session: ses_fc16226ffffeOn4QTVIbw4K7Nz
2026-08-26 17:31:00 +02:00
Tommy D. Rossi 3d759b21c3 Drop the VirtualList wrapper, and fix the starter after review
Oracle review of 7d45d36 plus a bug the review turned up.

**Remove `VirtualList`**

The windowed wrapper is gone from `@gpuix/react`. Windowing on the React side
is application state: pass `itemCount`, `estimatedItemHeight` and `windowStart`
to the host `<virtual-list>` and render only that slice.

A generic wrapper cannot know when to widen its own window. It moved `range`
only when the native list reported a new visible range, so growing `itemCount`
without a scroll left the extra rows unmounted forever. A filter does exactly
that. The todo example hit it immediately: switching from a 2-row view to a
6-row view painted 2 rows.

    view-done    ok
    view-starred ok
    view-inbox   FAILED: "Ship it" never mounted
    view-today   ok

`VirtualList` was never published, so the pending
`.changeset/windowed-virtual-list.md` now describes the native `itemCount` plus
`windowStart` path instead. README and AGENTS.md carry a worked example and the
rule that no wrapper should come back.

**Review fixes**

`web:build` ran `bun run --cwd ../packages/native build:web`, a path that does
not exist once the folder is copied, while the README claimed nothing depended
on the repository. The script is gone. The browser renderer ships inside
`@gpuix/native`; `web.ts` explains the repository-only build in its failure
message.

`example-app/README.md` claimed `<virtual-list>` builds only visible rows. GPUI
builds only visible native rows, but React still mounts every child. Corrected.

`noImplicitAny: false` is dropped from the Quickstart tsconfig and from
`example-app/tsconfig.json`. The example passes under full `strict`, so the
starter should not hand out weaker checking.

The lockfile finding resolved itself: `examples/package.json` on main now
declares `cbor-x` and `msgpackr`, and `bun install` in a clean checkout of HEAD
produces no diff.

**Verified**

`packages/react` 302 tests pass. `example-app` typechecks under full strict,
lints clean, and all four filter views paint through the automation client.

Session: ses_fc1c928b0ffeeHIfdRpxtE0QaV
2026-08-26 16:46:11 +02:00
Tommy D. Rossi 7d45d3681a Make GPUIX installable: quickstart, todo example app, and a key that typechecks
Installing the published packages and writing an app was not possible by
following the README. Three things blocked it.

**1. `key` failed on every GPUIX element**

    error TS2322: Type '{ key: string; ... }' is not assignable to type 'Props'.
      Property 'key' does not exist on type 'Props'.

Any list built with `.map()` broke on the first `tsc` run. `key` was expected
to arrive through `JSX.IntrinsicAttributes`, but TypeScript 5 ignores that
member for intrinsic elements. React's DOM types work only because
`DetailedHTMLProps` already carries `key`.

`key` now sits on `Props`, next to `ref`, and on `VirtualListShared`, the one
element prop type that does not extend `Props`. Verified with a probe over all
12 intrinsic elements plus `motion.div`, `VirtualList`, Select, Combobox and
Tooltip.

`jsx-dev-runtime.d.ts` also re-exported `jsx` and `jsxs` from
`react/jsx-dev-runtime`, which exports only `jsxDEV`. The declarations now
match `jsx-dev-runtime.js`.

Nothing caught this because Bun never typechecks and the build tsconfig
excludes `src/__tests__`. CI now runs `bun run typecheck` in `example-app`,
whose `app.tsx` maps a keyed list inside a `<virtual-list>`.

**2. A hello world installed 254 MB**

`@gpuix/native` packed all six platform binaries into the main tarball through
a `*.node` glob, on top of the six per-platform packages that
`optionalDependencies` already resolves.

    node_modules                       254M
    |- @gpuix/native                   185M  <- all six, never loaded
    |- @gpuix/native-darwin-arm64       23M  <- the one that loads
    |- @gpuix/react                    544K

The glob is gone. `index.js` still resolves the platform package at runtime.

**3. The README never said how to install**

It opened with the architecture and the mutation protocol. There was no
`bun add` anywhere, and no mention of `jsxImportSource`, which is required:
without it TypeScript uses DOM types and `<virtual-list>`, `<markdown>`,
`<code>` and `style.hover` all fail.

A **Quickstart** now sits between the hero and Examples: install, tsconfig,
entry file, `bun --hot`, `bun build --compile`. Nothing was removed. The
Building section says up front that it is for working on GPUIX itself.

**example-app/**

A todo app to copy, in one file, using the Waku palette from `chat.tsx`.

| Script | What it does |
|---|---|
| `bun run dev` | `bun --hot app.tsx` |
| `bun run build` | standalone binary in `dist/todo` |
| `bun run web:build` | browser renderer, needs nightly Rust |
| `bun run web:dev` | browser bundle served with COOP/COEP |
| `bun run screenshot` | drives the app through the automation client |
| `bun run typecheck` | `tsc --noEmit` |

It shows `<virtual-list>`, a native `<input>`, `motion.div`, tinted `<svg>`,
native `hover` and `active`, and `testId` hooks. `scripts/screenshots.ts`
regenerates `docs/images/todo-app.png` from it.

Verified on all four paths: `bun --hot`, the 77 MB binary, the browser build at
`localhost:4188`, and a clean `typecheck`.

Two things worth remembering, both now in comments:

- the browser build embeds only IBM Plex Sans and Lilex, so `Helvetica` falls
  back to a monospace and the family has to differ per target
- `maxWidth` on a `<virtual-list>` row does nothing; the list lays each row out
  at list width, so the column cap goes one level in

`bun.lock` carries a few entries from unrelated in-flight work in `examples/`.
They are inert and prune on the next install.

Session: ses_fc1c928b0ffeeHIfdRpxtE0QaV
2026-08-26 15:55:10 +02:00