@vvedantb/ui 0.3.0 → 0.3.2
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/README.md +11 -4
- package/package.json +1 -1
- package/shell/src/SessionsHoverCard.tsx +36 -1
- package/src/composer/Composer.tsx +52 -7
- package/src/composer/index.ts +1 -0
- package/src/ui/sonner.tsx +36 -6
package/README.md
CHANGED
|
@@ -59,8 +59,10 @@ import { Composer, ComposerIconButton } from "@vvedantb/ui"; // or "@vvedantb/ui
|
|
|
59
59
|
| Prop | Default | Notes |
|
|
60
60
|
| --- | --- | --- |
|
|
61
61
|
| `value`, `defaultValue`, `onChange` | uncontrolled, `""` | Pass `value` to control it. When controlled, clear the value yourself after `onSubmit`. When uncontrolled, the composer clears after submit. |
|
|
62
|
-
| `onSubmit(text)` | none | Called with the trimmed draft on Enter or the send button.
|
|
63
|
-
| `streaming`, `onStop` | `false` | The stop button
|
|
62
|
+
| `onSubmit(text)` | none | Called with the trimmed draft on Enter or the send button. By default it is not called for an empty draft, or when `disabled` or (with `stopMode="replace"`) `streaming`. A `canSend` override that allows an empty draft calls it with `""`. If it returns a rejected promise, an uncontrolled composer restores the draft. |
|
|
63
|
+
| `streaming`, `onStop` | `false` | The stop button calls `onStop`. With `stopMode="replace"` it replaces send and Enter does not submit. Typing stays on. |
|
|
64
|
+
| `stopMode` | `"replace"` | `"alongside"` shows stop and send together while streaming, in the order `trailingActions`, stop, send. Send stays usable to queue the next message: Enter and the send button submit while streaming when `canSend` allows. Not streaming, only send shows. Predictions stay off while streaming. |
|
|
65
|
+
| `canSend` | trimmed draft is not empty | `boolean` or `(draft: string) => boolean`. Overrides whether send is enabled. The function gets the live draft (not trimmed). `disabled` and, in replace mode, `streaming` still block send. With attachments, use `canSend={(draft) => draft.trim() !== "" \|\| hasAttachments}`. `onSubmit` then gets `""` for an empty draft. |
|
|
64
66
|
| `disabled` | `false` | Disables the textarea and send. |
|
|
65
67
|
| `placeholder` | `"Message"` | Also the textarea `aria-label`. |
|
|
66
68
|
| `layout` | `"stacked"` | `"stacked"`: input above, footer bar with `plusMenu` left and `trailingActions` plus send or stop right. `"inline"`: one row with `plusMenu`, the input and the actions. Attachments stay above the row. |
|
|
@@ -124,7 +126,7 @@ function MentionComposer({ draft, setDraft, send }: Props) {
|
|
|
124
126
|
}
|
|
125
127
|
```
|
|
126
128
|
|
|
127
|
-
`ComposerShell` props: `layout`, `disabled`, `streaming`, `canSend`, `onSend`, `onStop`, `plusMenu`, `trailingActions`, `attachments`, `className` and `children` (the input region). In the shell the Enter, Tab and Esc keys are yours to wire when you do not use `Composer`.
|
|
129
|
+
`ComposerShell` props: `layout`, `disabled`, `streaming`, `canSend` (always a boolean: `Composer` resolves the function form), `stopMode` (`"replace"` or `"alongside"`, where the buttons render), `onSend`, `onStop`, `plusMenu`, `trailingActions`, `attachments`, `className` and `children` (the input region). In the shell the Enter, Tab and Esc keys are yours to wire when you do not use `Composer`.
|
|
128
130
|
|
|
129
131
|
Fixture: `composer/examples/` (see its README). It has a `tokens.css` page and a `compat-shadcn-rgb.css` page, each with light and dark.
|
|
130
132
|
|
|
@@ -142,7 +144,7 @@ import { SessionsHoverCard } from "@vvedantb/ui/shell";
|
|
|
142
144
|
</SessionsHoverCard>
|
|
143
145
|
```
|
|
144
146
|
|
|
145
|
-
`children` is the trigger. It is rendered with `asChild` by default, so it keeps its own handlers (such as a context menu). Extra props and the ref go to it. Use `asChild={false}` for a default trigger element. The content portals to `document.body` by default, so `overflow:hidden` ancestors do not clip it. Pass `portal={false}` to opt out. `surface` is `"overlay"` (default), `"surface"` (`rounded-surface`) or `"glass"` (`glass-panel-strong`, `rounded-surface`). `className` merges over the surface without `!`. Defaults: `openDelay` 400 ms, `closeDelay` 150 ms, `side="right"`, `align="start"`, `sideOffset` 8, width and padding `w-64 p-3` (override with `className`). `body` is a node or a function of `{ open }`. `loadContent` runs only while the card is open. Control the state with `open`, `defaultOpen` and `onOpenChange`. Fixture: `shell/examples/sessions-hover-card/`.
|
|
147
|
+
`children` is the trigger. It is rendered with `asChild` by default, so it keeps its own handlers (such as a context menu). Extra props and the ref go to it. Use `asChild={false}` for a default trigger element. The content portals to `document.body` by default, so `overflow:hidden` ancestors do not clip it. Pass `portal={false}` to opt out. `surface` is `"overlay"` (default), `"surface"` (`rounded-surface`) or `"glass"` (`glass-panel-strong`, `rounded-surface`). `className` merges over the surface without `!`. Defaults: `openDelay` 400 ms, `closeDelay` 150 ms, `side="right"`, `align="start"`, `sideOffset` 8, width and padding `w-64 p-3` (override with `className`). `body` is a node or a function of `{ open }`. `loadContent` runs only while the card is open. Control the state with `open`, `defaultOpen` and `onOpenChange`. The card opens on pointer hover and on keyboard focus (`:focus-visible`) only. Focus that follows a click or a context menu close does not open it. If pointer-driven focus left it stuck open, pointer leave force-closes it; otherwise Radix handles close (including moving onto the content). Fixture: `shell/examples/sessions-hover-card/`.
|
|
146
148
|
|
|
147
149
|
## Toaster
|
|
148
150
|
|
|
@@ -176,6 +178,11 @@ Opt-in props (all default to the current look):
|
|
|
176
178
|
| `closeButtonStyle` | `"default"`, `"hoverChip"` | Text-style close button or a round chip at the top-right, shown on hover and always on small screens. It turns on `closeButton` unless you set it. |
|
|
177
179
|
| `sonnerVars` | `false` (default), `true` | Sets `--normal-bg`, `--normal-border`, `--normal-text`, `--normal-bg-hover` and `--normal-border-hover` from `--color-card`, `--color-border`, `--color-foreground` and `--color-muted`, and `font-family: inherit`. |
|
|
178
180
|
| `descriptionMuted` | `false` (default), `true` | Description uses `text-muted-foreground!`, which beats sonner's dark-theme rule. |
|
|
181
|
+
| `titleLeading` | `"snug"` (default), `"5"` | Title `leading-snug` (19.25px at `text-sm`) or `leading-5` (20px). |
|
|
182
|
+
| `loadingIcon` | node (default none) | Custom loading icon, merged into `icons`. A caller `icons.loading` wins. `icons={{ loading }}` works with both `toastIcons` values. |
|
|
183
|
+
| `hideStackedContent` | `false` (default), `true` | Adds `[&[data-expanded=false][data-front=false]>*]:opacity-0` to the toast, so content behind the front toast is hidden while the stack is collapsed. `sonner.css` applies the same rule when imported. |
|
|
184
|
+
|
|
185
|
+
With `closeButtonStyle="hoverChip"`, hover changes the text colour, and sonner may still change the background. The border stays `border-border` (pinned with `hover:border-border!` over sonner's dark hover border), so the ring does not brighten in dark mode.
|
|
179
186
|
|
|
180
187
|
Helpers: `createToasterClassNames`, `mergeToastClassNames`, `mergeToastOptions`, `mergeToasterStyle`, `filledToastIcons`. Fixture: `tokens/examples/toaster/`.
|
|
181
188
|
|
package/package.json
CHANGED
|
@@ -70,6 +70,15 @@ export type SessionsHoverCardProps = Omit<
|
|
|
70
70
|
className?: string;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
/** `:focus-visible` is true for keyboard focus only. Treat unsupported engines as keyboard. */
|
|
74
|
+
function isFocusVisible(el: Element): boolean {
|
|
75
|
+
try {
|
|
76
|
+
return el.matches(":focus-visible");
|
|
77
|
+
} catch {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
type LoadState = { status: "loading" } | { status: "ready"; node: ReactNode };
|
|
74
83
|
|
|
75
84
|
/** Mounts only while the card is open, so `loadContent` never runs while closed. */
|
|
@@ -140,6 +149,8 @@ export const SessionsHoverCard = forwardRef<
|
|
|
140
149
|
align = "start",
|
|
141
150
|
sideOffset = 8,
|
|
142
151
|
className,
|
|
152
|
+
onFocus,
|
|
153
|
+
onPointerLeave,
|
|
143
154
|
...triggerProps
|
|
144
155
|
}, ref) {
|
|
145
156
|
const [openState, setOpenState] = useState(defaultOpen);
|
|
@@ -150,6 +161,24 @@ export const SessionsHoverCard = forwardRef<
|
|
|
150
161
|
onOpenChange?.(next);
|
|
151
162
|
};
|
|
152
163
|
|
|
164
|
+
// Open on keyboard focus only. Focus that returns after a context menu or a
|
|
165
|
+
// click is pointer-driven, so it must not open the card.
|
|
166
|
+
const handleFocus = (event: React.FocusEvent<HTMLElement>) => {
|
|
167
|
+
onFocus?.(event as never);
|
|
168
|
+
if (event.defaultPrevented) return;
|
|
169
|
+
if (!isFocusVisible(event.currentTarget)) event.preventDefault();
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Radix handles hover close, including closeDelay and moving onto the
|
|
173
|
+
// content. Force-close only when the card is stuck open by pointer-driven
|
|
174
|
+
// focus on the trigger (focused, but not `:focus-visible`).
|
|
175
|
+
const handlePointerLeave = (event: React.PointerEvent<HTMLElement>) => {
|
|
176
|
+
onPointerLeave?.(event as never);
|
|
177
|
+
if (event.pointerType === "touch" || !open) return;
|
|
178
|
+
const el = event.currentTarget;
|
|
179
|
+
if (el.ownerDocument.activeElement === el && !isFocusVisible(el)) handleOpenChange(false);
|
|
180
|
+
};
|
|
181
|
+
|
|
153
182
|
const bodyNode = loadContent ? (
|
|
154
183
|
<LoadedBody loadContent={loadContent} fallback={loadingFallback} />
|
|
155
184
|
) : typeof body === "function" ? (
|
|
@@ -190,7 +219,13 @@ export const SessionsHoverCard = forwardRef<
|
|
|
190
219
|
openDelay={openDelay}
|
|
191
220
|
closeDelay={closeDelay}
|
|
192
221
|
>
|
|
193
|
-
<HoverCardTrigger
|
|
222
|
+
<HoverCardTrigger
|
|
223
|
+
ref={ref}
|
|
224
|
+
asChild={asChild}
|
|
225
|
+
{...triggerProps}
|
|
226
|
+
onFocus={handleFocus}
|
|
227
|
+
onPointerLeave={handlePointerLeave}
|
|
228
|
+
>
|
|
194
229
|
{children}
|
|
195
230
|
</HoverCardTrigger>
|
|
196
231
|
{portal ? <HoverCardPrimitive.Portal>{content}</HoverCardPrimitive.Portal> : content}
|
|
@@ -15,6 +15,12 @@ import { useComposerPrediction } from "./useComposerPrediction";
|
|
|
15
15
|
export type ComposerSlotContext = { disabled: boolean };
|
|
16
16
|
export type ComposerTrailingContext = { disabled: boolean; streaming: boolean };
|
|
17
17
|
export type ComposerLayout = "stacked" | "inline";
|
|
18
|
+
/**
|
|
19
|
+
* How stop behaves while `streaming`. `"replace"` (default): stop takes the
|
|
20
|
+
* place of send. `"alongside"`: stop and send show together, in the order
|
|
21
|
+
* trailing actions, stop, send.
|
|
22
|
+
*/
|
|
23
|
+
export type ComposerStopMode = "replace" | "alongside";
|
|
18
24
|
|
|
19
25
|
export type ComposerInputRenderContext = {
|
|
20
26
|
value: string;
|
|
@@ -44,13 +50,30 @@ export type ComposerProps = {
|
|
|
44
50
|
defaultValue?: string;
|
|
45
51
|
onChange?: (value: string) => void;
|
|
46
52
|
|
|
47
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Called with the trimmed draft on Enter or the send button. By default it
|
|
55
|
+
* is not called for an empty draft. A `canSend` override that allows an
|
|
56
|
+
* empty draft calls it with `""`.
|
|
57
|
+
*/
|
|
48
58
|
onSubmit?: (value: string) => void | Promise<void>;
|
|
49
59
|
onStop?: () => void;
|
|
50
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Shows the stop button. With `stopMode="replace"` (default) it takes the
|
|
62
|
+
* place of send and Enter does not submit. Typing stays enabled.
|
|
63
|
+
*/
|
|
51
64
|
streaming?: boolean;
|
|
52
|
-
/** Disables the textarea and the send button. */
|
|
65
|
+
/** Disables the textarea and the send button. `canSend` cannot override it. */
|
|
53
66
|
disabled?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Overrides whether send is enabled. Default: the trimmed draft is not
|
|
69
|
+
* empty. `disabled` and, with `stopMode="replace"`, `streaming` still block
|
|
70
|
+
* send. A function receives the live draft (not trimmed), so check
|
|
71
|
+
* `draft.trim()` yourself. Use it to allow an empty draft when attachments
|
|
72
|
+
* exist: `canSend={(draft) => draft.trim() !== "" || hasAttachments}`.
|
|
73
|
+
*/
|
|
74
|
+
canSend?: boolean | ((draft: string) => boolean);
|
|
75
|
+
/** `"replace"` (default) or `"alongside"`. See `ComposerStopMode`. */
|
|
76
|
+
stopMode?: ComposerStopMode;
|
|
54
77
|
|
|
55
78
|
placeholder?: string;
|
|
56
79
|
/** Maximum textarea height in px. The textarea scrolls after it. Default 200. */
|
|
@@ -124,8 +147,10 @@ export type ComposerShellProps = {
|
|
|
124
147
|
layout?: ComposerLayout;
|
|
125
148
|
disabled?: boolean;
|
|
126
149
|
streaming?: boolean;
|
|
127
|
-
/** Enables the send button. */
|
|
150
|
+
/** Enables the send button. Always a boolean: `Composer` resolves its function form. */
|
|
128
151
|
canSend?: boolean;
|
|
152
|
+
/** `"replace"` (default) or `"alongside"`. Alongside while streaming: trailing actions, stop, send. */
|
|
153
|
+
stopMode?: ComposerStopMode;
|
|
129
154
|
onSend?: () => void;
|
|
130
155
|
onStop?: () => void;
|
|
131
156
|
plusMenu?: SlotNode<ComposerSlotContext>;
|
|
@@ -147,6 +172,7 @@ function ComposerShell({
|
|
|
147
172
|
disabled = false,
|
|
148
173
|
streaming = false,
|
|
149
174
|
canSend = false,
|
|
175
|
+
stopMode = "replace",
|
|
150
176
|
onSend,
|
|
151
177
|
onStop,
|
|
152
178
|
plusMenu,
|
|
@@ -162,7 +188,7 @@ function ComposerShell({
|
|
|
162
188
|
: trailingActions;
|
|
163
189
|
const inline = layout === "inline";
|
|
164
190
|
|
|
165
|
-
const
|
|
191
|
+
const stopButton = (
|
|
166
192
|
<ComposerIconButton
|
|
167
193
|
variant="default"
|
|
168
194
|
aria-label="Stop generating"
|
|
@@ -171,7 +197,8 @@ function ComposerShell({
|
|
|
171
197
|
>
|
|
172
198
|
<IconPlayerStop className="size-4" aria-hidden="true" />
|
|
173
199
|
</ComposerIconButton>
|
|
174
|
-
)
|
|
200
|
+
);
|
|
201
|
+
const sendButton = (
|
|
175
202
|
<ComposerIconButton
|
|
176
203
|
variant="default"
|
|
177
204
|
aria-label="Send message"
|
|
@@ -182,6 +209,16 @@ function ComposerShell({
|
|
|
182
209
|
<IconArrowUp className="size-4" aria-hidden="true" />
|
|
183
210
|
</ComposerIconButton>
|
|
184
211
|
);
|
|
212
|
+
const action = !streaming ? (
|
|
213
|
+
sendButton
|
|
214
|
+
) : stopMode === "alongside" ? (
|
|
215
|
+
<>
|
|
216
|
+
{stopButton}
|
|
217
|
+
{sendButton}
|
|
218
|
+
</>
|
|
219
|
+
) : (
|
|
220
|
+
stopButton
|
|
221
|
+
);
|
|
185
222
|
|
|
186
223
|
return (
|
|
187
224
|
<div
|
|
@@ -189,6 +226,7 @@ function ComposerShell({
|
|
|
189
226
|
data-layout={layout}
|
|
190
227
|
data-disabled={disabled || undefined}
|
|
191
228
|
data-streaming={streaming || undefined}
|
|
229
|
+
data-stop-mode={stopMode}
|
|
192
230
|
className={cn(
|
|
193
231
|
"group/composer relative flex w-full min-w-0 flex-col rounded-field border border-border bg-field-background text-sm transition-[border-color,box-shadow] duration-200 ease-smooth",
|
|
194
232
|
"has-[[data-composer-input]:focus-visible]:border-focus-border has-[[data-composer-input]:focus-visible]:ring-2 has-[[data-composer-input]:focus-visible]:ring-focus-ring",
|
|
@@ -274,6 +312,8 @@ const Composer = React.forwardRef<HTMLTextAreaElement, ComposerProps>(function C
|
|
|
274
312
|
onStop,
|
|
275
313
|
streaming = false,
|
|
276
314
|
disabled = false,
|
|
315
|
+
canSend: canSendProp,
|
|
316
|
+
stopMode = "replace",
|
|
277
317
|
placeholder = "Message",
|
|
278
318
|
maxHeight = DEFAULT_MAX_HEIGHT,
|
|
279
319
|
autoFocus,
|
|
@@ -343,7 +383,11 @@ const Composer = React.forwardRef<HTMLTextAreaElement, ComposerProps>(function C
|
|
|
343
383
|
};
|
|
344
384
|
React.useLayoutEffect(syncScroll, [value, ghost]);
|
|
345
385
|
|
|
346
|
-
const
|
|
386
|
+
const override = typeof canSendProp === "function" ? canSendProp(value) : canSendProp;
|
|
387
|
+
const canSend =
|
|
388
|
+
!disabled &&
|
|
389
|
+
(stopMode === "alongside" || !streaming) &&
|
|
390
|
+
(override ?? value.trim() !== "");
|
|
347
391
|
|
|
348
392
|
const submit = () => {
|
|
349
393
|
if (!canSend) return;
|
|
@@ -454,6 +498,7 @@ const Composer = React.forwardRef<HTMLTextAreaElement, ComposerProps>(function C
|
|
|
454
498
|
disabled={disabled}
|
|
455
499
|
streaming={streaming}
|
|
456
500
|
canSend={canSend}
|
|
501
|
+
stopMode={stopMode}
|
|
457
502
|
onSend={submit}
|
|
458
503
|
onStop={onStop}
|
|
459
504
|
plusMenu={plusMenu}
|
package/src/composer/index.ts
CHANGED
package/src/ui/sonner.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CSSProperties } from "react";
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
2
|
import { Toaster, type ToasterProps } from "sonner";
|
|
3
3
|
import { filledToastIcons } from "./sonner-icons";
|
|
4
4
|
import "./sonner.css";
|
|
@@ -45,10 +45,22 @@ type SonnerToasterOptions = {
|
|
|
45
45
|
sonnerVars?: boolean;
|
|
46
46
|
/** When true, descriptions use `text-muted-foreground!` so sonner's dark rule cannot win. */
|
|
47
47
|
descriptionMuted?: boolean;
|
|
48
|
+
/** `"snug"` (default) keeps `leading-snug`. `"5"` uses `leading-5` (20px at `text-sm`). */
|
|
49
|
+
titleLeading?: "snug" | "5";
|
|
50
|
+
/** Custom loading icon. Merged into `icons`. A caller `icons.loading` wins. */
|
|
51
|
+
loadingIcon?: ReactNode;
|
|
52
|
+
/**
|
|
53
|
+
* When true, adds a class that hides the content of toasts behind the front toast
|
|
54
|
+
* while the stack is collapsed. Default false. `sonner.css` has the same rule.
|
|
55
|
+
*/
|
|
56
|
+
hideStackedContent?: boolean;
|
|
48
57
|
};
|
|
49
58
|
|
|
59
|
+
const hideStackedContentClassName =
|
|
60
|
+
"[&[data-expanded=false][data-front=false]>*]:opacity-0";
|
|
61
|
+
|
|
50
62
|
const hoverChipCloseButtonClassName =
|
|
51
|
-
"pointer-events-none absolute -right-2 -top-2 z-10 flex h-6 w-6 items-center justify-center rounded-full border border-border bg-card/95 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring group-hover:pointer-events-auto group-hover:opacity-100 max-sm:pointer-events-auto max-sm:opacity-100 max-sm:hit-target";
|
|
63
|
+
"pointer-events-none absolute -right-2 -top-2 z-10 flex h-6 w-6 items-center justify-center rounded-full border border-border bg-card/95 text-muted-foreground opacity-0 transition-opacity hover:border-border! hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring group-hover:pointer-events-auto group-hover:opacity-100 max-sm:pointer-events-auto max-sm:opacity-100 max-sm:hit-target";
|
|
52
64
|
|
|
53
65
|
const defaultCloseButtonClassName =
|
|
54
66
|
"rounded-lg bg-default px-2.5 py-1 text-xs font-medium text-foreground transition-colors hover:bg-default/78";
|
|
@@ -65,16 +77,23 @@ function createToasterClassNames({
|
|
|
65
77
|
density = "default",
|
|
66
78
|
closeButtonStyle = "default",
|
|
67
79
|
descriptionMuted = false,
|
|
80
|
+
titleLeading = "snug",
|
|
81
|
+
hideStackedContent = false,
|
|
68
82
|
}: Pick<
|
|
69
83
|
SonnerToasterOptions,
|
|
70
|
-
|
|
84
|
+
| "toastRadius"
|
|
85
|
+
| "density"
|
|
86
|
+
| "closeButtonStyle"
|
|
87
|
+
| "descriptionMuted"
|
|
88
|
+
| "titleLeading"
|
|
89
|
+
| "hideStackedContent"
|
|
71
90
|
> = {}): ToastClassNames {
|
|
72
91
|
const radius = toastRadiusClasses[toastRadius];
|
|
73
92
|
const compact = density === "compact";
|
|
74
93
|
const descriptionSize = compact ? "text-xs" : "text-[13px]";
|
|
75
94
|
return {
|
|
76
|
-
toast: `group toast glass-panel-strong flex w-full items-center ${compact ? "gap-2" : "gap-3"} ${radius} ${compact ? "px-3 py-2 text-xs" : "px-4 py-3 text-sm"} text-foreground smooth-shadow-ring-md`,
|
|
77
|
-
title: "font-medium leading-snug",
|
|
95
|
+
toast: `group toast glass-panel-strong flex w-full items-center ${compact ? "gap-2" : "gap-3"} ${radius} ${compact ? "px-3 py-2 text-xs" : "px-4 py-3 text-sm"} text-foreground smooth-shadow-ring-md${hideStackedContent ? ` ${hideStackedContentClassName}` : ""}`,
|
|
96
|
+
title: titleLeading === "5" ? "font-medium leading-5" : "font-medium leading-snug",
|
|
78
97
|
description: descriptionMuted
|
|
79
98
|
? `${descriptionSize} leading-snug text-muted-foreground!`
|
|
80
99
|
: `${descriptionSize} leading-snug text-muted-foreground`,
|
|
@@ -148,6 +167,9 @@ function SonnerToaster({
|
|
|
148
167
|
closeButtonStyle = "default",
|
|
149
168
|
sonnerVars = false,
|
|
150
169
|
descriptionMuted,
|
|
170
|
+
titleLeading,
|
|
171
|
+
loadingIcon,
|
|
172
|
+
hideStackedContent,
|
|
151
173
|
toastOptions,
|
|
152
174
|
icons,
|
|
153
175
|
style,
|
|
@@ -155,8 +177,14 @@ function SonnerToaster({
|
|
|
155
177
|
closeButton,
|
|
156
178
|
...props
|
|
157
179
|
}: ToasterProps & SonnerToasterOptions) {
|
|
180
|
+
const loadingOverride =
|
|
181
|
+
loadingIcon != null ? { loading: loadingIcon } : undefined;
|
|
158
182
|
const mergedIcons =
|
|
159
|
-
toastIcons === "filled24"
|
|
183
|
+
toastIcons === "filled24"
|
|
184
|
+
? { ...filledToastIcons, ...loadingOverride, ...icons }
|
|
185
|
+
: loadingOverride
|
|
186
|
+
? { ...loadingOverride, ...icons }
|
|
187
|
+
: icons;
|
|
160
188
|
return (
|
|
161
189
|
<Toaster
|
|
162
190
|
expand={false}
|
|
@@ -175,6 +203,8 @@ function SonnerToaster({
|
|
|
175
203
|
density,
|
|
176
204
|
closeButtonStyle,
|
|
177
205
|
descriptionMuted,
|
|
206
|
+
titleLeading,
|
|
207
|
+
hideStackedContent,
|
|
178
208
|
}),
|
|
179
209
|
},
|
|
180
210
|
toastOptions,
|