Skip to content

Layout widgets + theme tokens, engine perf, and a 3-layer test suite#1

Merged
AmbroseNTK merged 8 commits into
mainfrom
feat/layout-perf-and-tests
May 25, 2026
Merged

Layout widgets + theme tokens, engine perf, and a 3-layer test suite#1
AmbroseNTK merged 8 commits into
mainfrom
feat/layout-perf-and-tests

Conversation

@AmbroseNTK
Copy link
Copy Markdown
Contributor

Addresses two concerns — layout/CSS expressiveness and DOM-render performance — and adds a comprehensive test suite so future changes can't silently break products built on Gutter.

1. Layout & color tokens (less hand-written CSS)

  • New layout primitives (widgets/layout_ext.go): Expanded, Flexible, Spacer, Stack+Positioned, Grid (incl. MinColumnWidth for media-query-free responsive reflow), Wrap, Align (nine presets), AspectRatio, ConstrainedBox.
  • Color tokens (widgets/color.go): ColorPrimary, ColorInk, … "theme:*" sentinels resolved against the active theme. Raw CSS colors pass through unchanged → fully backward-compatible.
  • Container is now theme-aware (StatelessWidget, so it can read ctx.Theme): resolves Color/BorderColor tokens and gains Shadow, Overflow, Position+inset, Gap, min/max sizing, Opacity, Cursor, Transition, Flex/AlignSelf. Heading/Body/Caption/Link resolve tokens too.
  • Showcase gains a "Layout & color tokens" demo section.

2. Engine performance (minimize JS-boundary crossings)

WASM isn't automatically faster than React at DOM work — each syscall/js crossing is the real cost. These changes cut that work.

  • Batched SetState: queueMicrotask flush coalesces repeated/sibling SetStates into one rebuild pass; a mounted guard skips unmounted elements. DOM now updates on the next microtask (before paint), not synchronously.
  • Persistent per-name event listeners: one js.Func per event name dispatches to the live handler, so a rebuild that swaps the closure does zero DOM work. Replaces the release-all/recreate-all churn on every reconcile.
  • Lazy event payload + cached widget type in canUpdate.

3. Three-layer test suite + CI

  • Layer 1 — host (go test ./...): every widget's rendered *gutter.Host, token resolution, typography, Notifier, AssetURL, options, SetState-batching contract. Core 97.7% / themes 100% coverage.
  • Layer 2 — wasm runtime (element_wasm_test.go via wasmbrowsertest): the reconciler against a real DOM — mount/update/unmount, attr/style diffing, keyed reorder preserving node identity, event payload, persistent-listener handler swap, batched-SetState coalescing, dispose.
  • Layer 3 — e2e (e2e/): a deterministic testapp driven by Playwright — render, batched counter, controlled-input caret, keyed reorder value retention, conditional mount/unmount.
  • CI (.github/workflows/test.yml) runs all three on every push/PR. Setup documented in TESTING.md.

Behavioral note

SetState is no longer synchronous — the DOM updates on the next microtask (before paint). Verified safe for controlled inputs (caret preserved), 60Hz animations, and drag-drop. Documented in CLAUDE.md.

Heads-up surfaced by the e2e suite

Heading/typography widgets render a styled <span>, not semantic <h1><h6> (no ARIA roles). Noted in CLAUDE.md known limitations; not changed here.

🤖 Generated with Claude Code

AmbroseNTK and others added 8 commits May 25, 2026 23:12
These files predate the Go 1.19+ gofmt doc-comment reformatting and were
flagged by `gofmt -l`. Whitespace/comment-format only, no logic changes — so
the new CI gofmt gate starts green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d type

Reduce work crossing the syscall/js boundary — the real cost in a WASM-DOM
framework, where naive per-update churn makes it slower than React rather than
faster.

- SetState is now microtask-batched: scheduleRebuild enqueues the element and a
  single queueMicrotask flush drains the queue, deduping repeated SetStates on
  one element and coalescing across siblings into one rebuild pass. A mounted
  flag skips elements unmounted before the flush. DOM updates on the next
  microtask (before paint), no longer synchronously.
- Event listeners are persistent per event NAME: syncEvents registers one
  js.Func per name that dispatches to the live e.host.Events[name] at fire time,
  so a rebuild swapping the handler closure does zero DOM work. Only added or
  removed names touch addEventListener/removeEventListener — eliminating the
  release-all/recreate-all churn on every reconcile.
- fillEvent reads only the raw-event fields relevant to the event name (coords
  for pointer/mouse, key for keyboard, value for form/typing) instead of probing
  all six on every fire.
- canUpdate compares a reflect.Type cached on the element instead of reflecting
  on the old widget each reconcile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Let apps express richer layouts and reference theme colors by role instead of
hand-writing CSS or hard-coding hex.

- New layout primitives (widgets/layout_ext.go): Expanded, Flexible, Spacer,
  Stack+Positioned, Grid (incl. MinColumnWidth for media-query-free responsive
  reflow), Wrap, Align (nine presets), AspectRatio, ConstrainedBox.
- Color tokens (widgets/color.go): ColorPrimary, ColorInk, ... "theme:*"
  sentinel strings resolved against the active theme; raw CSS colors pass
  through unchanged, so fields stay backward-compatible.
- Container is now a theme-aware StatelessWidget (so it can read ctx.Theme):
  resolves Color/BorderColor tokens and gains Shadow, Overflow, Position+inset,
  Gap, Min/Max sizing, Opacity, Cursor, Transition, Flex/AlignSelf.
- Heading/Body/Caption/Link resolve color tokens too.
- Showcase: new "Layout & color tokens" section demoing all of the above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Guard framework quality so changes can't silently break products built on
Gutter.

- Layer 1 (host go test): every widget's rendered *gutter.Host, color-token
  resolution, typography, Notifier, AssetURL, options, and the SetState-batching
  contract. Core 97.7% / themes 100% coverage.
- Layer 2 (element_wasm_test.go via wasmbrowsertest): the reconciler against a
  real DOM — mount/update/unmount, attr/style diffing, keyed + positional
  reconcileChildren (asserting DOM node identity survives reorder), event
  payload, persistent-listener handler swap, batched-SetState coalescing,
  dispose lifecycle.
- Layer 3 (e2e/): a deterministic testapp driven by Playwright — render,
  batched counter, controlled-input caret, keyed reorder value retention,
  conditional mount/unmount.
- CI (.github/workflows/test.yml) runs all three on every push/PR.
- TESTING.md documents the layers and the wasmbrowsertest setup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update CLAUDE.md for the layout widgets, color tokens, theme-aware Container,
persistent event dispatch + batched SetState, and the three-layer test suite.
Note the Heading-renders-span accessibility gap surfaced by the e2e suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The wasm job's Chrome crashed in ZygoteHostImpl::Init — ubuntu-24.04 runners
block unprivileged user namespaces via AppArmor, and wasmbrowsertest runs
Chrome with the sandbox enabled (chromedp's defaults don't pass --no-sandbox,
and it exposes no flag to add it). Re-enable unprivileged userns via sysctl.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Silence the Node 20 deprecation warnings by moving to current majors:
checkout v6, setup-go v6, setup-node v6, upload-artifact v7, setup-chrome v2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AmbroseNTK AmbroseNTK merged commit c1b1d96 into main May 25, 2026
3 checks passed
@AmbroseNTK AmbroseNTK deleted the feat/layout-perf-and-tests branch May 25, 2026 16:28
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