diff --git a/apps/docs/src/routes/docs/components/toast-sileo.tsx b/apps/docs/src/routes/docs/components/toast-sileo.tsx index 52fe85b..b71bb8c 100644 --- a/apps/docs/src/routes/docs/components/toast-sileo.tsx +++ b/apps/docs/src/routes/docs/components/toast-sileo.tsx @@ -1,59 +1,69 @@ import { Button } from "@solidcn/core"; -import { sileo } from "@solidcn/toast"; +import { SileoToaster, sileo } from "@solidcn/toast"; import { DocPage } from "../../../components/ui/DocPage.js"; function SileoDemo() { return ( -
- - - - - - -
+ <> + +
+ + + + + + +
+ ); } @@ -66,87 +76,145 @@ export default function ToastSileoPage() { phase="Toast" componentName="toast" manualInstall="npm install @solidcn/toast" - usage={`import { sileo } from "@solidcn/toast"; + usage={`import { SileoToaster, sileo } from "@solidcn/toast"; + +// Mount once in this page/layout + // Trigger a physics-based toast: -sileo.success({ title: "Saved!" }); -sileo.error({ title: "Failed." }); -sileo.warning({ title: "Heads up." }); -sileo.info({ title: "FYI." }); +sileo.success({ title: "Changes saved" }); + +sileo.error({ + title: "Something went wrong", + description: "Please try again later.", + duration: 6000, +}); + +sileo.warning({ title: "Storage almost full", duration: 6000 }); + +sileo.info({ title: "New update available", duration: 6000 }); // With options: sileo.success({ title: "Event created", description: "Sunday, December 03 at 9:00 AM", preset: "glass", + duration: 6000, animation: "spring", });`} examples={[ { title: "Variants", description: - 'Click to trigger physics-based toasts. Requires in your layout.', + "Click any button to trigger Sileo toasts on this page, including description cards. Each toast stays open briefly before auto-closing.", preview: , - code: `import { sileo } from "@solidcn/toast" -import { Button } from "~/components/ui/button" + code: `import { SileoToaster, sileo } from "@solidcn/toast" +import { Button } from "@solidcn/core" export function ToastSileoVariants() { return ( -
- - - - - - -
+ <> + +
+ + + + + + +
+ ) }`, }, ]} notes={
+
+

Standalone toast item

+

+ Use SileoToast when you want to + render a single toast card from your own state instead of the global store. +

+
+
{`import { SileoToast } from "@solidcn/toast";
+
+ {
+    // remove the toast from your own state
+  }}
+/>
+`}
+
+
+

Setup

- Add {''}{" "} - once in your app root: + Add{" "} + {''}{" "} + where you want Sileo toasts to appear:

-
{`import { Toaster } from "@solidcn/toast";
+              
{`import { SileoToaster } from "@solidcn/toast";
 
-// In your App component:
-`}
+// In your page/layout: +`}
diff --git a/biome.json b/biome.json index 6ec08c4..89e8ed4 100644 --- a/biome.json +++ b/biome.json @@ -44,6 +44,7 @@ ".pnpm-store", "dist", ".turbo", + "packages/mcp-cloudflare/.wrangler", "apps/docs/.solid", "apps/docs/.output", "apps/docs/.vinxi", diff --git a/packages/toast/README.md b/packages/toast/README.md index bdd8ab0..b8cab66 100644 --- a/packages/toast/README.md +++ b/packages/toast/README.md @@ -68,6 +68,24 @@ export default function App() { } ``` +If you need to render a single toast card manually, you can also use the standalone `SileoToast` component: + +```tsx +import { SileoToast } from "@solidcn/toast"; + + { + /* remove the toast from your own state */ + }} +/> +``` + ```ts // Sileo toast types sileo.success({ title: "Success", description? }); @@ -96,7 +114,7 @@ toast.promise(submitForm(), { ```tsx interface StandardToasterProps { - position?: "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right"; + position?: "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "top-center"; mode?: "standard"; theme?: "light" | "dark" | "system"; richColors?: boolean; @@ -111,7 +129,7 @@ interface StandardToasterProps { | Prop | Type | Default | Description | |---|---|---|---| -| `position` | See above | `"bottom-right"` | Toast placement | +| `position` | See above | `"top-center"` | Toast placement | | `theme` | `"light" \| "dark" \| "system"` | `"system"` | Toast theme override | | `richColors` | `boolean` | `false` | When enabled, toast color reflects type (success=green, error=red, etc.) | | `closeButton` | `boolean` | `false` | Show dismiss button on each toast | @@ -141,7 +159,7 @@ interface SileoToasterProps { ### ToastPosition (Sileo) -`"top-left" | "top-right" | "bottom-left" | "bottom-right" | "center"` +`"top-left" | "top-right" | "bottom-left" | "top-center" | "center"` ## Toast Options @@ -217,13 +235,14 @@ sileo.dismiss(); export { Toaster, toast, sileo }; // Mode-specific -export { StandardToaster, SileoToaster }; +export { StandardToaster, SileoToaster, SileoToast }; // Types export type { ToasterProps, StandardToasterProps, SileoToasterProps, + SileoToastProps, StandardToastOptions, SileoToastOptions, SileoPreset, diff --git a/packages/toast/src/index.tsx b/packages/toast/src/index.tsx index 3404d89..ef70680 100644 --- a/packages/toast/src/index.tsx +++ b/packages/toast/src/index.tsx @@ -16,6 +16,7 @@ export { toast } from "./standard/store.js"; export { sileo } from "./sileo/store.js"; export { StandardToaster } from "./standard/toaster.js"; export { SileoToaster } from "./sileo/toaster.js"; +export { SileoToast } from "./sileo/sileo.js"; // Types export type { @@ -24,6 +25,7 @@ export type { SileoToasterProps, StandardToastOptions, SileoToastOptions, + SileoToastProps, SileoPreset, SileoStyles, ToastPosition, diff --git a/packages/toast/src/sileo/index.ts b/packages/toast/src/sileo/index.ts index 714f416..1765245 100644 --- a/packages/toast/src/sileo/index.ts +++ b/packages/toast/src/sileo/index.ts @@ -1,5 +1,5 @@ export { sileo, sileoStore } from "./store.js"; export { SileoToaster } from "./toaster.js"; -export { SileoItem } from "./sileo.js"; +export { SileoToast } from "./sileo.js"; export { SILEO_PRESETS, resolveStyles } from "./presets.js"; export { SPRING_CONFIGS, springTick, isSettled, getAnimationClass } from "./animations.js"; diff --git a/packages/toast/src/sileo/sileo.tsx b/packages/toast/src/sileo/sileo.tsx index c374488..97664a0 100644 --- a/packages/toast/src/sileo/sileo.tsx +++ b/packages/toast/src/sileo/sileo.tsx @@ -1,5 +1,5 @@ import { type Component, Show, createEffect, createSignal, onCleanup, onMount } from "solid-js"; -import type { SileoPreset, SileoToastItem, ToastAnimation } from "../types.js"; +import type { SileoToastProps, ToastAnimation } from "../types.js"; import { SPRING_CONFIGS, isSettled, springTick } from "./animations.js"; import { resolveStyles } from "./presets.js"; import { sileo } from "./store.js"; @@ -9,14 +9,10 @@ function prefersReducedMotion(): boolean { return window.matchMedia("(prefers-reduced-motion: reduce)").matches; } -interface SileoItemProps { - toast: SileoToastItem; - preset: SileoPreset; - globalAnimation: ToastAnimation; -} +export const SileoToast: Component = (props) => { + const getStartOffset = () => (props.position?.startsWith("top") ? -40 : 40); -export const SileoItem: Component = (props) => { - const [translateY, setTranslateY] = createSignal(40); + const [translateY, setTranslateY] = createSignal(getStartOffset()); const [opacity, setOpacity] = createSignal(0); const [scale, setScale] = createSignal(0.92); @@ -24,15 +20,21 @@ export const SileoItem: Component = (props) => { let timer: ReturnType | undefined; const styles = () => - resolveStyles(props.toast.preset ?? props.preset, props.toast.type, props.toast.styles); + resolveStyles( + props.toast.preset ?? props.preset ?? "default", + props.toast.type, + props.toast.styles, + ); const animation = () => { // prefers-reduced-motion: auto-fallback ke "none" if (prefersReducedMotion()) return "none" as ToastAnimation; - return props.toast.animation ?? props.globalAnimation; + return props.toast.animation ?? props.animation ?? "spring"; }; onMount(() => { + const startOffset = getStartOffset(); + if (animation() === "none") { setTranslateY(0); setOpacity(1); @@ -42,6 +44,7 @@ export const SileoItem: Component = (props) => { // fade: CSS opacity transition via rAF double-frame trick if (animation() === "fade") { + setTranslateY(startOffset); requestAnimationFrame(() => { requestAnimationFrame(() => { setOpacity(1); @@ -54,6 +57,7 @@ export const SileoItem: Component = (props) => { // slide: CSS translateY transition via rAF double-frame trick if (animation() === "slide") { + setTranslateY(startOffset); requestAnimationFrame(() => { requestAnimationFrame(() => { setTranslateY(0); @@ -66,7 +70,7 @@ export const SileoItem: Component = (props) => { const config = SPRING_CONFIGS[animation()] ?? SPRING_CONFIGS.spring ?? { stiffness: 280, damping: 20, mass: 1 }; - let yState = { value: 40, velocity: 0 }; + let yState = { value: startOffset, velocity: 0 }; let opState = { value: 0, velocity: 0 }; let scaleState = { value: 0.92, velocity: 0 }; let lastTime: number | null = null; @@ -103,8 +107,12 @@ export const SileoItem: Component = (props) => { const ms = duration ?? 4000; timer = setTimeout(() => { - props.toast.onDismiss?.(props.toast.id); - sileo.dismiss(props.toast.id); + if (props.onDismiss) { + props.onDismiss(props.toast.id); + } else { + props.toast.onDismiss?.(props.toast.id); + sileo.dismiss(props.toast.id); + } }, ms); }); @@ -126,6 +134,7 @@ export const SileoItem: Component = (props) => {
= (props) => { "backdrop-filter": isGlass() ? "blur(12px)" : undefined, "-webkit-backdrop-filter": isGlass() ? "blur(12px)" : undefined, }} - class="relative flex w-full items-start gap-3 rounded-2xl border p-4 shadow-lg" > @@ -173,7 +181,14 @@ export const SileoItem: Component = (props) => { type="button" style={{ color: styles().descriptionColor }} class="shrink-0 rounded-full p-0.5 opacity-60 hover:opacity-100 transition-opacity focus:outline-none focus:ring-2" - onClick={() => sileo.dismiss(props.toast.id)} + onClick={() => { + if (props.onDismiss) { + props.onDismiss(props.toast.id); + } else { + props.toast.onDismiss?.(props.toast.id); + sileo.dismiss(props.toast.id); + } + }} aria-label="Dismiss" > = { "top-left": "top-0 left-0", @@ -10,11 +10,10 @@ const positionClasses: Record = { "top-right": "top-0 right-0", "bottom-left": "bottom-0 left-0", "bottom-center": "bottom-0 left-1/2 -translate-x-1/2", - "bottom-right": "bottom-0 right-0", }; export const SileoToaster: Component = (props) => { - const position = () => props.position ?? "bottom-right"; + const position = () => props.position ?? "top-center"; const maxToasts = () => props.maxToasts ?? 5; const offsetX = () => props.offset?.x ?? 16; const offsetY = () => props.offset?.y ?? 16; @@ -26,7 +25,7 @@ export const SileoToaster: Component = (props) => { return (
    = (props) => { {(t) => (
  1. - +
  2. )}
    diff --git a/packages/toast/src/standard/toaster.tsx b/packages/toast/src/standard/toaster.tsx index 8cb7010..2e2edcc 100644 --- a/packages/toast/src/standard/toaster.tsx +++ b/packages/toast/src/standard/toaster.tsx @@ -25,7 +25,7 @@ const STACK_SCALE_STEP = 0.05; const MAX_STACK_VISIBLE = 3; export const StandardToaster: Component = (props) => { - const position = () => props.position ?? "bottom-right"; + const position = () => props.position ?? "top-center"; const maxToasts = () => props.maxToasts ?? 5; const offsetX = () => props.offset?.x ?? 16; const offsetY = () => props.offset?.y ?? 16; @@ -38,7 +38,7 @@ export const StandardToaster: Component = (props) => { const themeClass = () => resolveThemeClass(props.theme); const isBottom = () => - position() === "bottom-left" || position() === "bottom-center" || position() === "bottom-right"; + position() === "bottom-left" || position() === "bottom-center" || position() === "top-center"; // most recent toast is last in array → front of stack const visibleToasts = () => { diff --git a/packages/toast/src/types.ts b/packages/toast/src/types.ts index 20de3de..d7cb65b 100644 --- a/packages/toast/src/types.ts +++ b/packages/toast/src/types.ts @@ -6,7 +6,7 @@ export type ToastPosition = | "top-right" | "bottom-left" | "bottom-center" - | "bottom-right"; + | "top-center"; export type ToastType = "success" | "error" | "warning" | "info" | "loading" | "default"; @@ -94,6 +94,15 @@ export interface SileoToastItem extends SileoToastOptions { createdAt: number; } +export interface SileoToastProps { + toast: SileoToastItem; + position?: ToastPosition; + preset?: SileoPreset; + animation?: ToastAnimation; + onDismiss?: (id: string) => void; + class?: string; +} + export interface SileoToasterProps { position?: ToastPosition; mode: "sileo";