@rtzll/reflow 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sidebar.css CHANGED
@@ -926,68 +926,27 @@
926
926
  margin-inline-start: 8px;
927
927
  }
928
928
 
929
- /* Mobile drawer: below 768px the rail becomes a sheet that slides in from the left. */
929
+ /* Mobile drawer: below 768px the rail moves into a left Drawer, which brings
930
+ the slide, the drag to close and the backdrop; it keeps the rail's width,
931
+ surface and shadow. */
930
932
  .rf-sidebar-drawer-trigger {
931
933
  display: none;
932
934
  }
933
- .rf-sidebar-drawer {
934
- position: fixed;
935
- inset-block: 0;
936
- inset-inline-start: 0;
937
- box-sizing: border-box;
938
- width: min(288px, 100vw);
939
- max-width: none;
940
- height: 100dvh;
941
- max-height: none;
942
- margin: 0;
943
- padding: 0;
935
+ .rf-drawer.rf-sidebar-drawer[data-side] {
936
+ --rf-drawer-bg: var(--rf-surface-2);
937
+ inline-size: min(288px, 100vw);
944
938
  overflow: hidden;
945
- border: 0;
946
- background: var(--rf-surface-2);
947
- box-shadow: var(--rf-sb-surface-shadow);
948
- color: var(--rf-fg);
949
- translate: -100% 0;
950
- transition:
951
- translate var(--rf-exit-moderate),
952
- display var(--rf-exit-moderate) allow-discrete,
953
- overlay var(--rf-exit-moderate) allow-discrete;
954
- }
955
- .rf-sidebar-drawer[open] {
956
- translate: 0 0;
957
- transition:
958
- translate var(--rf-spring-moderate),
959
- display var(--rf-spring-moderate) allow-discrete,
960
- overlay var(--rf-spring-moderate) allow-discrete;
961
- }
962
- @starting-style {
963
- .rf-sidebar-drawer[open] {
964
- translate: -100% 0;
965
- }
966
- }
967
- .rf-sidebar-drawer::backdrop {
968
- background: light-dark(rgb(0 0 0 / 40%), rgb(0 0 0 / 80%));
969
- opacity: 0;
970
- transition:
971
- opacity var(--rf-exit-moderate),
972
- display var(--rf-exit-moderate) allow-discrete,
973
- overlay var(--rf-exit-moderate) allow-discrete;
974
- }
975
- .rf-sidebar-drawer[open]::backdrop {
976
- opacity: 1;
977
- transition:
978
- opacity 160ms ease,
979
- display 160ms allow-discrete,
980
- overlay 160ms allow-discrete;
939
+ box-shadow: var(--rf-drawer-tail), var(--rf-sb-surface-shadow);
981
940
  }
982
- @starting-style {
983
- .rf-sidebar-drawer[open]::backdrop {
984
- opacity: 0;
985
- }
941
+ .rf-sidebar-drawer .rf-drawer-body {
942
+ display: flex;
943
+ flex-direction: column;
986
944
  }
987
- .rf-sidebar-drawer-content {
988
- height: 100%;
945
+ .rf-sidebar-drawer .rf-sidebar-inner {
946
+ flex: 1;
947
+ min-block-size: 0;
989
948
  }
990
- .rf-sidebar-drawer-content > .rf-basic-sr-only:focus-visible {
949
+ .rf-sidebar-drawer .rf-drawer-body > .rf-basic-sr-only:focus-visible {
991
950
  clip-path: none;
992
951
  width: auto;
993
952
  height: auto;
@@ -996,8 +955,6 @@
996
955
  position: static;
997
956
  }
998
957
  @media (prefers-reduced-motion: reduce) {
999
- .rf-sidebar-drawer,
1000
- .rf-sidebar-drawer::backdrop,
1001
958
  .rf-sidebar-layout,
1002
959
  .rf-sidebar-card {
1003
960
  transition-duration: 0s !important;
package/dist/sidebar.js CHANGED
@@ -4,6 +4,7 @@ import { on, ref } from 'remix/component';
4
4
  import { Button, Card, CardGroup, CardHeader, CardTitle, CardDescription, CardMedia, CardImage, } from './basic.js';
5
5
  import { fluidHover } from './interactive.js';
6
6
  import { ScrollArea } from './scroll-area.js';
7
+ import { Drawer } from './drawer.js';
7
8
  /** How far past the minimum width a drag has to travel before it previews the collapse. */
8
9
  const collapseSlop = 56;
9
10
  function SidebarNavigation(handle) {
@@ -113,13 +114,14 @@ export function Sidebar(handle) {
113
114
  let exitingPeek;
114
115
  let peekTimer;
115
116
  let drag;
116
- let drawer;
117
+ let drawerOpen = false;
117
118
  const panelId = `${handle.id}-sidebar`;
118
- const drawerId = `${panelId}-drawer`;
119
- const captureDrawer = ref((node) => {
120
- if (node instanceof HTMLDialogElement)
121
- drawer = node;
122
- });
119
+ const setDrawer = (open) => {
120
+ if (drawerOpen === open)
121
+ return;
122
+ drawerOpen = open;
123
+ void handle.update();
124
+ };
123
125
  function limits() {
124
126
  const minimum = handle.props.minWidth ?? 160;
125
127
  return { minimum, maximum: Math.max(minimum, handle.props.maxWidth ?? 360) };
@@ -375,13 +377,7 @@ export function Sidebar(handle) {
375
377
  if (!peeking)
376
378
  cancelPeekTimer();
377
379
  }),
378
- ], children: _jsx(Icon, { name: "panel-left" }) }), _jsx(Button, { class: "rf-sidebar-drawer-trigger", variant: "ghost", size: "compact", "aria-controls": drawerId, "aria-haspopup": "dialog", mix: on('click', () => drawer?.showModal()), children: "Open navigation" })] })), toolbar != null && _jsx("div", { class: "rf-sidebar-toolbar-content", children: toolbar })] })), children] }), _jsx("dialog", { id: drawerId, class: "rf-sidebar-drawer", "aria-label": label, mix: [
379
- captureDrawer,
380
- on('click', (event) => {
381
- if (event.target === drawer)
382
- drawer?.close();
383
- }),
384
- ], children: _jsxs("div", { class: "rf-sidebar-drawer-content", children: [_jsx("button", { type: "button", class: "rf-basic-sr-only", mix: on('click', () => drawer?.close()), children: "Close navigation" }), _jsx(SidebarNavigation, { ...navigation, variant: "default", onNavigate: () => drawer?.close() })] }) })] }));
380
+ ], children: _jsx(Icon, { name: "panel-left" }) }), _jsxs(Drawer, { side: "left", title: label, flush: true, class: "rf-sidebar-drawer", open: drawerOpen, onOpenChange: setDrawer, renderTrigger: (trigger) => (_jsx(Button, { class: "rf-sidebar-drawer-trigger", variant: "ghost", size: "compact", ...trigger, children: "Open navigation" })), children: [_jsx("button", { type: "button", class: "rf-basic-sr-only", mix: on('click', () => setDrawer(false)), children: "Close navigation" }), _jsx(SidebarNavigation, { ...navigation, variant: "default", onNavigate: () => setDrawer(false) })] })] })), toolbar != null && _jsx("div", { class: "rf-sidebar-toolbar-content", children: toolbar })] })), children] })] }));
385
381
  };
