Commit 1c1666a
authored
[Render Preview] [Feature] Toast (sonner port, Hotwire-native) (#389)
* [Feature] Add Toast component (sonner port) — Phlex side
Adds RubyUI::Toast{Region,Item,Title,Description,Icon,Action,Cancel,Close}
with full variant set (default/success/error/warning/info/loading), inline
lucide SVG icons, role/aria mapping, Rails flash bridge, and per-variant
hidden <template> skeletons inside a turbo-frame for client-side spawning.
No custom CSS — pure Tailwind utilities + tailwindcss-animate.
* [Feature] Add Toast Stimulus controllers + docs stub
- toast_controller: per-item lifecycle, hover-pause, swipe-to-dismiss,
Escape key, force-dismiss + restart events.
- toaster_controller: stack/expand-on-hover, MutationObserver for
Turbo Stream appends, window.RubyUI.toast JS API (success/error/
warning/info/loading/dismiss/promise), hotkey to focus region.
- toast_docs.rb stub.
- Register toast in dependencies.yml.
* [Documentation] Wire Toast into docs site
- Mount RubyUI::ToastRegion globally in application layout.
- Add /docs/toast page with usage, variants, server-pushed, JS API,
position, and Rails flash bridge examples.
- Add Docs::ToastDemoController with turbo_stream.append endpoints
(default/success/error/warning/info/with_action) for live demo.
- Register Stimulus controllers (ruby-ui--toast, ruby-ui--toaster).
- Add Toast to sidebar components list.
* [Documentation] Polish Toast docs page + devcontainer compose
- Replace onclick handlers with Stimulus toast-demo controller
(Phlex blocks unsafe inline event attrs).
- Move server-push and JS API examples to plain Codeblocks; reserve
VisualCodeExample for snippets that work under instance_eval.
- Wire docs/.devcontainer/compose.yaml to monorepo layout (mounts
ruby_ui root, working_dir=docs, ports 3001:3000).
* [Documentation] Match shadcn sonner demo style + visual polish
- Drop variant-tinted borders; icons monochrome (currentColor) — matches
shadcn sonner exactly.
- Rewrite docs page: shadcn-style "Examples > Types" grid (2-col) with
one box per variant, each containing a 'Show toast' button.
- Position section: 6-button grid; click spawns toast in chosen corner
via new `position` override in spawn detail (toaster_controller
swaps data-position before spawning).
- JS API section explains it's sugar over a window CustomEvent
(Hotwire-friendly: any source can dispatch `ruby-ui:toast`).
- Server-pushed example: fix button_to to use form-level data-turbo-stream.
- toaster_controller: register window.RubyUI.toast earlier so click
handlers don't race with controller connect.
* [Bug Fix] Mount ToastRegion in DocsLayout
DocsController uses DocsLayout, not ApplicationLayout — without this,
the Region was missing on every /docs/* page, so window.RubyUI.toast
was never registered (Toaster Stimulus controller never connected) and
Turbo Stream appends had no target.
* [Bug Fix] Move ruby-ui-toaster id from turbo-frame to <ol>
turbo_stream.append('ruby-ui-toaster') was injecting toasts INSIDE
the turbo-frame wrapper but as siblings of the <ol>. The MutationObserver
in toaster_controller watches the <ol>, so new toasts were never
tracked and never animated in.
Drop the turbo-frame wrapper (refresh:morph wasn't actually wired to
flash on navigation anyway) and put the target id on the <ol> itself.
Now Turbo Stream appends land where the controller can see them.
* [Bug Fix] Toast stack/expand — proper layout, no trembling, max enforced
Items are now position:absolute inside a relative <ol>; transforms
applied via inline style (no flex layout shift on hover).
Stack behavior matches sonner:
- Collapsed: front toast full-size; rear toasts peek 16px upward each,
scaled 0.95/0.90, opacity 0.8/0.6 — visible as cards behind.
- Expanded (hover): all toasts spread by their actual heights + gap;
full opacity + scale.
- OL min-height set to expandedHeight (when expanded) or collapsedHeight
(when collapsed) so hover hit-area is stable — no enter/leave flicker.
Region restructured: outer <div data-controller> wrapping <ol id> +
<template> skeletons. Skeletons must be descendants of controller for
Stimulus targets to resolve (this was breaking JS API spawning).
Auto-dismiss when toast count exceeds max (default 3): oldest items
get force-dismiss, exit anim runs, DOM cleans up.
* [Bug Fix] Stop tracking docs/vendor/bundle (devcontainer install path)
* [Documentation] Ignore docs/vendor/bundle
* [Bug Fix] Toast review polish: swap promise icon, drop dead code, more tests
Issues found and fixed during implementation review:
- _mutate (promise transitions) now swaps the icon SVG to match the
resolved variant, and updates aria role for error case. Previously
loading -> success kept the spinner.
- Drop unused dataset bracket-key assignment in _spawn (only the
setAttribute path was effective).
- _dismissById iterates the tracked _items list rather than raw OL
children (defensive against non-toast nodes).
- Drop unused POSITIONS constant from ToastRegion.
- Add Minitest coverage for ToastAction label/data-action,
ToastCancel label, ToastClose dismiss action + sr-only,
ToastIcon per-variant SVG content, ToastTitle/Description slots.
181 runs, 787 assertions, 0 failures.
* [Bug Fix] Anchor toast items per position; add Text Only example
Items now anchor inline to top:0 (top positions) or bottom:0 (bottom
positions) via the toaster controller's reflow. Previously every item
was hardcoded to bottom:0 in the Tailwind class list, so for top-*
positions the OL grew downward on hover, pulling the front toast out
from under the cursor and causing an enter/leave thrash.
Add a Text Only example after Promise: default variant with title
only, no description, no icon, no action — matches shadcn sonner's
Text Only demo.
* [Bug Fix] Honor close_button:true Region prop (was inert)
Region wrapper now carries data-close-button=hover|always plus a
group/toaster anchor. Toast::Close opacity rule extended:
group-data-[close-button=always]/toaster:opacity-100 — when consumer
sets close_button: true on the Region, the X is always visible
(better a11y for touch + cognitive accessibility).
Default stays close_button: false (hover/focus reveal) to match
shadcn sonner. Keyboard a11y unchanged: tab focuses item, focus shows
button, Escape dismisses.
* [Bug Fix] ToastTitle font-medium (500), match shadcn sonner
* [Bug Fix] cursor-pointer on Toast action/cancel/close buttons
* [Bug Fix] Match shadcn sonner styling tokens with Tailwind utilities
Item:
- gap-3 -> gap-1.5 (6px, sonner)
- text-[13px] (sonner font-size)
- p-4 only (close moved out of right side via translate)
- shadow-lg -> shadow-[0_4px_12px_rgba(0,0,0,0.1)] (matches sonner)
Title: drop text-sm so it inherits 13px; leading-tight -> leading-normal.
Description: drop text-sm; add font-normal leading-[1.4].
Content wrapper (skeleton): gap-1 -> gap-0.5 (2px).
Icon span: justify-start, relative, size-4, -ml-[3px] mr-1
(sonner icon margins). SVG: add -ml-px.
Action button: solid dark style — h-6 px-2 text-xs rounded
bg-foreground text-background ml-auto hover:opacity-90.
Cancel button: soft fill — bg-foreground/10 text-foreground
ml-auto hover:bg-foreground/15.
Close button: moved to top-left with -translate-x-[35%] -translate-y-[35%];
size-5 rounded-full border bg-popover; SVG size-3.
Toaster JS: spawn-time action/cancel buttons mirror new classes.
Docs: shorten Header description; move triggering blurb + sonner
credit to an About section after Installation tabs.
* [Bug Fix] Drop overflow-hidden on Toast item
Close button uses -translate-x-[35%] -translate-y-[35%] to sit
outside the top-left corner; overflow-hidden was clipping it.
* [Bug Fix] close_button prop: render only when true; X at top-right inside
- Region.skeleton: render ToastClose only when close_button: true.
- Region wrapper: data-close-button=true|false.
- Item: group-data-[close-button=true]/toaster:pr-10 reserves space.
- ToastClose: top-right inside (right-2 top-2), size-6 rounded-md,
always visible, ghost-button style. SVG bumped to size-3.5.
- Default close_button: false (no X shown). Set to true on Region
to opt in.
* [Documentation] Add close_button example + API Reference tables
- New 'Close Button' example box: clicking spawns a toast with X
visible at top-right (top of stack inside item).
- Toaster JS spawn now honors detail.closeButton: appends a top-right
ghost X button and adds pr-10 to the cloned node.
- API Reference section after About:
- Toaster (Region) props: 12 entries with default + values + desc.
- ToastItem props: 7 entries.
- JS API options: 10 entries (callable as RubyUI.toast.* or via
ruby-ui:toast CustomEvent detail).
- Reusable inline props_table helper; uses RubyUI Table primitives.
* [Documentation] Reorder Toast docs sections + Close+Action example
- About moved to top (right after Header).
- Examples second; Mount moved to AFTER Examples.
- New 'Close + Action' example after 'Close Button' (X visible plus
Undo button).
- Components table moved above API Reference (file table first, then
the props/options reference).
* [Documentation] Move About below Examples in Toast docs
* [Refactor] Toast aligned with Hotwire best practices
Critical review against the Hotwire skill references and refactored to
the more idiomatic patterns:
1. **Target callbacks instead of MutationObserver** (toaster).
Toast items now carry data-ruby-ui--toaster-target="toast". Stimulus
fires toastTargetConnected/toastTargetDisconnected automatically;
the manual MutationObserver is gone. Cleaner, matches the
superpowers reference: 2024-05-07-stimulus-target-callbacks.
2. **data-turbo-permanent on the Region wrapper** so the toaster
survives Turbo Drive navigations (in-flight toasts no longer get
wiped when the user clicks a link). Outer <div> now also carries a
stable id="ruby-ui-toaster-region" so morph + permanent reconciles
correctly. Reference: hwc-navigation-content cache lifecycle.
3. **Custom Turbo Stream action **.
StreamActions.toast registers once at controller connect; reads
variant/title/description/duration/id from the <turbo-stream>
attributes and dispatches the standard ruby-ui:toast event.
Server-side becomes ergonomic:
turbo_stream.action(:toast, target: 'ruby-ui-toaster',
variant: :success, title: 'Saved')
The append-with-rendered-Item path is still supported for cases
needing Action/Cancel slots. Reference:
2023-08-15-turbo-streams-custom-stream-actions.
4. **JS API now dispatches CustomEvent instead of calling _spawn
directly.** Decoupled — any listener (including future multi-region
setups) sees the same ruby-ui:toast events. window.RubyUI.toast.*
stays as sugar over window.dispatchEvent.
5. Visible-only items get tabindex=-1 in _reflow for keyboard a11y.
182 runs, 793 assertions, 0 failures.
* [Bug Fix] Fix standardrb single-quote offense in toast docs view
* [Documentation] Wrap Types in VisualCodeExample (Preview/Code tabs)
* [Refactor] Clone Toast slot buttons from <template> targets
Drops ~25 lines of hardcoded Tailwind classes from toaster_controller.js.
Phlex now owns the single source of truth for ToastAction, ToastCancel,
and ToastClose styling.
- Region renders three additional <template>s next to the variant
skeletons: actionTpl, cancelTpl, closeTpl. Each renders the
corresponding Phlex component once, with default classes.
- Toaster controller declares actionTplTarget / cancelTplTarget /
closeTplTarget. _spawn clones from these instead of building DOM
elements with hand-written className strings.
- _cloneSlot helper centralizes the clone-firstElementChild pattern.
Both delivery paths benefit from a single Tailwind source:
- Server-pushed (Turbo Stream append) — already used Phlex Items
end-to-end; unchanged.
- Client-spawned (window.RubyUI.toast.*) — now also Phlex-sourced.
Update Tailwind/style tweaks in Action/Cancel/Close .rb files and JS
picks them up automatically; @source scan continues to see the
classes in the gem files.1 parent d737706 commit 1c1666a
29 files changed
Lines changed: 1895 additions & 1 deletion
File tree
- docs
- .devcontainer
- app
- controllers
- docs
- javascript/controllers
- ruby_ui
- views
- docs
- layouts
- config
- gem
- lib
- generators/ruby_ui
- ruby_ui/toast
- test/ruby_ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
11 | 14 | | |
12 | 15 | | |
13 | 16 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| 53 | + | |
53 | 54 | | |
54 | 55 | | |
55 | 56 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
222 | 222 | | |
223 | 223 | | |
224 | 224 | | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
225 | 229 | | |
226 | 230 | | |
227 | 231 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
10 | 13 | | |
11 | 14 | | |
12 | 15 | | |
| |||
94 | 97 | | |
95 | 98 | | |
96 | 99 | | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
97 | 106 | | |
98 | 107 | | |
99 | 108 | | |
| |||
Lines changed: 151 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
0 commit comments