-
Notifications
You must be signed in to change notification settings - Fork 14
PT-4193: Add secondary notification action, position, and dismissible #2561
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
1ff72bc
PT-4193: Add secondary notification action, position, and dismissible
rolfheij-sil 74330d8
PT-4193: Fix dismissible: false silently disabling the secondary button
rolfheij-sil c90014f
PT-4193: Regenerate papi.d.ts to match reflowed source JSDoc
rolfheij-sil 181ea82
PT-4193: Fix two-button toast crushing message content to a sliver
rolfheij-sil b07cde0
PT-4193: Drop stale send() updates racing their own dismiss()
rolfheij-sil 44a8809
fix(notifications): remove the 5s dismiss grace window (PT-4193 revie…
rolfheij-sil e5acbc7
fix(notifications): rework two-button toast CSS + reach every positio…
rolfheij-sil 2585b02
fix(notifications): harden the notification service host (PT-4193 rev…
rolfheij-sil 7daea47
fix(notifications): extend the two-row toast reflow to single-button …
rolfheij-sil 7ca04dc
fix(notifications): keep the buttoned-toast row break through Sonner'…
rolfheij-sil f780c8d
docs(notifications): tag the secondary-action/position/dismissible su…
rolfheij-sil 90c2bc5
fix(notifications): address PT-4193 round-5 review findings
lyonsil 9c3be33
feat(notifications): style the secondary toast button as shadcn secon…
lyonsil 3918b13
chore(deps): pin sonner to exactly 1.7.4 (PT-4193)
lyonsil 4db3519
fix(notifications): follow the app theme in the notification Toaster …
lyonsil d741a3a
docs(notifications): drop PR/review-round references from code comments
lyonsil File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| // A toast with buttons renders, by Sonner 1.7.4's own stylesheet | ||
| // (node_modules/sonner/dist/styles.css), as a single un-wrapped flex row: icon, content, cancel | ||
| // button, action button. Sonner's buttons are `flex-shrink: 0` and its toast width is a fixed | ||
| // `--width` (356px), so wide buttons crush the message content down to a sliver - confirmed live | ||
| // twice: a two-button Send/Receive consent toast squeezed its message to ~1 character, and (round-4 | ||
| // E2E) a single-action break-lock toast ("Break lock and retry" beside a long warning) was just as | ||
| // cramped. So give EVERY toast with at least one button a two-row layout instead: [icon][message] | ||
| // on the first row, and the button(s) right-aligned on a row of their own. Scope the fix to | ||
| // buttoned toasts via `:has()` so plain-message toasts stay pixel-identical to before. | ||
| // | ||
| // Sonner exposes no supported API for a two-row toast body (the only real escape hatch, | ||
| // `toast.custom`, means re-implementing Sonner's severity icon/color rendering), so this reaches | ||
| // into its flex layout. It therefore DEPENDS on Sonner 1.7.4 rendering the icon, content, cancel, | ||
| // and action as direct flex children of the toast `<li>`; revisit if Sonner changes its toast DOM. | ||
| // Deliberately avoids Sonner's own `::after` (its ALWAYS-ON hover-bridge between stacked toasts) | ||
| // and repurposes `::before`, which Sonner only styles in the swiping/removed states - see the | ||
| // comment on the row break below for how that collision is resolved. | ||
| .notification-toast:has(.notification-toast-cancel-button, .notification-toast-action-button) { | ||
| flex-wrap: wrap; | ||
|
|
||
| .notification-toast-content { | ||
| // Grow to fill the first row beside the icon. `flex-basis: 0` keeps the message's hypothetical | ||
| // width at 0 so the flex-wrap algorithm never bumps the message onto a line below the icon | ||
| // (which would leave the icon orphaned on a row of its own); the message wraps *within* this | ||
| // grown box instead. | ||
| flex: 1 1 0; | ||
| min-inline-size: 0; | ||
| } | ||
|
|
||
| // Full-width, zero-height flex break that forces the buttons onto their own row. Unlike the | ||
| // previous `::after` hack this leaves Sonner's `::after` hover-bridge alone, and it must apply in | ||
| // EVERY toast state: Sonner flips the toast to `data-swiping="true"` the instant any mouse button | ||
| // is pressed on the toast body (before any movement - a plain press-and-hold counts), and an | ||
| // earlier revision that gated this break out of the swiping/removed states collapsed the toast to | ||
| // a one-character-wide sliver for the duration of every such press, because the `flex-wrap` and | ||
| // `flex-basis: 0` rules above stayed active with no row break. Sonner's own use of `::before` in | ||
| // those states is an invisible, ABSOLUTELY-positioned hover/hit-area extender declared at zero | ||
| // specificity (`:where(...)`), so this higher-specificity rule wins on every property it declares; | ||
| // `position: static` is declared explicitly to keep the pseudo-element in flex flow when Sonner's | ||
| // swipe/removal styles try to absolutely position it (its leftover declarations - inset, height, | ||
| // transform, z-index - are inert on a zero-height in-flow item, `height` losing to `block-size` | ||
| // by specificity). Trade-off: buttoned toasts lose Sonner's enlarged swipe/removal hover hit | ||
| // area; swipe-to-dismiss tracking is unaffected because Sonner captures the pointer at press | ||
| // time. | ||
| &::before { | ||
|
lyonsil marked this conversation as resolved.
|
||
| content: ''; | ||
| position: static; | ||
| order: 1; | ||
| flex-basis: 100%; | ||
| block-size: 0; | ||
|
lyonsil marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // Collect the button(s) right-aligned on the button row. Sonner puts `margin-inline-start: auto` | ||
| // on *every* button, which right-aligns a lone button all by itself but, when both buttons share | ||
| // a row, splits the free space *between* them instead of pushing them together; so leave the auto | ||
| // margin alone except on an action button whose toast also has a cancel button - zeroing only | ||
| // that one leaves the pair flush at the row's end, separated by the toast's own gap. | ||
| .notification-toast-cancel-button { | ||
| order: 2; | ||
| margin-inline-start: auto; | ||
| } | ||
|
|
||
| .notification-toast-action-button { | ||
| order: 3; | ||
| } | ||
|
|
||
| &:has(.notification-toast-cancel-button) .notification-toast-action-button { | ||
|
lyonsil marked this conversation as resolved.
|
||
| margin-inline-start: 0; | ||
| } | ||
|
|
||
| // Settle the DEFAULT look of the secondary (cancel-slot) button as the shadcn `secondary` button | ||
| // variant (button.tsx: bg-secondary / text-secondary-foreground / hover:bg-secondary/80), per UX | ||
| // direction. Without this, Sonner 1.7.4's styled mode renders BOTH buttons | ||
| // identically dark: its base `[data-sonner-toast][data-styled='true'] [data-button]` rule | ||
| // (specificity (0,3,0)) sets the action colors on every `[data-button]` - including the cancel | ||
| // button, which also carries `data-button` - and its softer `:where([data-cancel])` defaults are | ||
| // zero-specificity so they never win. Which button gets this look is deterministic: the service | ||
| // host maps `clickCommand` to Sonner's `action` slot (keeps the emphasized primary look) and | ||
| // `secondaryClickCommand` to the `cancel` slot, whose class this targets. The extra `&:has()` | ||
| // keeps specificity at (0,4,0) so this beats Sonner's (0,3,0) base rule regardless of which | ||
| // stylesheet loads last (same pattern as the margin rule above). | ||
| &:has(.notification-toast-cancel-button) .notification-toast-cancel-button { | ||
| background: var(--secondary); | ||
| color: var(--secondary-foreground); | ||
|
|
||
| &:hover { | ||
| background: color-mix(in oklab, var(--secondary) 80%, transparent); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.