386
382
  }
387
383
  /** A measured, dismissible pile of real Cards. Keyboard focus opens the inline pile too. */
@@ -0,0 +1,3 @@
1
+ /** The status glyphs Banner and toasts share. Internal: not exported from the package. */
2
+ export type StatusIconName = 'default' | 'info' | 'success' | 'warning' | 'error';
3
+ export declare function statusIcon(status: StatusIconName): import("remix/component").RemixElement;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "remix/component/jsx-runtime";
2
+ export function statusIcon(status) {
3
+ if (status === 'default') {
4
+ return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M12 16v-4m0-4h.01" })] }));
5
+ }
6
+ return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", children: [status === 'warning' ? (_jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z", fill: "currentColor", stroke: "currentColor", "stroke-width": "1.5", "stroke-linejoin": "round" })) : (_jsx("circle", { cx: "12", cy: "12", r: "10.5", fill: "currentColor" })), _jsx("g", { stroke: "var(--rf-bg)", "stroke-width": "2.25", "stroke-linecap": "round", "stroke-linejoin": "round", children: status === 'success' ? (_jsx("path", { d: "m8.5 12.25 2.5 2.5 4.5-5" })) : status === 'warning' ? (_jsx("path", { d: "M12 9.5v3.5M12 17h.01" })) : status === 'error' ? (_jsx("path", { d: "M12 7.5v5M12 16.5h.01" })) : (_jsx("path", { d: "M12 16.5v-5M12 7.5h.01" })) })] }));
7
+ }
package/dist/styles.css CHANGED
@@ -12,4 +12,6 @@
12
12
  @import './breadcrumb.css';
