Skip to content

Add poster-board to the experiments realm (CS-12213)#5554

Open
burieberry wants to merge 2 commits into
mainfrom
cs-12213-poster-board
Open

Add poster-board to the experiments realm (CS-12213)#5554
burieberry wants to merge 2 commits into
mainfrom
cs-12213-poster-board

Conversation

@burieberry

@burieberry burieberry commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Adds packages/experiments-realm/poster-board/: a FigJam-style infinite-canvas board card — pan, cursor-anchored wheel/pinch zoom with momentum, a zoom toolbar, and layout-independent keyboard shortcuts — with live tests and a demo instance.

Closes CS-12213.

Moved from boxel-catalog

This work started as cardstack/boxel-catalog#666; we've since decided to continue development in the monorepo's experiments realm, so this PR carries that branch's final state. It includes all fixes from the code review on #666:

  • Keyboard shortcuts match the physical key (event.code Equal/Minus/Digit0) instead of event.key, which reports shifted characters and left Shift+- / Shift+0 dead; ctrl/meta/alt chords bail out so browser zoom (Ctrl/Cmd+Shift+=) is never swallowed
  • Pan sessions are primary-button only and scoped to one pointer id — right-drag no longer pans (or wedges the session when a context menu eats the pointerup), and a second touch pointer can't hijack a live drag
  • The wheel pan path normalizes deltaMode like the zoom path, so line-mode wheels (Firefox) pan at pixel-equivalent speed
  • Programmatic zoom (zoomCentered/zoomAtPoint) stops pending momentum first, so a pinch followed by the 100% button can't drift off target

The remaining review follow-ups are ticketed: rig hardening (final-frame onChange, dt clamp) on CS-12217, the finite dot grid on CS-12216. This supersedes the test copy in #5544.

