feat: add SnackbarToast component for canvas feedback#11731
feat: add SnackbarToast component for canvas feedback#11731dante01yoon wants to merge 3 commits intomainfrom
Conversation
Introduces a singleton snackbar toast component (bottom-center, Teleport to body) intended for non-blocking canvas feedback. Surfaces an optional keybinding badge or an action button (e.g. Undo) and supports auto- dismiss with hover-pause. Component-only — no app integration in this PR. Wiring to specific canvas commands (link visibility, focus mode, subgraph unpack) will land in follow-up PRs once the UX direction is settled (#11718 thread). Visuals match Figma node 6826:77784 in the Comfy Design System. Refs FE-484
📝 WalkthroughWalkthroughAdds a typed snackbar toast system: a composable ( Changes
Sequence DiagramsequenceDiagram
participant Consumer as Consumer / Story
participant Composable as useSnackbarToast (inject)
participant Provider as SnackbarToastProvider
participant ToastComp as SnackbarToast Component
participant User as End User
Consumer->>Composable: call show(message, options)
Composable->>Provider: (injected) show(message, options)
Provider->>Provider: create id, build SnackbarToastItem
Provider->>Provider: replace toasts array (single item)
Provider-->>ToastComp: reactive prop (toast item)
ToastComp->>User: render message, controls
User->>ToastComp: click action (if present)
ToastComp->>Provider: invoke onAction (calls provided callback) and emit dismiss
Provider->>Provider: dismiss(id) -> remove toast from array
User->>ToastComp: click close
ToastComp->>Provider: emit dismiss
Provider->>Provider: remove toast
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 6 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (6 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.Comment |
🎨 Storybook: ✅ Built — View Storybook |
🎭 Playwright: ✅ 1423 passed, 0 failed · 1 flaky📊 Browser Reports
|
📦 Bundle: 5.23 MB gzip 🔴 +118 BDetailsSummary
Category Glance App Entry Points — 22.5 kB (baseline 22.5 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.24 MB (baseline 1.24 MB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 77.7 kB (baseline 77.7 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 9 added / 9 removed / 2 unchanged Panels & Settings — 484 kB (baseline 484 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 10 added / 10 removed / 11 unchanged User & Accounts — 17.4 kB (baseline 17.4 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 5 added / 5 removed / 2 unchanged Editors & Dialogs — 113 kB (baseline 113 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 4 added / 4 removed UI Components — 61 kB (baseline 61 kB) • ⚪ 0 BReusable component library chunks
Status: 5 added / 5 removed / 8 unchanged Data & Services — 3.04 MB (baseline 3.04 MB) • ⚪ 0 BStores, services, APIs, and repositories
Status: 13 added / 13 removed / 4 unchanged Utilities & Hooks — 364 kB (baseline 364 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 13 added / 13 removed / 18 unchanged Vendor & Third-Party — 9.88 MB (baseline 9.88 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 8.83 MB (baseline 8.83 MB) • ⚪ 0 BBundles that do not match a named category
Status: 57 added / 57 removed / 78 unchanged ⚡ Performance Report
No regressions detected. All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-04-29T00:59:05.157Z",
"gitSha": "abc86f97c613eb1945285dd16bc7d6b5a4f8e7ec",
"branch": "feat/snackbar-toast-component",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2013.261999999969,
"styleRecalcs": 7,
"styleRecalcDurationMs": 7.601,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 455.18100000000004,
"heapDeltaBytes": 22477796,
"heapUsedBytes": 70676364,
"domNodes": 14,
"jsHeapTotalBytes": 14417920,
"scriptDurationMs": 25.020999999999997,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-idle",
"durationMs": 2012.4889999999596,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.046999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 343.3639999999999,
"heapDeltaBytes": 2005572,
"heapUsedBytes": 68588780,
"domNodes": 18,
"jsHeapTotalBytes": 20709376,
"scriptDurationMs": 15.315999999999995,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2003.011000000015,
"styleRecalcs": 10,
"styleRecalcDurationMs": 8.889,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 334.517,
"heapDeltaBytes": 23067696,
"heapUsedBytes": 71422368,
"domNodes": 19,
"jsHeapTotalBytes": 15204352,
"scriptDurationMs": 15.477999999999998,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1931.7990000000123,
"styleRecalcs": 75,
"styleRecalcDurationMs": 47.416,
"layouts": 12,
"layoutDurationMs": 3.7739999999999996,
"taskDurationMs": 818.0000000000001,
"heapDeltaBytes": -2138284,
"heapUsedBytes": 64027316,
"domNodes": 59,
"jsHeapTotalBytes": 20971520,
"scriptDurationMs": 134.712,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1772.803999999951,
"styleRecalcs": 73,
"styleRecalcDurationMs": 35.422999999999995,
"layouts": 12,
"layoutDurationMs": 3.3379999999999996,
"taskDurationMs": 765.7389999999999,
"heapDeltaBytes": -14957908,
"heapUsedBytes": 51300448,
"domNodes": -261,
"jsHeapTotalBytes": 20668416,
"scriptDurationMs": 127.06699999999998,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2001.0389999999916,
"styleRecalcs": 80,
"styleRecalcDurationMs": 45.732,
"layouts": 12,
"layoutDurationMs": 3.4800000000000004,
"taskDurationMs": 953.87,
"heapDeltaBytes": 972476,
"heapUsedBytes": 48657660,
"domNodes": -258,
"jsHeapTotalBytes": 15593472,
"scriptDurationMs": 133.046,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1754.4599999999946,
"styleRecalcs": 31,
"styleRecalcDurationMs": 16.701999999999998,
"layouts": 6,
"layoutDurationMs": 0.5810000000000002,
"taskDurationMs": 336.26,
"heapDeltaBytes": 5959232,
"heapUsedBytes": 71991360,
"domNodes": 78,
"jsHeapTotalBytes": 19230720,
"scriptDurationMs": 27.764999999999997,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1739.5930000000135,
"styleRecalcs": 31,
"styleRecalcDurationMs": 17.769,
"layouts": 6,
"layoutDurationMs": 0.704,
"taskDurationMs": 306.343,
"heapDeltaBytes": 5841532,
"heapUsedBytes": 71902232,
"domNodes": 78,
"jsHeapTotalBytes": 18706432,
"scriptDurationMs": 26.153999999999996,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1730.660999999941,
"styleRecalcs": 31,
"styleRecalcDurationMs": 17.586,
"layouts": 6,
"layoutDurationMs": 0.6899999999999998,
"taskDurationMs": 309.503,
"heapDeltaBytes": 5697548,
"heapUsedBytes": 72009528,
"domNodes": 78,
"jsHeapTotalBytes": 19230720,
"scriptDurationMs": 22.037000000000003,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 559.8290000000361,
"styleRecalcs": 13,
"styleRecalcDurationMs": 8.818000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 348.257,
"heapDeltaBytes": -12249920,
"heapUsedBytes": 53997424,
"domNodes": 22,
"jsHeapTotalBytes": 19755008,
"scriptDurationMs": 62.96399999999999,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "dom-widget-clipping",
"durationMs": 548.546999999985,
"styleRecalcs": 12,
"styleRecalcDurationMs": 8.366000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 332.677,
"heapDeltaBytes": -12546432,
"heapUsedBytes": 54010956,
"domNodes": 19,
"jsHeapTotalBytes": 19230720,
"scriptDurationMs": 57.329,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.669999999999998,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "dom-widget-clipping",
"durationMs": 563.0199999999377,
"styleRecalcs": 13,
"styleRecalcDurationMs": 8.876,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 345.14099999999996,
"heapDeltaBytes": -12103872,
"heapUsedBytes": 54163080,
"domNodes": 22,
"jsHeapTotalBytes": 20279296,
"scriptDurationMs": 65.153,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2010.5630000000474,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.600999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 520.1450000000001,
"heapDeltaBytes": 4883816,
"heapUsedBytes": 63040484,
"domNodes": -264,
"jsHeapTotalBytes": 552960,
"scriptDurationMs": 89.76800000000001,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2017.733000000021,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.911999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 521.8249999999999,
"heapDeltaBytes": 1603832,
"heapUsedBytes": 59733844,
"domNodes": -264,
"jsHeapTotalBytes": 5009408,
"scriptDurationMs": 89.046,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-idle",
"durationMs": 2039.5260000000235,
"styleRecalcs": 7,
"styleRecalcDurationMs": 6.832999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 526.464,
"heapDeltaBytes": 5332000,
"heapUsedBytes": 63584464,
"domNodes": -264,
"jsHeapTotalBytes": 552960,
"scriptDurationMs": 92.893,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2139.2910000000143,
"styleRecalcs": 68,
"styleRecalcDurationMs": 17.784,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1097.413,
"heapDeltaBytes": 4147788,
"heapUsedBytes": 63844000,
"domNodes": -262,
"jsHeapTotalBytes": 4456448,
"scriptDurationMs": 393.759,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-pan",
"durationMs": 2120.8819999999946,
"styleRecalcs": 67,
"styleRecalcDurationMs": 16.836,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1116.646,
"heapDeltaBytes": 5172316,
"heapUsedBytes": 64328528,
"domNodes": -266,
"jsHeapTotalBytes": 1282048,
"scriptDurationMs": 408.583,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-pan",
"durationMs": 2099.796999999967,
"styleRecalcs": 67,
"styleRecalcDurationMs": 15.420999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1115.147,
"heapDeltaBytes": 30421548,
"heapUsedBytes": 81017424,
"domNodes": -244,
"jsHeapTotalBytes": 3350528,
"scriptDurationMs": 460.4050000000001,
"eventListeners": -121,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3114.2729999999688,
"styleRecalcs": 65,
"styleRecalcDurationMs": 18.159,
"layouts": 60,
"layoutDurationMs": 7.0889999999999995,
"taskDurationMs": 1312.8090000000002,
"heapDeltaBytes": -6821588,
"heapUsedBytes": 52872060,
"domNodes": -268,
"jsHeapTotalBytes": -1081344,
"scriptDurationMs": 480.05,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3163.618999999983,
"styleRecalcs": 66,
"styleRecalcDurationMs": 19.644,
"layouts": 60,
"layoutDurationMs": 7.684999999999999,
"taskDurationMs": 1359.175,
"heapDeltaBytes": 8066000,
"heapUsedBytes": 68539132,
"domNodes": -265,
"jsHeapTotalBytes": 4222976,
"scriptDurationMs": 496.788,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-zoom",
"durationMs": 3130.6409999999687,
"styleRecalcs": 65,
"styleRecalcDurationMs": 17.927,
"layouts": 60,
"layoutDurationMs": 7.246,
"taskDurationMs": 1291.891,
"heapDeltaBytes": 9694476,
"heapUsedBytes": 69139884,
"domNodes": -269,
"jsHeapTotalBytes": 6057984,
"scriptDurationMs": 474.59700000000004,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2053.6789999999883,
"styleRecalcs": 10,
"styleRecalcDurationMs": 10.102000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 537.515,
"heapDeltaBytes": 14990404,
"heapUsedBytes": 75058148,
"domNodes": -255,
"jsHeapTotalBytes": -233472,
"scriptDurationMs": 94.63499999999999,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2022.3250000000235,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.843,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 516.4799999999999,
"heapDeltaBytes": 10424576,
"heapUsedBytes": 69427316,
"domNodes": -263,
"jsHeapTotalBytes": 552960,
"scriptDurationMs": 85.329,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2052.3230000000012,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.141,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 520.523,
"heapDeltaBytes": 13968704,
"heapUsedBytes": 73160576,
"domNodes": -260,
"jsHeapTotalBytes": -233472,
"scriptDurationMs": 88.64800000000001,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 572.9979999999841,
"styleRecalcs": 48,
"styleRecalcDurationMs": 11.995000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 386.58099999999996,
"heapDeltaBytes": -12107264,
"heapUsedBytes": 54327408,
"domNodes": 21,
"jsHeapTotalBytes": 20017152,
"scriptDurationMs": 136.95100000000002,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 529.6959999999444,
"styleRecalcs": 47,
"styleRecalcDurationMs": 11.380999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 359.7300000000001,
"heapDeltaBytes": -10456924,
"heapUsedBytes": 55787092,
"domNodes": 20,
"jsHeapTotalBytes": 20709376,
"scriptDurationMs": 119.976,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 560.6610000000956,
"styleRecalcs": 47,
"styleRecalcDurationMs": 11.458999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 360.36300000000006,
"heapDeltaBytes": -12411328,
"heapUsedBytes": 54326012,
"domNodes": 20,
"jsHeapTotalBytes": 19230720,
"scriptDurationMs": 121.40399999999998,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "subgraph-idle",
"durationMs": 2020.858999999973,
"styleRecalcs": 12,
"styleRecalcDurationMs": 12.950999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 388.96,
"heapDeltaBytes": 971500,
"heapUsedBytes": 66786536,
"domNodes": 23,
"jsHeapTotalBytes": 20017152,
"scriptDurationMs": 22.999000000000006,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-idle",
"durationMs": 2022.1860000000333,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.440999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 398.36199999999997,
"heapDeltaBytes": -21976020,
"heapUsedBytes": 44604556,
"domNodes": -261,
"jsHeapTotalBytes": 18571264,
"scriptDurationMs": 18.873,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2006.6770000000815,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.732000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 340.587,
"heapDeltaBytes": 1159120,
"heapUsedBytes": 67767032,
"domNodes": 19,
"jsHeapTotalBytes": 18968576,
"scriptDurationMs": 14.021,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1711.9959999999992,
"styleRecalcs": 77,
"styleRecalcDurationMs": 37.721000000000004,
"layouts": 16,
"layoutDurationMs": 4.554000000000001,
"taskDurationMs": 688.348,
"heapDeltaBytes": -7253388,
"heapUsedBytes": 59355520,
"domNodes": 64,
"jsHeapTotalBytes": 20279296,
"scriptDurationMs": 97.345,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1707.2430000000054,
"styleRecalcs": 77,
"styleRecalcDurationMs": 38.307,
"layouts": 16,
"layoutDurationMs": 4.9350000000000005,
"taskDurationMs": 681.452,
"heapDeltaBytes": -5813592,
"heapUsedBytes": 60788268,
"domNodes": 64,
"jsHeapTotalBytes": 19922944,
"scriptDurationMs": 99.026,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1707.8229999999621,
"styleRecalcs": 76,
"styleRecalcDurationMs": 37.001,
"layouts": 16,
"layoutDurationMs": 4.387,
"taskDurationMs": 704.499,
"heapDeltaBytes": -7115544,
"heapUsedBytes": 59374976,
"domNodes": 63,
"jsHeapTotalBytes": 19230720,
"scriptDurationMs": 96.44800000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "viewport-pan-sweep",
"durationMs": 8135.9429999999975,
"styleRecalcs": 249,
"styleRecalcDurationMs": 51.54,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3695.3689999999997,
"heapDeltaBytes": 17351128,
"heapUsedBytes": 75983816,
"domNodes": -262,
"jsHeapTotalBytes": 4194304,
"scriptDurationMs": 1258.5249999999999,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "viewport-pan-sweep",
"durationMs": 8153.731999999991,
"styleRecalcs": 250,
"styleRecalcDurationMs": 51.634,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3615.44,
"heapDeltaBytes": 21458032,
"heapUsedBytes": 79021744,
"domNodes": -258,
"jsHeapTotalBytes": 1744896,
"scriptDurationMs": 1229.6789999999999,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8127.408999999943,
"styleRecalcs": 249,
"styleRecalcDurationMs": 51.141,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3677.9770000000003,
"heapDeltaBytes": 17533568,
"heapUsedBytes": 75815456,
"domNodes": -263,
"jsHeapTotalBytes": 1544192,
"scriptDurationMs": 1249.239,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 12433.410000000038,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12423.696999999998,
"heapDeltaBytes": -46883664,
"heapUsedBytes": 173812888,
"domNodes": -9850,
"jsHeapTotalBytes": 27848704,
"scriptDurationMs": 576.3999999999999,
"eventListeners": -23957,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 12475.641999999993,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12463.238,
"heapDeltaBytes": -36866632,
"heapUsedBytes": 181558292,
"domNodes": -9850,
"jsHeapTotalBytes": -15405056,
"scriptDurationMs": 623.27,
"eventListeners": -23957,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 33.20000000000073
},
{
"name": "vue-large-graph-idle",
"durationMs": 12193.06000000006,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12181.995999999997,
"heapDeltaBytes": -45564040,
"heapUsedBytes": 173179016,
"domNodes": -9850,
"jsHeapTotalBytes": 24702976,
"scriptDurationMs": 601.595,
"eventListeners": -23956,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 33.20000000000073
},
{
"name": "vue-large-graph-pan",
"durationMs": 14673.465999999962,
"styleRecalcs": 68,
"styleRecalcDurationMs": 16.392000000000017,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14651.616,
"heapDeltaBytes": -49395900,
"heapUsedBytes": 165632404,
"domNodes": -9850,
"jsHeapTotalBytes": -12783616,
"scriptDurationMs": 852.417,
"eventListeners": -23983,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 14438.173000000006,
"styleRecalcs": 67,
"styleRecalcDurationMs": 18.576000000000036,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14416.16,
"heapDeltaBytes": -62745316,
"heapUsedBytes": 164948036,
"domNodes": -9850,
"jsHeapTotalBytes": -10162176,
"scriptDurationMs": 863.785,
"eventListeners": -23983,
"totalBlockingTimeMs": 71,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 14406.810999999947,
"styleRecalcs": 66,
"styleRecalcDurationMs": 16.508000000000024,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14388.105999999998,
"heapDeltaBytes": -56369204,
"heapUsedBytes": 172159996,
"domNodes": -9850,
"jsHeapTotalBytes": -20209664,
"scriptDurationMs": 958.9680000000001,
"eventListeners": -23955,
"totalBlockingTimeMs": 16,
"frameDurationMs": 17.776666666666763,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 465.1950000000511,
"styleRecalcs": 17,
"styleRecalcDurationMs": 23.291000000000004,
"layouts": 5,
"layoutDurationMs": 1.43,
"taskDurationMs": 137.67700000000002,
"heapDeltaBytes": -13411788,
"heapUsedBytes": 54039932,
"domNodes": 167,
"jsHeapTotalBytes": 5242880,
"scriptDurationMs": 26.871,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "workflow-execution",
"durationMs": 447.9760000000397,
"styleRecalcs": 18,
"styleRecalcDurationMs": 25.241,
"layouts": 5,
"layoutDurationMs": 1.243,
"taskDurationMs": 119.05400000000002,
"heapDeltaBytes": 5089104,
"heapUsedBytes": 56087868,
"domNodes": 154,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 23.969000000000005,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "workflow-execution",
"durationMs": 478.0009999999493,
"styleRecalcs": 16,
"styleRecalcDurationMs": 21.935999999999996,
"layouts": 5,
"layoutDurationMs": 1.3569999999999998,
"taskDurationMs": 138.25900000000001,
"heapDeltaBytes": -14816196,
"heapUsedBytes": 52662260,
"domNodes": 156,
"jsHeapTotalBytes": 4288512,
"scriptDurationMs": 30.26,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
}
]
} |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/composables/useSnackbarToast.ts (1)
3-11: UsecreateSharedComposableto wrap module-scoped state.This composable relies on module-scoped singleton state, which leaks across HMR/tests/SSR boundaries. The codebase already establishes this pattern in other composables (useVueFeatureFlags, useImageLoader, useVueNodeLifecycle, useCoordinateTransform, useBillingContext). Wrap the internal state and timer logic with
createSharedComposableto ensure proper lifecycle management.♻️ Suggested refactor
import { ref } from 'vue' +import { createSharedComposable } from '@vueuse/core' -const message = ref('') -const shortcut = ref('') -const visible = ref(false) -const actionLabel = ref('') -const onAction = ref<(() => void) | null>(null) -let timeout: ReturnType<typeof setTimeout> | null = null -let duration = 2000 - -export function useSnackbarToast() { +export const useSnackbarToast = createSharedComposable(function useSnackbarToast() { + const message = ref('') + const shortcut = ref('') + const visible = ref(false) + const actionLabel = ref('') + const onAction = ref<(() => void) | null>(null) + let timeout: ReturnType<typeof setTimeout> | null = null + let duration = 2000 function show( msg: string, @@ return { @@ } -} +})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/composables/useSnackbarToast.ts` around lines 3 - 11, The module-scoped state (message, shortcut, visible, actionLabel, onAction, timeout, duration) in useSnackbarToast must be wrapped with createSharedComposable to avoid singleton leaks across HMR/tests/SSR; refactor by moving these refs and the timer logic inside a createSharedComposable call and return the shared composable from export function useSnackbarToast so useSnackbarToast delegates to the shared instance, ensuring the timeout is cleared when the shared composable is disposed and all original methods/values (message, shortcut, visible, actionLabel, onAction, show/hide logic) are preserved.src/components/graph/SnackbarToast.stories.ts (1)
32-33: Story files throughout the codebase omit i18n; this refactor is optional.While guidelines specify vue-i18n for src/**/*.ts, none of the 57+ story files in the repository use i18n. Story files are excluded from test coverage and treated as dev-only fixtures in the build config. If you'd like to standardize this file, you can move the hardcoded strings ('Toast message', 'Links hidden', 'Subgraph unpacked', 'Subgraph repacked', 'Stays open until dismissed', 'Auto-dismiss after 2s...', 'Toast with assigned keybinding badge...', etc.) to src/locales/en/main.json and access them via
useI18n()in the story setup blocks. However, this is a low-priority improvement since the project does not enforce i18n for story files.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/graph/SnackbarToast.stories.ts` around lines 32 - 33, This story file uses hardcoded strings (e.g., 'Toast message', 'Links hidden', 'Subgraph unpacked', etc.) instead of i18n; to standardize, add those strings as keys in your locales (e.g., src/locales/en/main.json) and update the story's setup to call useI18n(), then replace direct literals used in the story actions (for example where toast.show(...) is called and any labels/controls in the story setup) with t('your.key') lookups; ensure keys are unique and descriptive (e.g., snackbar.toast_message) and update all occurrences in SnackbarToast.stories.ts including examples that show auto-dismiss text and keybinding badges.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/graph/SnackbarToast.vue`:
- Line 69: The computed `hasAction` currently only checks `onAction` and
`shortcut`; update it to also require a non-empty `actionLabel` so the action
button isn't shown without a label. Modify the `hasAction` computed (the
function named hasAction that references onAction.value and shortcut.value) to
include a check like `actionLabel.value && actionLabel.value.trim().length > 0`
(or equivalent truthy check) alongside the existing conditions.
In `@src/composables/useSnackbarToast.ts`:
- Around line 31-40: The pause/resume logic currently restarts the full duration
because startTimer always uses duration; fix by tracking remaining milliseconds
and start timestamp: add a numeric variable (e.g., remaining = duration) and a
startTime timestamp; when startTimer runs set startTime = Date.now(), clear any
existing timeout, and set timeout = setTimeout(..., remaining); in pause()
compute remaining = Math.max(0, remaining - (Date.now() - startTime)),
clearTimeout(timeout) but do not reset remaining to the full duration; ensure
startTimer can be called to resume using the remaining value and that
visible.value behavior remains unchanged.
---
Nitpick comments:
In `@src/components/graph/SnackbarToast.stories.ts`:
- Around line 32-33: This story file uses hardcoded strings (e.g., 'Toast
message', 'Links hidden', 'Subgraph unpacked', etc.) instead of i18n; to
standardize, add those strings as keys in your locales (e.g.,
src/locales/en/main.json) and update the story's setup to call useI18n(), then
replace direct literals used in the story actions (for example where
toast.show(...) is called and any labels/controls in the story setup) with
t('your.key') lookups; ensure keys are unique and descriptive (e.g.,
snackbar.toast_message) and update all occurrences in SnackbarToast.stories.ts
including examples that show auto-dismiss text and keybinding badges.
In `@src/composables/useSnackbarToast.ts`:
- Around line 3-11: The module-scoped state (message, shortcut, visible,
actionLabel, onAction, timeout, duration) in useSnackbarToast must be wrapped
with createSharedComposable to avoid singleton leaks across HMR/tests/SSR;
refactor by moving these refs and the timer logic inside a
createSharedComposable call and return the shared composable from export
function useSnackbarToast so useSnackbarToast delegates to the shared instance,
ensuring the timeout is cleared when the shared composable is disposed and all
original methods/values (message, shortcut, visible, actionLabel, onAction,
show/hide logic) are preserved.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ebdeee51-fc9d-43f7-862f-66b497e156ca
📒 Files selected for processing (3)
src/components/graph/SnackbarToast.stories.tssrc/components/graph/SnackbarToast.vuesrc/composables/useSnackbarToast.ts
| startTimer | ||
| } = useSnackbarToast() | ||
|
|
||
| const hasAction = computed(() => !!onAction.value && !shortcut.value) |
There was a problem hiding this comment.
Require both callback and label before rendering the action button.
hasAction should also require a non-empty actionLabel; otherwise an unlabeled action button can render when only onAction is set.
✅ Minimal fix
-const hasAction = computed(() => !!onAction.value && !shortcut.value)
+const hasAction = computed(
+ () => !!onAction.value && !!actionLabel.value && !shortcut.value
+)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const hasAction = computed(() => !!onAction.value && !shortcut.value) | |
| const hasAction = computed( | |
| () => !!onAction.value && !!actionLabel.value && !shortcut.value | |
| ) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/graph/SnackbarToast.vue` at line 69, The computed `hasAction`
currently only checks `onAction` and `shortcut`; update it to also require a
non-empty `actionLabel` so the action button isn't shown without a label. Modify
the `hasAction` computed (the function named hasAction that references
onAction.value and shortcut.value) to include a check like `actionLabel.value &&
actionLabel.value.trim().length > 0` (or equivalent truthy check) alongside the
existing conditions.
| function startTimer() { | ||
| if (timeout) clearTimeout(timeout) | ||
| timeout = setTimeout(() => { | ||
| visible.value = false | ||
| }, duration) | ||
| } | ||
|
|
||
| function pause() { | ||
| if (timeout) clearTimeout(timeout) | ||
| } |
There was a problem hiding this comment.
Hover “pause/resume” currently restarts the full timer.
pause() clears the timeout, and startTimer() restarts with full duration, so hover extends lifetime instead of resuming remaining time. Track remaining milliseconds to match pause/resume behavior.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/composables/useSnackbarToast.ts` around lines 31 - 40, The pause/resume
logic currently restarts the full duration because startTimer always uses
duration; fix by tracking remaining milliseconds and start timestamp: add a
numeric variable (e.g., remaining = duration) and a startTime timestamp; when
startTimer runs set startTime = Date.now(), clear any existing timeout, and set
timeout = setTimeout(..., remaining); in pause() compute remaining = Math.max(0,
remaining - (Date.now() - startTime)), clearTimeout(timeout) but do not reset
remaining to the full duration; ensure startTimer can be called to resume using
the remaining value and that visible.value behavior remains unchanged.
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #11731 +/- ##
==========================================
+ Coverage 51.90% 52.40% +0.50%
==========================================
Files 1376 1379 +3
Lines 70445 70494 +49
Branches 18716 18725 +9
==========================================
+ Hits 36562 36940 +378
+ Misses 33336 33007 -329
Partials 547 547
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 15 files with indirect coverage changes 🚀 New features to boost your workflow:
|
Belongs with the existing toast components (`GlobalToast`, `ProgressToastItem`, `RerouteMigrationToast`), not under `components/graph/`. Story title updated to `Components/Toast/SnackbarToast`.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/components/toast/SnackbarToast.vue (1)
69-69:⚠️ Potential issue | 🟡 MinorRequire a non-empty
actionLabelbefore showing the action button.
hasActionstill allows rendering an action button whenonActionexists but
the label is empty, which can produce an unlabeled control.✅ Minimal fix
-const hasAction = computed(() => !!onAction.value && !shortcut.value) +const hasAction = computed( + () => !!onAction.value && !!actionLabel.value?.trim() && !shortcut.value +)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/toast/SnackbarToast.vue` at line 69, The computed `hasAction` currently only checks `onAction` and `shortcut`, allowing an action button with an empty label; update the `hasAction` logic in SnackbarToast.vue to also require a non-empty, non-whitespace `actionLabel` (e.g., check that `actionLabel.value` exists and trim().length > 0) alongside the existing `onAction` and `!shortcut` checks so the action button is only shown when there is an actual label.
🧹 Nitpick comments (1)
src/components/toast/SnackbarToast.stories.ts (1)
32-107: Move hardcoded user-facing strings to i18n keys.Story descriptions, button labels, and toast message literals are hardcoded in
English. Please route them through vue-i18n and add entries in
src/locales/en/main.jsonfor consistency with project rules.As per coding guidelines, "Use vue-i18n for ALL user-facing strings, configured in
src/locales/en/main.json" and "Use vue-i18n in composition API for any string literals. Place new translation entries insrc/locales/en/main.json."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/toast/SnackbarToast.stories.ts` around lines 32 - 107, The story file contains hardcoded user-facing strings in the story components (see Story exports WithShortcut, WithUndoAction, Persistent and their inner setup functions trigger/dismiss using useSnackbarToast, plus Button labels and paragraph descriptions); replace each literal (toast.show message strings, paragraph descriptions, Button labels, actionLabel, shortcut badge text) with vue-i18n lookups via the composition API (import/use the t function in each render/setup) and add corresponding keys to src/locales/en/main.json (e.g., keys for toast.messages.subgraphUnpacked, toast.messages.repacked, toast.messages.autoDismiss, button.show, button.dismiss, toast.undoLabel, paragraph.descriptions.*) so all user-facing strings are loaded from i18n instead of hardcoded literals.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/components/toast/SnackbarToast.vue`:
- Line 69: The computed `hasAction` currently only checks `onAction` and
`shortcut`, allowing an action button with an empty label; update the
`hasAction` logic in SnackbarToast.vue to also require a non-empty,
non-whitespace `actionLabel` (e.g., check that `actionLabel.value` exists and
trim().length > 0) alongside the existing `onAction` and `!shortcut` checks so
the action button is only shown when there is an actual label.
---
Nitpick comments:
In `@src/components/toast/SnackbarToast.stories.ts`:
- Around line 32-107: The story file contains hardcoded user-facing strings in
the story components (see Story exports WithShortcut, WithUndoAction, Persistent
and their inner setup functions trigger/dismiss using useSnackbarToast, plus
Button labels and paragraph descriptions); replace each literal (toast.show
message strings, paragraph descriptions, Button labels, actionLabel, shortcut
badge text) with vue-i18n lookups via the composition API (import/use the t
function in each render/setup) and add corresponding keys to
src/locales/en/main.json (e.g., keys for toast.messages.subgraphUnpacked,
toast.messages.repacked, toast.messages.autoDismiss, button.show,
button.dismiss, toast.undoLabel, paragraph.descriptions.*) so all user-facing
strings are loaded from i18n instead of hardcoded literals.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a2b159e8-4c72-4a91-8335-04e75dd3594a
📒 Files selected for processing (2)
src/components/toast/SnackbarToast.stories.tssrc/components/toast/SnackbarToast.vue
…ider Codex adversarial review (PR #11731) flagged the original `useSnackbarToast` for owning UI state as raw module-level refs and a manual `setTimeout`, which conflicts with the existing toast stack (PrimeVue/GlobalToast/HoneyToast) and is unsafe under HMR, multiple hosts, rapid back-to-back show() calls, and throwing action callbacks. This rewrites the component on top of Reka's `ToastProvider` / `ToastRoot` / `ToastAction` / `ToastClose` / `ToastViewport` primitives, which already handle the queue, duration, hover/focus pause, swipe dismiss, and SR announcement. State now lives in a `<SnackbarToastProvider>` component scope, not at module load. Changes: - `useSnackbarToast()` is now an inject-based hook returning `{ show, dismiss }`. Throws when no Provider is in scope. - `SnackbarToastProvider.vue` owns the toasts array, provides the API, and replaces the previous toast on rapid show() (singleton policy preserved). Renders `<ToastViewport>` at the same bottom-center position as before. - `SnackbarToast.vue` is now a single `<ToastRoot>` item renderer, driven by a typed `toast` prop. Action handler is wrapped in try/finally so a throwing callback still dismisses and is logged. - Stories wrap each variant in `<SnackbarToastProvider>` with simple trigger components. - Visuals match Figma node 6826:77784 (verified via Figma MCP). Tests: - `useSnackbarToast.test.ts` covers no-provider throw and inject contract. - `SnackbarToastProvider.test.ts` covers initial empty state, show rendering, singleton replace, shortcut badge vs action exclusivity, action click + dismiss, throwing action still dismisses, dismiss(id) targeting, and unique-id guarantee. Refs FE-484
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/components/toast/SnackbarToastProvider.test.ts (1)
54-56: Restore global spies in the suite cleanup.If the error-path test fails before Line 137,
console.errorstays mocked and can hide later failures. Addvi.restoreAllMocks()to the existingafterEach()so cleanup does not depend on the test reaching its last line.♻️ Suggested cleanup
afterEach(() => { capturedApi = null + vi.restoreAllMocks() }) @@ - errSpy.mockRestore()Also applies to: 122-137
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/toast/SnackbarToastProvider.test.ts` around lines 54 - 56, The afterEach cleanup currently only resets capturedApi to null (capturedApi) which leaves global spies (like console.error mocked in error-path tests) active if a test fails early; update the existing afterEach() to also call vi.restoreAllMocks() so all global mocks/spies are restored regardless of test outcome, ensuring tests like those that mock console.error are cleaned up even if they fail before their final assertions.src/composables/useSnackbarToast.ts (1)
4-19: Make shortcut and action states unrepresentable together.
ShowSnackbarOptionscurrently allowsshortcut,actionLabel, andonActionin the same payload even though the UI only honors one branch. A mutually exclusive union would catch those invalid calls at compile time instead of silently dropping the action.♻️ Suggested type shape
-export interface ShowSnackbarOptions { - shortcut?: string - duration?: number - actionLabel?: string - onAction?: () => void -} +interface BaseSnackbarOptions { + duration?: number +} + +interface ShortcutSnackbarOptions extends BaseSnackbarOptions { + shortcut: string + actionLabel?: never + onAction?: never +} + +interface ActionSnackbarOptions extends BaseSnackbarOptions { + shortcut?: never + actionLabel: string + onAction: () => void +} + +interface PassiveSnackbarOptions extends BaseSnackbarOptions { + shortcut?: never + actionLabel?: never + onAction?: never +} + +export type ShowSnackbarOptions = + | PassiveSnackbarOptions + | ShortcutSnackbarOptions + | ActionSnackbarOptionsAs per coding guidelines "Use TypeScript for type safety."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/composables/useSnackbarToast.ts` around lines 4 - 19, ShowSnackbarOptions currently permits shortcut and actionLabel/onAction together even though the UI only uses one branch; change ShowSnackbarOptions into a mutually exclusive union (e.g. { shortcut: string; duration?: number } | { actionLabel: string; onAction: () => void; duration?: number } | { duration?: number }) so callers cannot pass both shortcut and action fields; update SnackbarToastItem (which extends ShowSnackbarOptions) and the SnackbarToastApi.show signature to use the new union type so type-checking prevents invalid payloads.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/composables/useSnackbarToast.test.ts`:
- Around line 24-41: The test currently only inspects the shape of the injected
object; update it to assert the actual injected instance or that the provided
mocks are invoked: render Provider (which provides SnackbarToastKey with api)
and have the Consumer call useSnackbarToast() and either (a) expose the returned
object so the test can assert strict equality with the provided api, or (b) call
the returned show() and dismiss() inside the Consumer and assert that api.show
and api.dismiss (vi.fn mocks) were called; target symbols: useSnackbarToast,
SnackbarToastKey, Provider, Consumer, and the api mock (show/dismiss).
---
Nitpick comments:
In `@src/components/toast/SnackbarToastProvider.test.ts`:
- Around line 54-56: The afterEach cleanup currently only resets capturedApi to
null (capturedApi) which leaves global spies (like console.error mocked in
error-path tests) active if a test fails early; update the existing afterEach()
to also call vi.restoreAllMocks() so all global mocks/spies are restored
regardless of test outcome, ensuring tests like those that mock console.error
are cleaned up even if they fail before their final assertions.
In `@src/composables/useSnackbarToast.ts`:
- Around line 4-19: ShowSnackbarOptions currently permits shortcut and
actionLabel/onAction together even though the UI only uses one branch; change
ShowSnackbarOptions into a mutually exclusive union (e.g. { shortcut: string;
duration?: number } | { actionLabel: string; onAction: () => void; duration?:
number } | { duration?: number }) so callers cannot pass both shortcut and
action fields; update SnackbarToastItem (which extends ShowSnackbarOptions) and
the SnackbarToastApi.show signature to use the new union type so type-checking
prevents invalid payloads.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0a2c13df-d5d1-4955-aec2-91d1eb0f21d7
📒 Files selected for processing (6)
src/components/toast/SnackbarToast.stories.tssrc/components/toast/SnackbarToast.vuesrc/components/toast/SnackbarToastProvider.test.tssrc/components/toast/SnackbarToastProvider.vuesrc/composables/useSnackbarToast.test.tssrc/composables/useSnackbarToast.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/toast/SnackbarToast.vue
| it('returns the injected api', () => { | ||
| const api: SnackbarToastApi = { | ||
| show: vi.fn(() => 'id-1'), | ||
| dismiss: vi.fn() | ||
| } | ||
| const Provider = defineComponent({ | ||
| setup(_, { slots }) { | ||
| provide(SnackbarToastKey, api) | ||
| return () => slots.default?.() | ||
| } | ||
| }) | ||
|
|
||
| render(Provider, { | ||
| slots: { default: () => h(Consumer) } | ||
| }) | ||
|
|
||
| expect(screen.getByTestId('has-show').textContent).toBe('function') | ||
| expect(screen.getByTestId('has-dismiss').textContent).toBe('function') |
There was a problem hiding this comment.
Assert the injected instance, not just its shape.
This still passes if useSnackbarToast() returns a fresh { show, dismiss } object instead of the provided api. Capture the returned value and compare identity, or invoke the provided mocks through the consumer so the test fails when injection breaks.
As per coding guidelines "Do not write tests that just test the mocks; ensure tests fail when code behaves unexpectedly."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/composables/useSnackbarToast.test.ts` around lines 24 - 41, The test
currently only inspects the shape of the injected object; update it to assert
the actual injected instance or that the provided mocks are invoked: render
Provider (which provides SnackbarToastKey with api) and have the Consumer call
useSnackbarToast() and either (a) expose the returned object so the test can
assert strict equality with the provided api, or (b) call the returned show()
and dismiss() inside the Consumer and assert that api.show and api.dismiss
(vi.fn mocks) were called; target symbols: useSnackbarToast, SnackbarToastKey,
Provider, Consumer, and the api mock (show/dismiss).
|
@dante01yoon I don't have access to that figma, just assign to @DrJKL for review |
|
I don't think we should add this speculatively. |
What do you mean? |
We have an existing toast system which I'd love to replace. This adds a secondary component with no immediate usage in response to a problem that we agreed wasn't worth solving in this way. |
figma: https://www.figma.com/design/vALUV83vIdBzEsTJAhQgXq/Comfy-Design-System?node-id=6826-77784&m=dev

Summary
useSnackbarToastcomposable and Storybook stories.Background
Extracted from #11718 per Slack discussion: https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1777399633850579
The team was undecided on whether to surface a snackbar for canvas actions like link-visibility toggle / focus-mode toggle / subgraph unpack ("Sonner for basic actions like this is overkill"). Conclusion: ship the component now, defer integration until UX direction settles.
Behavior
ToolbarRoot/ToolbarButtonfor arrow-key navigation;role=status/aria-live=politefor SR announcement.Storybook
Components/Graph/SnackbarToast— Default / WithShortcut / WithUndoAction / Persistent.Visuals
Matches Figma node
6826:77784in the Comfy Design System.Test plan
onActioncallbackRefs FE-484
┆Issue is synchronized with this Notion page by Unito