13
13
  @import './segmented-control.css';
14
14
  @import './popover.css';
15
+ @import './toaster.css';
16
+ @import './drawer.css';
15
17
  @import './systems.css';
@@ -0,0 +1,268 @@
1
+ /* Toaster: a pile of toasts fixed to one corner or edge of the viewport.
2
+ The newest toast sits in front; the ones behind peek out and narrow on
3
+ QueuedStack's stacking values. Pointed at or focused, the pile spreads
4
+ out a gap apart. The script writes each toast's depth, height and spread
5
+ offset; the lift turns offsets upward for the bottom positions. */
6
+ .rf-toaster-region {
7
+ display: block;
8
+ outline: none;
9
+ }
10
+
11
+ .rf-toaster {
12
+ --rf-toaster-offset: 24px;
13
+ --rf-toast-width: 356px;
14
+ --rf-toast-lift: -1;
15
+ position: fixed;
16
+ z-index: 2147483000;
17
+ box-sizing: border-box;
18
+ inline-size: var(--rf-toast-width);
19
+ max-inline-size: calc(100vw - 2 * var(--rf-toaster-offset));
20
+ block-size: var(--rf-toaster-piled-height, 0px);
21
+ margin: 0;
22
+ padding: 0;
23
+ list-style: none;
24
+ color: var(--rf-fg);
25
+ font: var(--rf-text-body) / var(--rf-leading-body) var(--rf-font);
26
+ -webkit-font-smoothing: antialiased;
27
+ }
28
+ .rf-toaster[data-expanded] {
29
+ block-size: var(--rf-toaster-spread-height, 0px);
30
+ }
31
+ .rf-toaster[data-position^='top'] {
32
+ --rf-toast-lift: 1;
33
+ top: var(--rf-toaster-offset);
34
+ }
35
+ .rf-toaster[data-position^='bottom'] {
36
+ bottom: var(--rf-toaster-offset);
37
+ }
38
+ .rf-toaster[data-position$='left'] {
39
+ left: var(--rf-toaster-offset);
40
+ }
41
+ .rf-toaster[data-position$='right'] {
42
+ right: var(--rf-toaster-offset);
43
+ }
44
+ .rf-toaster[data-position$='center'] {
45
+ left: 50%;
46
+ translate: -50% 0;
47
+ }
48
+ @media (max-width: 600px) {
49
+ .rf-toaster {
50
+ --rf-toaster-offset: 16px;
51
+ inset-inline: var(--rf-toaster-offset);
52
+ inline-size: auto;
53
+ translate: none;
54
+ }
55
+ }
56
+
57
+ .rf-toast {
58
+ --rf-status-color: var(--rf-fg);
59
+ --rf-toast-y: calc(var(--rf-toast-lift) * var(--rf-toast-depth, 0) * var(--rf-stack-peek));
60
+ position: absolute;
61
+ inset-inline: 0;
62
+ z-index: calc(100 - var(--rf-toast-depth, 0));
63
+ box-sizing: border-box;
64
+ block-size: var(--rf-toast-front-height, auto);
65
+ overflow: hidden;
66
+ border-radius: var(--rf-panel-radius);
67
+ outline: none;
68
+ background: var(--rf-surface-3, white);
69
+ box-shadow:
70
+ inset 0 1px light-dark(transparent, rgb(255 255 255 / 2%)),
71
+ inset 0 0 0 1px light-dark(transparent, rgb(255 255 255 / 2%)),
72
+ 0 0 0 1px light-dark(rgb(0 0 0 / 6%), rgb(0 0 0 / 12%)),
73
+ 0 1px 1px -0.5px light-dark(rgb(0 0 0 / 6%), rgb(0 0 0 / 18%)),
74
+ 0 3px 3px -1.5px light-dark(rgb(0 0 0 / 6%), rgb(0 0 0 / 18%)),
75
+ 0 12px 28px light-dark(rgb(0 0 0 / 10%), rgb(0 0 0 / 24%));
76
+ translate: var(--rf-swipe-x, 0px) calc(var(--rf-toast-y) + var(--rf-swipe-y, 0px));
77
+ scale: calc(1 - var(--rf-toast-depth, 0) * var(--rf-stack-scale-step)) 1;
78
+ touch-action: none;
79
+ transition:
80
+ translate var(--rf-spring-moderate),
81
+ scale var(--rf-spring-moderate),
82
+ block-size var(--rf-spring-moderate),
83
+ opacity var(--rf-spring-moderate);
84
+ }
85
+ .rf-toaster[data-position^='top'] > .rf-toast {
86
+ top: 0;
87
+ }
88
+ .rf-toaster[data-position^='bottom'] > .rf-toast {
89
+ bottom: 0;
90
+ }
91
+ .rf-toast[data-front] {
92
+ block-size: var(--rf-toast-height, auto);
93
+ }
94
+ .rf-toast:focus-visible {
95
+ outline: 2px solid var(--rf-accent);
96
+ outline-offset: 2px;
97
+ }
98
+ /* A new toast slides in from the edge. */
99
+ @starting-style {
100
+ .rf-toast {
101
+ --rf-toast-y: calc(var(--rf-toast-lift) * -100%);
102
+ opacity: 0;
103
+ }
104
+ }
105
+
106
+ .rf-toaster[data-expanded] > .rf-toast {
107
+ --rf-toast-y: calc(var(--rf-toast-lift) * var(--rf-toast-offset, 0px));
108
+ block-size: var(--rf-toast-height, auto);
109
+ scale: 1;
110
+ }
111
+ .rf-toast[data-hidden] {
112
+ visibility: hidden;
113
+ opacity: 0;
114
+ pointer-events: none;
115
+ }
116
+ .rf-toast[data-swiping] {
117
+ cursor: grabbing;
118
+ user-select: none;
119
+ transition: none;
120
+ }
121
+ /* Leaving, the front toast drops back to its edge; a swiped one carries on
122
+ the way it was thrown; the others fade where they are. */
123
+ .rf-toast[data-removed] {
124
+ opacity: 0;
125
+ pointer-events: none;
126
+ transition:
127
+ translate var(--rf-exit-slow),
128
+ scale var(--rf-exit-slow),
129
+ opacity var(--rf-exit-moderate);
130
+ }
131
+ .rf-toast[data-removed][data-front]:not([data-swipe-out]) {
132
+ --rf-toast-y: calc(var(--rf-toast-lift) * -100%);
133
+ }
134
+
135
+ .rf-toast-inner {
136
+ display: flex;
137
+ align-items: flex-start;
138
+ gap: 12px;
139
+ padding: 16px;
140
+ transition: opacity var(--rf-spring-moderate);
141
+ }
142
+ /* Piled, only the front toast shows its content. */
143
+ .rf-toaster:not([data-expanded]) > .rf-toast:not([data-front]) > .rf-toast-inner {
144
+ opacity: 0;
145
+ }
146
+ .rf-toast[data-status='info'] {
147
+ --rf-status-color: light-dark(#3b82f6, var(--rf-info));
148
+ }
149
+ .rf-toast[data-status='success'] {
150
+ --rf-status-color: light-dark(#22c55e, var(--rf-success));
151
+ }
152
+ .rf-toast[data-status='warning'] {
153
+ --rf-status-color: light-dark(#f59e0b, var(--rf-warning));
154
+ }
155
+ .rf-toast[data-status='error'] {
156
+ --rf-status-color: light-dark(#ef4444, var(--rf-danger));
157
+ }
158
+ .rf-toast[data-status='loading'] {
159
+ --rf-status-color: var(--rf-muted);
160
+ }
161
+ .rf-toast-icon {
162
+ display: grid;
163
+ flex: none;
164
+ place-items: center;
165
+ inline-size: 20px;
166
+ block-size: var(--rf-leading-subtitle);
167
+ color: var(--rf-status-color);
168
+ }
169
+ .rf-toast-icon > svg {
170
+ inline-size: 20px;
171
+ block-size: 20px;
172
+ }
173
+ .rf-toast-spinner g {
174
+ transform-box: fill-box;
175
+ transform-origin: center;
176
+ animation: rf-button-spinner-turn 1s cubic-bezier(0.65, 0, 0.35, 1) infinite;
177
+ }
178
+ .rf-toast-spinner circle {
179
+ stroke-dasharray: 0.01 49.99;
180
+ stroke-dashoffset: -25;
181
+ animation: rf-button-spinner-breathe 1s cubic-bezier(0.65, 0, 0.35, 1) infinite;
182
+ }
183
+ .rf-toast-content {
184
+ display: flex;
185
+ flex: 1;
186
+ flex-direction: column;
187
+ gap: 2px;
188
+ min-inline-size: 0;
189
+ }
190
+ .rf-toast-title {
191
+ font-size: var(--rf-text-subtitle);
192
+ line-height: var(--rf-leading-subtitle);
193
+ font-variation-settings: var(--rf-weight-strong);
194
+ overflow-wrap: anywhere;
195
+ }
196
+ .rf-toast-description {
197
+ color: var(--rf-muted);
198
+ overflow-wrap: anywhere;
199
+ }
200
+ .rf-toast-action {
201
+ flex: none;
202
+ margin-block: calc((var(--rf-leading-subtitle) - 28px) / 2);
203
+ }
204
+ .rf-toast-close {
205
+ display: flex;
206
+ flex: none;
207
+ align-items: center;
208
+ justify-content: center;
209
+ box-sizing: border-box;
210
+ inline-size: 28px;
211
+ block-size: 28px;
212
+ margin-block: calc((var(--rf-leading-subtitle) - 28px) / 2);
213
+ margin-inline: -4px -6px;
214
+ padding: 0;
215
+ border: 0;
216
+ border-radius: var(--rf-radius);
217
+ outline: none;
218
+ background: transparent;
219
+ color: var(--rf-muted);
220
+ cursor: pointer;
221
+ transition:
222
+ color var(--rf-fast),
223
+ background-color var(--rf-fast);
224
+ }
225
+ .rf-toast-close:focus-visible {
226
+ box-shadow: 0 0 0 1px var(--rf-accent);
227
+ }
228
+ @media (hover: hover) {
229
+ .rf-toast-close:hover {
230
+ background: var(--rf-hover);
231
+ color: var(--rf-fg);
232
+ }
233
+ }
234
+ .rf-toast-close > svg {
235
+ inline-size: 15px;
236
+ block-size: 15px;
237
+ }
238
+
239
+ [data-density='compact'] .rf-toast-inner {
240
+ gap: 10px;
241
+ padding: 12px;
242
+ }
243
+ [data-density='compact'] .rf-toast-icon,
244
+ [data-density='compact'] .rf-toast-icon > svg {
245
+ inline-size: 16px;
246
+ }
247
+ [data-density='compact'] .rf-toast-icon > svg {
248
+ block-size: 16px;
249
+ }
250
+ [data-density='compact'] .rf-toast-close > svg {
251
+ inline-size: 13px;
252
+ block-size: 13px;
253
+ }
254
+
255
+ @media (prefers-reduced-motion: reduce) {
256
+ .rf-toast,
257
+ .rf-toast[data-removed] {
258
+ transition: opacity var(--rf-spring-moderate);
259
+ }
260
+ .rf-toast-spinner :is(g, circle) {
261
+ animation: none;
262
+ }
263
+ }
264
+ @media (forced-colors: active) {
265
+ .rf-toast {
266
+ border: 1px solid CanvasText;
267
+ }
268
+ }
@@ -0,0 +1,79 @@
1
+ import { type Handle, type RemixNode } from 'remix/component';
2
+ export type ToastStatus = 'default' | 'info' | 'success' | 'warning' | 'error' | 'loading';
3
+ export type ToasterPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
4
+ export interface ToastAction {
5
+ label: string;
6
+ /** Runs on click. The toast then closes, unless the handler calls `event.preventDefault()`. */
7
+ onClick: (event: MouseEvent) => void;
8
+ }
9
+ export interface ToastOptions {
10
+ /** Reuse an id to update a toast in place instead of adding another. */
11
+ id?: string;
12
+ description?: RemixNode;
13
+ /** Milliseconds before it closes on its own. `Infinity` keeps it until dismissed. Defaults to the Toaster's duration. */
14
+ duration?: number;
15
+ action?: ToastAction;
16
+ /** Whether a swipe, Escape or the close button can close it. Default true. */
17
+ dismissible?: boolean;
18
+ /** Replaces the status icon. */
19
+ icon?: RemixNode;
20
+ /** Called when the toast is closed by a swipe, Escape, the close button, an action or `toast.dismiss`. */
21
+ onDismiss?: (toast: Toast) => void;
22
+ /** Called when the toast closes on its own after its duration. */
23
+ onAutoClose?: (toast: Toast) => void;
24
+ }
25
+ export interface Toast extends ToastOptions {
26
+ id: string;
27
+ title: RemixNode;
28
+ status: ToastStatus;
29
+ }
30
+ export interface ToastPromiseMessages<T> {
31
+ loading: RemixNode;
32
+ success: RemixNode | ((value: T) => RemixNode);
33
+ error: RemixNode | ((error: unknown) => RemixNode);
34
+ description?: RemixNode;
35
+ }
36
+ /**
37
+ * Raises a toast in the app's `<Toaster>` and returns its id. `toast.success`,
38
+ * `toast.info`, `toast.warning`, `toast.error` and `toast.loading` set its
39
+ * status; `toast.promise` follows a promise from loading to its outcome;
40
+ * `toast.dismiss` closes one toast, or all of them.
41
+ */
42
+ export declare const toast: ((title: RemixNode, options?: ToastOptions) => string) & {
43
+ success: (title: RemixNode, options?: ToastOptions) => string;
44
+ info: (title: RemixNode, options?: ToastOptions) => string;
45
+ warning: (title: RemixNode, options?: ToastOptions) => string;
46
+ error: (title: RemixNode, options?: ToastOptions) => string;
47
+ loading: (title: RemixNode, options?: ToastOptions) => string;
48
+ promise<T>(promise: Promise<T> | (() => Promise<T>), messages: ToastPromiseMessages<T>, options?: ToastOptions): string;
49
+ dismiss: (id?: string) => void;
50
+ };
51
+ export interface ToasterProps {
52
+ /** Where the stack sits. Default bottom-right. */
53
+ position?: ToasterPosition;
54
+ /** Show the stack spread out instead of piled. Default false. */
55
+ expand?: boolean;
56
+ /** How many toasts stay in view; older ones wait behind them. Default 3. */
57
+ visibleToasts?: number;
58
+ /** Milliseconds each toast stays before closing on its own. Default 4000. */
59
+ duration?: number;
60
+ /** Give every dismissible toast a close button. Default false. */
61
+ closeButton?: boolean;
62
+ /** Moves focus to the newest toast. Default alt+t. */
63
+ hotkey?: string;
64
+ /** Names the notifications region. Default "Notifications". */
65
+ label?: string;
66
+ /** Distance from the viewport's edges in pixels. Default 24, or 16 on narrow screens. */
67
+ offset?: number;
68
+ size?: 'default' | 'compact';
69
+ class?: string;
70
+ }
71
+ /**
72
+ * Shows the toasts raised with `toast()`. Place one in the app, outside
73
+ * anything that scrolls. The newest toast sits in front with up to two
74
+ * peeking behind it, on QueuedStack's stacking values; pointing at or
75
+ * focusing the stack spreads it out and pauses the timers. A swipe toward
76
+ * the nearest edge, Escape, or the close button dismisses a toast, and the
77
+ * hotkey moves focus to the newest one.
78
+ */
79
+ export declare function Toaster(handle: Handle<ToasterProps>): () => import("remix/component").RemixElement;