@rtzll/reflow 0.4.0 → 0.6.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.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `@rtzll/reflow/highlight`: Shiki behind one function, for CodeBlock.
3
+ *
4
+ * This entry is optional. It needs the `shiki` and `@shikijs/transformers`
5
+ * peer dependencies, and nothing else in Reflow imports it, so an app that
6
+ * never imports it never installs or loads Shiki.
7
+ */
8
+ import { type BundledTheme, type ShikiTransformer, type ThemeRegistrationAny } from 'shiki';
9
+ import type { HighlightedCode } from './code-block.tsx';
10
+ export type { CodeLine, CodeToken, HighlightedCode } from './code-block.tsx';
11
+ type Theme = BundledTheme | ThemeRegistrationAny;
12
+ export interface HighlightOptions {
13
+ /** Any language Shiki bundles, or one of its aliases. Unknown languages render as plain text. Default text. */
14
+ lang?: string;
15
+ /** A Shiki theme, by name or definition. Defaults to Reflow's, which colours tokens with `--rf-code-token-*` variables. */
16
+ theme?: Theme;
17
+ /** A light and a dark theme; the block follows the page's color scheme between them. */
18
+ themes?: {
19
+ light: Theme;
20
+ dark: Theme;
21
+ };
22
+ /**
23
+ * Reads `// [!code highlight]`, `[!code ++]`, `[!code --]`, `[!code focus]`,
24
+ * `[!code error]`, `[!code warning]` and `[!code word:name]` comments into
25
+ * line and word marks, and removes them from the code. Default true.
26
+ */
27
+ notation?: boolean;
28
+ /** More Shiki transformers, run after the notation ones. */
29
+ transformers?: ShikiTransformer[];
30
+ }
31
+ /**
32
+ * Highlights code for CodeBlock. Call it where the code is known: in a route
33
+ * handler, so the page arrives highlighted and the browser loads nothing, or
34
+ * in the browser for code that arrives later, such as a streamed reply.
35
+ */
36
+ export declare function highlight(code: string, options?: HighlightOptions): Promise<HighlightedCode>;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * `@rtzll/reflow/highlight`: Shiki behind one function, for CodeBlock.
3
+ *
4
+ * This entry is optional. It needs the `shiki` and `@shikijs/transformers`
5
+ * peer dependencies, and nothing else in Reflow imports it, so an app that
6
+ * never imports it never installs or loads Shiki.
7
+ */
8
+ import { bundledLanguages, bundledLanguagesAlias, createCssVariablesTheme, createHighlighter, createJavaScriptRegexEngine, } from 'shiki';
9
+ import { transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight, } from '@shikijs/transformers';
10
+ // Reflow's theme: every colour is a CSS variable, set in code-block.css for
11
+ // the light and dark themes, so highlighted code follows the page.
12
+ const reflowTheme = createCssVariablesTheme({
13
+ name: 'reflow',
14
+ variablePrefix: '--rf-code-',
15
+ fontStyle: true,
16
+ });
17
+ let shared;
18
+ // One highlighter for the process or page, with the JavaScript regex engine,
19
+ // which needs no WebAssembly. Languages and themes load as they are first used.
20
+ function highlighter() {
21
+ shared ??= createHighlighter({
22
+ themes: [reflowTheme],
23
+ langs: [],
24
+ engine: createJavaScriptRegexEngine({ forgiving: true }),
25
+ });
26
+ return shared;
27
+ }
28
+ function bundled(lang) {
29
+ return lang in bundledLanguages || lang in bundledLanguagesAlias;
30
+ }
31
+ function themeName(theme) {
32
+ return typeof theme === 'string' ? theme : (theme.name ?? 'custom');
33
+ }
34
+ function classes(node) {
35
+ const value = node.properties?.class ?? node.properties?.className;
36
+ return Array.isArray(value) ? value.join(' ') : typeof value === 'string' ? value : '';
37
+ }
38
+ // Flattens a line into runs of text, each with the innermost style and
39
+ // every class around it, such as a token inside a highlighted word.
40
+ function tokens(node, style = '', classList = []) {
41
+ if (node.type === 'text') {
42
+ if (!node.value)
43
+ return [];
44
+ const token = { content: node.value };
45
+ if (style)
46
+ token.style = style;
47
+ if (classList.length)
48
+ token.class = classList.join(' ');
49
+ return [token];
50
+ }
51
+ const own = typeof node.properties?.style === 'string' ? node.properties.style : '';
52
+ const name = classes(node);
53
+ const nested = name ? [...classList, name] : classList;
54
+ return (node.children ?? []).flatMap((child) => tokens(child, own || style, nested));
55
+ }
56
+ function trimEnd(list) {
57
+ while (list.length) {
58
+ const last = list[list.length - 1];
59
+ last.content = last.content.replace(/[ \t]+$/, '');
60
+ if (last.content)
61
+ return;
62
+ list.pop();
63
+ }
64
+ }
65
+ /**
66
+ * Highlights code for CodeBlock. Call it where the code is known: in a route
67
+ * handler, so the page arrives highlighted and the browser loads nothing, or
68
+ * in the browser for code that arrives later, such as a streamed reply.
69
+ */
70
+ export async function highlight(code, options = {}) {
71
+ const instance = await highlighter();
72
+ const requested = options.lang ?? 'text';
73
+ const lang = bundled(requested) ? requested : 'text';
74
+ if (lang !== 'text' && !instance.getLoadedLanguages().includes(lang))
75
+ await instance.loadLanguage(lang);
76
+ const themes = options.themes ? [options.themes.light, options.themes.dark] : [options.theme];
77
+ for (const theme of themes)
78
+ if (theme && !instance.getLoadedThemes().includes(themeName(theme)))
79
+ await instance.loadTheme(theme);
80
+ const transformers = [
81
+ ...(options.notation === false
82
+ ? []
83
+ : [
84
+ transformerNotationHighlight(),
85
+ transformerNotationDiff(),
86
+ transformerNotationFocus(),
87
+ transformerNotationErrorLevel(),
88
+ transformerNotationWordHighlight(),
89
+ ]),
90
+ ...(options.transformers ?? []),
91
+ ];
92
+ const root = instance.codeToHast(code, options.themes
93
+ ? {
94
+ lang,
95
+ themes: {
96
+ light: themeName(options.themes.light),
97
+ dark: themeName(options.themes.dark),
98
+ },
99
+ defaultColor: false,
100
+ transformers,
101
+ }
102
+ : { lang, theme: options.theme ? themeName(options.theme) : 'reflow', transformers });
103
+ const pre = root.children?.find((node) => node.tagName === 'pre');
104
+ const body = pre?.children?.find((node) => node.tagName === 'code');
105
+ const lines = (body?.children ?? [])
106
+ .filter((node) => node.type === 'element')
107
+ .map((node) => {
108
+ const mark = classes(node)
109
+ .split(/\s+/)
110
+ .filter((name) => name && name !== 'line')
111
+ .join(' ');
112
+ const line = { tokens: tokens({ ...node, properties: {} }) };
113
+ // Removing a notation comment leaves the space before it.
114
+ if (options.notation !== false)
115
+ trimEnd(line.tokens);
116
+ if (mark)
117
+ line.class = mark;
118
+ return line;
119
+ });
120
+ const flags = classes(pre ?? { type: 'element' })
121
+ .split(/\s+/)
122
+ .filter((name) => name.startsWith('has-'));
123
+ const style = typeof pre?.properties?.style === 'string' ? pre.properties.style : undefined;
124
+ const result = {
125
+ lang: requested === 'text' || lang === 'text' ? 'text' : requested,
126
+ code: lines.map((line) => line.tokens.map((token) => token.content).join('')).join('\n'),
127
+ lines,
128
+ flags,
129
+ };
130
+ // Reflow's own theme takes its colours from CSS; others bring theirs inline.
131
+ if (style && (options.theme || options.themes))
132
+ result.style = style;
133
+ return result;
134
+ }
@@ -28,5 +28,9 @@ export interface HoverCardProps {
28
28
  * follows it, and the card never takes focus or locks scrolling. The pointer
29
29
  * can cross into the card and use its links; it closes once the pointer has
30
30
  * left both, or on Escape. It shares Popover's surface and motion.
31
+ *
32
+ * The card is a span, so a trigger can sit inside a paragraph. Keep its
33
+ * content to phrasing elements there, such as span and a, since a div or a
34
+ * list would end the paragraph when the browser parses server-rendered HTML.
31
35
  */
32
36
  export declare function HoverCard(handle: Handle<HoverCardProps>): () => import("remix/component").RemixElement;
@@ -8,6 +8,10 @@ import { anchor } from '@remix-run/ui/anchor';
8
8
  * follows it, and the card never takes focus or locks scrolling. The pointer
9
9
  * can cross into the card and use its links; it closes once the pointer has
10
10
  * left both, or on Escape. It shares Popover's surface and motion.
11
+ *
12
+ * The card is a span, so a trigger can sit inside a paragraph. Keep its
13
+ * content to phrasing elements there, such as span and a, since a div or a
14
+ * list would end the paragraph when the browser parses server-rendered HTML.
11
15
  */
12
16
  export function HoverCard(handle) {
13
17
  let trigger = null;
@@ -114,6 +118,6 @@ export function HoverCard(handle) {
114
118
  });
115
119
  return () => {
116
120
  handle.queueTask(sync);
117
- return (_jsxs(_Fragment, { children: [handle.props.renderTrigger({ mix: triggerMix }), _jsx("div", { class: "rf-menu-surface rf-popover-surface rf-hover-card", popover: "manual", "data-density": handle.props.size, mix: cardMix, children: handle.props.children })] }));
121
+ return (_jsxs(_Fragment, { children: [handle.props.renderTrigger({ mix: triggerMix }), _jsx("span", { class: "rf-menu-surface rf-popover-surface rf-hover-card", popover: "manual", "data-density": handle.props.size, mix: cardMix, children: handle.props.children })] }));
118
122
  };
119
123
  }