What's here

  • rig.gts — pure-TS pan/zoom camera engine: cursor-anchored zoom, drag-pan sessions with velocity sampling, rAF momentum with time-normalized inertia decay
  • poster-board.gts — the PosterBoard card def: single-transform plane, dot grid, zoom HUD (+ / % / − / 100% / Fit), Shift+=//0 keyboard zoom
  • poster-board.test.gts — 3 consolidated live tests / 13 assertions: toolbar rendering + all zoom controls, keyboard shortcuts (incl. browser-zoom pass-through), and the momentum regression
  • Demo instance + README (test-running instructions)
  • packages/experiments-realm package.json/tsconfig gain qunit + qunit-dom types and the @cardstack/host/* path mapping so .test.gts files type-check in the editor (mirrors the catalog package's setup)

Testing

The experiments realm is not publicly readable locally, so the live-test loader can't discover tests against it yet (401 at _mtimes). The identical test file was verified against the catalog realm: 3 tests / 13 assertions, all passing. Template lint (the package's CI lint) passes; adding lint:js for this package is ticketed as CS-12258.

🤖 Generated with Claude Code

poster-board-initial

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Host Test Results

    1 files      1 suites   2h 38m 48s ⏱️
3 553 tests 3 538 ✅ 15 💤 0 ❌
3 572 runs  3 557 ✅ 15 💤 0 ❌

Results for commit be87b70.

Realm Server Test Results

    1 files  ±    0      1 suites  +1   16m 42s ⏱️ + 16m 42s
1 898 tests +1 898  1 898 ✅ +1 898  0 💤 ±0  0 ❌ ±0 
1 977 runs  +1 977  1 977 ✅ +1 977  0 💤 ±0  0 ❌ ±0 

Results for commit be87b70. ± Comparison against earlier commit faafe37.

@burieberry
burieberry force-pushed the cs-12213-poster-board branch from a9eb6af to faafe37 Compare July 20, 2026 21:35
burieberry and others added 2 commits July 20, 2026 17:45
FigJam-style infinite-canvas board card, step 1 of CS-12193: pan,
cursor-anchored wheel/pinch zoom with momentum (rig.gts camera engine),
zoom toolbar, and layout-independent keyboard shortcuts, with live tests
and a demo instance. Continues development in the monorepo; the
boxel-catalog PRs (#666, #667) hold the earlier history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
qunit/qunit-dom devDependencies plus the qunit-dom types entry give
assert.dom its Assert augmentation, and the @cardstack/host/* path
mapping resolves the host service registry so getService is typed —
mirroring the catalog package's setup for .test.gts files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@habdelra habdelra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖]

Reviewed with two lenses: the correctness/cleanup of the pan-zoom engine and card, and — because the file counts are dominated by it — the dependency/lockfile change riding along with the card.

Bottom line: the card and rig code are solid and I found no functional bugs. The one blocking issue is the pnpm-lock.yaml diff, which is ~3,900 lines of unrelated whole-graph re-resolution around a ~6-line intended change, and should be regenerated before merge.

What lands right

  • rig.gts is a clean, DOM-free camera engine. Momentum is time-normalized (current * Math.exp(velocity * dt) with Math.pow(decay, dt/16.67)), programmatic zoom (zoomAtPoint/zoomCentered) calls stopAll() first so stale pinch velocity can't drift the camera off target, and destroy() tears down both rAF loops and the pending timeout. The momentum-regression test pins exactly that stop-before-zoom contract.
  • The pointer-pan hardening carried over from the earlier review is all present and correct: primary-button-only, single-pointer-id sessions, pointercancel sharing the up path with a guarded releasePointerCapture, and the HUD closest() bail so toolbar clicks don't start a pan.
  • Keyboard handling matches event.code (physical key) and bails on ctrl/meta/alt, so Shift+-/0 work and browser zoom passes through — and the test asserts both the shifted-character case and the ctrl pass-through.

Coverage gap (non-blocking, already disclosed)
The live tests only register when the realm's _mtimes is publicly readable, and the experiments realm returns 401, so this file runs in neither CI nor against experiments today — it was verified only against the catalog realm. Worth confirming the plan to make the realm readable (or add a CI path filter) so this becomes real coverage rather than manual-only. The runTests export shape does match the in-repo harness (packages/host/tests/live-test.js), so once discovery works it should register cleanly.

Recommendations

  1. Regenerate pnpm-lock.yaml from main with the pinned pnpm 11.0.9, adding only qunit/qunit-dom — file-level comment on pnpm-lock.yaml. (blocking)
  2. Drop the redundant @ember/test-helpers override in pnpm-workspace.yaml — inline thread there. (part of #1)
  3. Fix the two README references (in-repo harness path; test:live working directory) — README thread. (nit)
  4. Consider scoping the window keydown listener to board focus when this graduates — poster-board.gts thread. (nit)

Nothing adjacent/out-of-scope beyond the above. Submitted as COMMENT — approve / request-changes is the maintainer's call.

Comment thread pnpm-lock.yaml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] This lockfile diff is ~200× larger than the change that motivates it, and almost none of it is intended.

What the change actually needs. The only manifest change in this PR is adding qunit and qunit-dom to packages/experiments-realm's devDependencies. Both already resolve in main's lockfile (qunit@2.26.0, qunit-dom@3.5.1), so the correct diff is ~6 lines — the two catalog: entries added to the packages/experiments-realm: importer stanza.

What's actually in the diff. 3,911 changed lines. Two dominant sources, neither related to adding two devDeps:

  • 1,824 new supports-color@8.1.1 peer-key threadsmain has zero. Keys like @babel/core@7.29.7 become @babel/core@7.29.7(supports-color@8.1.1), fanned out across the graph.
  • esbuild@0.25.12 threaded into webpack peer keys, duplicating css-loader / mini-css-extract-plugin / style-loader entries.

Those are whole-graph re-resolution artifacts. They indicate the lockfile was regenerated under different pnpm tooling than the repo pins (mise.tomlnpm:pnpm = 11.0.9). CI's pnpm install --frozen-lockfile (.github/actions/init/action.yml) only verifies the lockfile satisfies the manifests — not that it's minimal — which is why this is green despite the churn.

Why it matters. It silently reshapes transitive peer resolution for every package in the workspace, it will conflict with any concurrent lockfile-touching PR, and the next clean pnpm install on 11.0.9 is likely to churn it right back — lockfile thrash.

The fix. Regenerate from a clean main with the pinned pnpm (pnpm@11.0.9) after adding only the two devDeps, so the diff is just the packages/experiments-realm: importer entries. If there's a genuine lockfile-maintenance update worth landing, it belongs in its own PR rather than riding along with a card.

Class: regression introduced by this PR. Blocking — please don't merge the ~3,900-line re-resolution into main.

Comment thread pnpm-workspace.yaml
allowedDeprecatedVersions:
babel-eslint: 10.1.0
overrides:
"@ember/test-helpers": ^5.4.1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] This override is a no-op and can be dropped. The default catalog already pins @ember/test-helpers: ^5.4.1 (line 35), and experiments-realm already declared "@ember/test-helpers": "catalog:" before this PR — the only manifest additions here are qunit/qunit-dom. The sole resolved version is 5.4.3 on both main and this branch, with or without the override, so it constrains nothing.

Its only lockfile effect is to move @ember/test-helpers out of the catalogs: snapshot into override-governed resolution — that's the deleted catalogs: entry you can see near the top of the pnpm-lock.yaml diff. Removing this line keeps test-helpers catalog-governed like every other test dep and shrinks the diff.

Class: cleanup, non-blocking — but part of the same lockfile regeneration as the file-level comment on pnpm-lock.yaml.

Comment on lines +8 to +10

Tests live in `poster-board.test.gts` and run on the catalog live-test
infrastructure (see the catalog repo's `tests/live/README.md` for the full details): the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Two small doc-accuracy points now that this lives in the monorepo:

  • "see the catalog repo's tests/live/README.md" points out to a separate repo, but the harness that actually runs these tests is in-repo at packages/host/tests/live-test.js — it fetches _mtimes, filters *.test.gts, and calls each module's exported runTests. Point the reader there.
  • The pnpm test:live command at the bottom (line 44) resolves against packages/host — that's the only package where the test:live script is defined; there's none in experiments-realm. It reads correctly only because it sits under the cd packages/host from step 2b, so make the working directory explicit on that line to keep a reader who jumps straight to it from running it in the realm package.

Non-blocking, doc accuracy.

Comment on lines +153 to +156
handleInserted = (el: HTMLElement) => {
this.rootElement = el;
this.keydownHandler = this.handleKeyDown;
window.addEventListener('keydown', this.keydownHandler);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Confirmation plus one scoping note.

The teardown is correct. willDestroy removes this exact listener reference, and SurfaceRig.destroy()stopAll() cancels both rAF loops and clears the pending momentum timeout. Component here extends @glimmer/component, whose willDestroy fires on teardown, so nothing leaks — across app navigation or across rendering-test cases (each renderPosterBoard() gets a clean listener). Good.

The note. This is a window-level keydown listener guarded only against INPUT/TEXTAREA/contentEditable. It fires regardless of whether the board holds focus, so Shift+=//0 typed while focus is on unrelated host chrome will still zoom the board, and two isolated instances would both react to one keystroke. Fine for an experiment card; when this graduates, consider scoping to the board (a tabindex root with an element-level listener, or a focus/hover check) so the shortcut only acts when the board is the active surface. Non-blocking.

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.

2 participants