Skip to content

feat(ctx): replace alert() toast with a styled Datastar toast (#41)#54

Merged
joaomdsg merged 2 commits into
mainfrom
fix/styled-toast-41
May 29, 2026
Merged

feat(ctx): replace alert() toast with a styled Datastar toast (#41)#54
joaomdsg merged 2 commits into
mainfrom
fix/styled-toast-41

Conversation

@joaomdsg

Copy link
Copy Markdown
Member

Closes #41.

Problem

ctx.Toast(msg) emitted window.alert(msg) — blocking, unstyled, and
not something you want in front of a user. It is also the default
surface for recovered action-handler panics (dispatchActionError
ctx.Toast("Something went wrong")), so every via app either shipped
alert() to production or built its own toast and abandoned ctx.Toast.

Fix

ctx.Toast now emits a small, self-contained, styled toast: a fixed
bottom-right overlay that slides in, stacks, and auto-dismisses
after ~4s. Zero setup — the first toast on a page injects its own
<style> and #via-toast-root container (keyed by element id), later
toasts reuse them.

Deliberately in-scope-only, per the issue:

  • No new public API. ctx.Toast's signature and every call site are
    unchanged — this is a rendering change behind the existing surface.
  • Same transport. The snippet rides the same ExecScript
    script-frame path the old alert() used, so there is no document
    change
    (no AppendToHead/foot injection) and no CSP posture
    shift
    versus the alert it replaces.
  • Neutral styling so it reads acceptably on a bare page and under the
    picocss plugin.

Safety

The message is rendered via el.textContent (never as HTML) and
JSON-encoded into the snippet, so it can neither break out of the JS
string literal nor inject markup. Go's json.Marshal HTML-escaping also
neutralises a </script> breakout of the surrounding datastar <script>
element. A test pins this with an <img src=x onerror=…> payload.

Accessibility: each toast carries role="status" / aria-live="polite".

Tests (RYGBA TDD)

Each new test was confirmed failing for the right reason (missing the
via-toast-root marker) before implementing:

  • TestCtx_Toast_rendersStyledToastNotAlert — toast marker + message
    reach the wire; alert( never does.
  • TestCtx_Toast_injectsMessageAsJSStringLiteral — quotes/newline/
    backslash arrive JSON-escaped (JS-string-literal safe).
  • TestCtx_Toast_escapesHTMLMarkupInMessage — HTML markup arrives
    escaped, not as live DOM.
  • TestAction_defaultErrorPathToastsTheBrowser /
    TestAction_defaultPanicToastHidesInternalMessage — the default error
    and panic surfaces are styled toasts; the internal panic message stays
    hidden.

Full suite green under go test -race ./... (and -count=20 stress on
the toast tests). gofmt / go vet / golangci-lint clean. Emitted JS
validated with node --check. Visually verified in a browser: dark
slate toasts stack bottom-right, the long message wraps within the
max-width cap, and the container empties after the auto-dismiss.

joaomdsg added 2 commits May 29, 2026 10:45
ctx.Toast emitted window.alert(message) — blocking, unstyled, and the
default surface for recovered action-handler panics. Replace it with a
self-contained styled toast: a fixed bottom-right overlay that slides
in, stacks, and auto-dismisses after a few seconds, with zero setup.

The snippet rides the same ExecScript script-frame path the alert used,
so there is no document change, no new public API, and no CSP posture
shift. The <style> and #via-toast-root container are created once per
page (keyed by element id) and reused. The message is rendered via
textContent and JSON-encoded into the snippet, so it can neither break
out of the JS string nor inject markup — Go's json HTML-escaping also
neutralises a </script> breakout of the surrounding datastar script.

Tests assert the toast marker + message reach the wire and that alert(
never does; an XSS case pins that HTML markup arrives escaped, not live.
…akout test

- Move role=status / aria-live=polite onto the persistent #via-toast-root
  (created empty, already in the DOM) so each appended toast reliably
  announces; a pre-populated live region is not announced by NVDA/JAWS.
- Replace the non-standard word-break:break-word with the standard
  overflow-wrap:anywhere so long unbreakable tokens (URLs, paths) wrap
  within max-width instead of overflowing on engines that ignore it.
- Add TestCtx_Toast_cannotBreakOutOfTheScriptElement, pinning that a
  literal </script> in the message can't escape the datastar <script>
  element (json HTML-escaping keeps it inside).
- Correct the HTML-escape test comment to state what it actually asserts
  (wire-level json escaping), not DOM rendering.
@joaomdsg
joaomdsg merged commit ea98d7f into main May 29, 2026
4 checks passed
@joaomdsg
joaomdsg deleted the fix/styled-toast-41 branch May 29, 2026 10:56
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.

Replace the alert()-based default error toast with a styled Datastar toast

1 participant