feat(ctx): replace alert() toast with a styled Datastar toast (#41)#54
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #41.
Problem
ctx.Toast(msg)emittedwindow.alert(msg)— blocking, unstyled, andnot 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 shippedalert()to production or built its own toast and abandonedctx.Toast.Fix
ctx.Toastnow emits a small, self-contained, styled toast: a fixedbottom-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-rootcontainer (keyed by element id), latertoasts reuse them.
Deliberately in-scope-only, per the issue:
ctx.Toast's signature and every call site areunchanged — this is a rendering change behind the existing surface.
ExecScriptscript-frame path the old
alert()used, so there is no documentchange (no
AppendToHead/foot injection) and no CSP postureshift versus the alert it replaces.
picocss plugin.
Safety
The message is rendered via
el.textContent(never as HTML) andJSON-encoded into the snippet, so it can neither break out of the JS
string literal nor inject markup. Go's
json.MarshalHTML-escaping alsoneutralises 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-rootmarker) before implementing:TestCtx_Toast_rendersStyledToastNotAlert— toast marker + messagereach the wire;
alert(never does.TestCtx_Toast_injectsMessageAsJSStringLiteral— quotes/newline/backslash arrive JSON-escaped (JS-string-literal safe).
TestCtx_Toast_escapesHTMLMarkupInMessage— HTML markup arrivesescaped, not as live DOM.
TestAction_defaultErrorPathToastsTheBrowser/TestAction_defaultPanicToastHidesInternalMessage— the default errorand panic surfaces are styled toasts; the internal panic message stays
hidden.
Full suite green under
go test -race ./...(and-count=20stress onthe toast tests). gofmt / go vet / golangci-lint clean. Emitted JS
validated with
node --check. Visually verified in a browser: darkslate toasts stack bottom-right, the long message wraps within the
max-width cap, and the container empties after the auto-dismiss.