package/dist/index.d.ts CHANGED
@@ -13,6 +13,12 @@ export * from './breadcrumb.tsx';
13
13
  export * from './segmented-control.tsx';
14
14
  export * from './popover.tsx';
15
15
  export * from './hover-card.tsx';
16
+ export * from './toaster.tsx';
17
+ export * from './drawer.tsx';
18
+ export * from './code-block.tsx';
19
+ export * from './citation.tsx';
20
+ export * from './avatar.tsx';
21
+ export * from './chat-layout.tsx';
16
22
  export * from './systems.ts';
17
23
  export * from './queued-stack.tsx';
18
24
  export * from './scroll-area.tsx';
package/dist/index.js CHANGED
@@ -13,6 +13,12 @@ export * from './breadcrumb.js';
13
13
  export * from './segmented-control.js';
14
14
  export * from './popover.js';
15
15
  export * from './hover-card.js';
16
+ export * from './toaster.js';
17
+ export * from './drawer.js';
18
+ export * from './code-block.js';
19
+ export * from './citation.js';
20
+ export * from './avatar.js';
21
+ export * from './chat-layout.js';
16
22
  export * from './systems.js';
17
23
  export * from './queued-stack.js';
18
24
  export * from './scroll-area.js';
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,10 @@
12
12
  @import './breadcrumb.css';
13
13
  @import './segmented-control.css';
14
14
  @import './popover.css';
15
+ @import './toaster.css';
16
+ @import './drawer.css';
17
+ @import './code-block.css';
18
+ @import './citation.css';
19
+ @import './avatar.css';
20
+ @import './chat-layout.css';
15
21
  @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
+ }