@vvedantb/ui 0.3.0 → 0.3.1
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 +6 -1
- package/package.json +1 -1
- package/shell/src/SessionsHoverCard.tsx +36 -1
- package/src/ui/sonner.tsx +36 -6
package/README.md
CHANGED
|
@@ -142,7 +142,7 @@ import { SessionsHoverCard } from "@vvedantb/ui/shell";
|
|
|
142
142
|
</SessionsHoverCard>
|
|
143
143
|
```
|
|
144
144
|
|
|
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/`.
|
|
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`. 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
146
|
|
|
147
147
|
## Toaster
|
|
148
148
|
|
|
@@ -176,6 +176,11 @@ Opt-in props (all default to the current look):
|
|
|
176
176
|
| `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
177
|
| `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
178
|
| `descriptionMuted` | `false` (default), `true` | Description uses `text-muted-foreground!`, which beats sonner's dark-theme rule. |
|
|
179
|
+
| `titleLeading` | `"snug"` (default), `"5"` | Title `leading-snug` (19.25px at `text-sm`) or `leading-5` (20px). |
|
|
180
|
+
| `loadingIcon` | node (default none) | Custom loading icon, merged into `icons`. A caller `icons.loading` wins. `icons={{ loading }}` works with both `toastIcons` values. |
|
|
181
|
+
| `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. |
|
|
182
|
+
|
|
183
|
+
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
184
|
|
|
180
185
|
Helpers: `createToasterClassNames`, `mergeToastClassNames`, `mergeToastOptions`, `mergeToasterStyle`, `filledToastIcons`. Fixture: `tokens/examples/toaster/`.
|
|
181
186
|
|
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}
|
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,
|