feat: add SPA navigation with view transitions

Swap page content over the existing SSE connection without full page
loads. A persistent Context resets its page-specific state (signals,
actions, intervals, subscriptions) on navigate while preserving the
SSE stream, CSRF token, and session.

- c.Navigate(path) for programmatic SPA navigation from actions
- Injected JS intercepts same-origin <a> clicks (opt out with
  data-via-no-boost) and handles popstate for back/forward
- v.Layout() wraps pages in a shared shell for DRY nav/chrome
- View Transition API integration via WithViewTransitions() on
  PatchElements and h.DataViewTransition() helper
- POST /_navigate endpoint with CSRF validation and rate limiting
- pageStopChan cancels page-level OnInterval goroutines on navigate
- Includes SPA example with layout, counter, and live clock pages
This commit is contained in:
Ryan Hamamura
2026-02-12 13:52:47 -10:00
parent 532651552a
commit 27b8540b71
6 changed files with 288 additions and 9 deletions

View File

@@ -5,7 +5,7 @@ import (
"time"
)
func newOnInterval(ctxDisposedChan chan struct{}, duration time.Duration, handler func()) func() {
func newOnInterval(ctxDisposedChan, pageStopChan chan struct{}, duration time.Duration, handler func()) func() {
localInterrupt := make(chan struct{})
var stopped atomic.Bool
@@ -16,6 +16,8 @@ func newOnInterval(ctxDisposedChan chan struct{}, duration time.Duration, handle
select {
case <-ctxDisposedChan:
return
case <-pageStopChan:
return
case <-localInterrupt:
return
case <-tkr.C: