@waveso/ui 0.7.2 → 0.7.5

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/toast.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"toast.js","names":["ToastPrimitive"],"sources":["../src/toast.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Toast as ToastPrimitive } from \"@base-ui/react/toast\"\nimport { cva } from \"class-variance-authority\"\n\nimport { cn } from \"./lib/utils\"\nimport {\n CloseIcon,\n SuccessCircleIcon,\n InfoCircleIcon,\n AlertTriangleIcon,\n ErrorCircleIcon,\n LoaderIcon,\n} from \"./lib/internal-icons\"\n\n// ---------------------------------------------------------------------------\n// Styles\n// ---------------------------------------------------------------------------\n\nconst toastRootClass =\n \"bg-elevated text-contrast ring-contrast/10 rounded-md p-4 text-sm shadow-md ring-1 outline-none select-none\"\n\nconst toastIconVariants = cva(\n \"mt-0.5 size-4 shrink-0\",\n {\n variants: {\n type: {\n loading: \"text-muted animate-spin\",\n success: \"text-success\",\n info: \"text-info\",\n warning: \"text-warning\",\n error: \"text-destructive\",\n },\n },\n }\n)\n\nconst toastViewportVariants = cva(\n \"fixed z-[100] flex w-full outline-none sm:max-w-sm\",\n {\n variants: {\n position: {\n \"top-left\": \"top-4 left-4\",\n \"top-center\": \"top-4 right-0 left-0 mx-auto\",\n \"top-right\": \"top-4 right-4\",\n \"bottom-left\": \"bottom-4 left-4\",\n \"bottom-center\": \"bottom-4 right-0 left-0 mx-auto\",\n \"bottom-right\": \"bottom-4 right-4\",\n },\n },\n defaultVariants: {\n position: \"bottom-right\",\n },\n },\n)\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype ToastType = \"loading\" | \"success\" | \"info\" | \"warning\" | \"error\"\n\ntype ToastOptions = {\n title?: string\n description?: string\n type?: ToastType\n timeout?: number\n priority?: \"low\" | \"high\"\n actionProps?: React.ComponentPropsWithoutRef<\"button\">\n onClose?: () => void\n onRemove?: () => void\n}\n\ntype ToasterPosition =\n | \"top-left\"\n | \"top-center\"\n | \"top-right\"\n | \"bottom-left\"\n | \"bottom-center\"\n | \"bottom-right\"\n\ntype ToasterProps = {\n position?: ToasterPosition\n limit?: number\n timeout?: number\n}\n\ntype ToastProps = React.ComponentProps<typeof ToastPrimitive.Root>\ntype ToastTitleProps = React.ComponentProps<typeof ToastPrimitive.Title>\ntype ToastDescriptionProps = React.ComponentProps<typeof ToastPrimitive.Description>\ntype ToastActionProps = React.ComponentProps<typeof ToastPrimitive.Action>\ntype ToastCloseProps = React.ComponentProps<typeof ToastPrimitive.Close>\n\n// ---------------------------------------------------------------------------\n// Toast manager API\n// ---------------------------------------------------------------------------\n\nconst toastManager = ToastPrimitive.createToastManager()\n\nconst toast = Object.assign(\n (titleOrOptions: string | ToastOptions) => {\n const options =\n typeof titleOrOptions === \"string\"\n ? { title: titleOrOptions }\n : titleOrOptions\n return toastManager.add(options)\n },\n {\n success: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"success\", title }),\n\n error: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"error\", title }),\n\n warning: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"warning\", title }),\n\n info: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"info\", title }),\n\n loading: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({\n ...options,\n type: \"loading\",\n title,\n timeout: options?.timeout ?? 0,\n }),\n\n dismiss: (id: string) => toastManager.close(id),\n\n update: (id: string, updates: Partial<ToastOptions>) =>\n toastManager.update(id, updates),\n\n promise: toastManager.promise.bind(toastManager),\n },\n)\n\n// ---------------------------------------------------------------------------\n// Icons\n// ---------------------------------------------------------------------------\n\nconst iconMap = {\n loading: LoaderIcon,\n success: SuccessCircleIcon,\n info: InfoCircleIcon,\n warning: AlertTriangleIcon,\n error: ErrorCircleIcon,\n} as const\n\nfunction ToastIcon({ type }: { type?: ToastType }) {\n if (!type) return null\n const Icon = iconMap[type]\n return <Icon className={cn(toastIconVariants({ type }))} />\n}\n\n// ---------------------------------------------------------------------------\n// Toast primitives\n// ---------------------------------------------------------------------------\n\nfunction Toast({ className, ...props }: ToastProps) {\n return (\n <ToastPrimitive.Root\n data-slot=\"toast\"\n className={cn(toastRootClass, className)}\n {...props}\n />\n )\n}\n\nfunction ToastBody({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"toast-body\"\n className={cn(\"flex flex-1 flex-col gap-1\", className)}\n {...props}\n />\n )\n}\n\nfunction ToastTitle({ className, ...props }: ToastTitleProps) {\n return (\n <ToastPrimitive.Title\n data-slot=\"toast-title\"\n className={cn(\"text-sm font-medium\", className)}\n {...props}\n />\n )\n}\n\nfunction ToastDescription({ className, ...props }: ToastDescriptionProps) {\n return (\n <ToastPrimitive.Description\n data-slot=\"toast-description\"\n className={cn(\"text-muted text-sm\", className)}\n {...props}\n />\n )\n}\n\nfunction ToastAction({ className, ...props }: ToastActionProps) {\n return (\n <ToastPrimitive.Action\n data-slot=\"toast-action\"\n className={cn(\n \"hover:bg-secondary inline-flex h-7 items-center rounded-md border border-line px-2.5 text-xs font-medium transition-colors\",\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction ToastClose({ className, children, ...props }: ToastCloseProps) {\n return (\n <ToastPrimitive.Close\n data-slot=\"toast-close\"\n className={cn(\n \"text-muted hover:text-contrast shrink-0 rounded-md p-0.5 transition-colors\",\n className,\n )}\n {...props}\n >\n {children ?? (\n <>\n <CloseIcon className=\"size-4\" />\n <span className=\"sr-only\">Close</span>\n </>\n )}\n </ToastPrimitive.Close>\n )\n}\n\n// ---------------------------------------------------------------------------\n// 3D stacking\n// ---------------------------------------------------------------------------\n\n// Shared classes for both top and bottom positions\nconst STACKING_BASE = [\n // CSS custom properties\n \"[--toast-gap:0.5rem]\",\n \"[--toast-peek:0.5rem]\",\n \"[--toast-scale:calc(max(0,1-(var(--toast-index)*0.05)))]\",\n \"[--toast-shrink:calc(1-var(--toast-scale))]\",\n \"[--toast-stack-height:var(--toast-frontmost-height,var(--toast-height))]\",\n // Layout\n \"absolute w-full\",\n \"[z-index:calc(1000-var(--toast-index))]\",\n \"h-[var(--toast-stack-height)] data-[expanded]:h-[var(--toast-height)]\",\n // Depth blur — subtle defocus on behind toasts, cleared on expand\n \"[filter:blur(calc(var(--toast-index)*0.4px))]\",\n \"data-[expanded]:[filter:none]\",\n // Expanded (hover)\n \"data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-stack-offset-y)))]\",\n // Exit — shared\n \"data-[ending-style]:opacity-0\",\n \"data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]\",\n \"data-[expanded]:data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]\",\n \"data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--toast-stack-offset-y))]\",\n \"data-[expanded]:data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--toast-stack-offset-y))]\",\n \"data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]\",\n \"data-[expanded]:data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]\",\n \"data-[limited]:opacity-0\",\n \"[transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,filter_0.5s,height_0.15s]\",\n].join(\" \")\n\n// Direction-specific classes — sign flips for anchor, offset, transform, enter/exit\nconst STACKING_TOP = [\n \"[--toast-stack-offset-y:calc(var(--toast-offset-y)+calc(var(--toast-index)*var(--toast-gap))+var(--toast-swipe-movement-y))]\",\n \"top-0 origin-top\",\n \"[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-stack-height))))_scale(var(--toast-scale))]\",\n \"after:absolute after:bottom-full after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full after:content-['']\",\n \"data-[starting-style]:[transform:translateY(-150%)]\",\n \"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(-150%)]\",\n].join(\" \")\n\nconst STACKING_BOTTOM = [\n \"[--toast-stack-offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--toast-gap)*-1)+var(--toast-swipe-movement-y))]\",\n \"bottom-0 origin-bottom\",\n \"[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-stack-height))))_scale(var(--toast-scale))]\",\n \"after:absolute after:top-full after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full after:content-['']\",\n \"data-[starting-style]:[transform:translateY(150%)]\",\n \"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]\",\n].join(\" \")\n\nconst STACKING_TOP_CLASS = `${STACKING_BASE} ${STACKING_TOP}`\nconst STACKING_BOTTOM_CLASS = `${STACKING_BASE} ${STACKING_BOTTOM}`\n\n// ---------------------------------------------------------------------------\n// Toaster\n// ---------------------------------------------------------------------------\n\nfunction ToasterContent({ position = \"bottom-right\" }: Pick<ToasterProps, \"position\">) {\n const { toasts } = ToastPrimitive.useToastManager()\n const isTop = position.startsWith(\"top\")\n\n return (\n <ToastPrimitive.Portal>\n <ToastPrimitive.Viewport\n data-slot=\"toast-viewport\"\n className={cn(toastViewportVariants({ position }))}\n >\n {toasts.map((t) => (\n <Toast\n key={t.id}\n toast={t}\n swipeDirection={isTop ? [\"up\", \"right\"] : [\"down\", \"right\"]}\n className={isTop ? STACKING_TOP_CLASS : STACKING_BOTTOM_CLASS}\n >\n <ToastPrimitive.Content\n data-slot=\"toast-content\"\n className=\"flex items-start gap-3 overflow-hidden transition-opacity duration-200 data-[behind]:pointer-events-none data-[behind]:opacity-0 data-[expanded]:pointer-events-auto data-[expanded]:opacity-100\"\n >\n <ToastIcon type={t.type as ToastType | undefined} />\n\n <ToastBody>\n {t.title && <ToastTitle>{t.title}</ToastTitle>}\n {t.description && (\n <ToastDescription>{t.description}</ToastDescription>\n )}\n {t.actionProps && (\n <div className=\"mt-1.5\">\n <ToastAction {...t.actionProps} />\n </div>\n )}\n </ToastBody>\n\n <ToastClose />\n </ToastPrimitive.Content>\n </Toast>\n ))}\n </ToastPrimitive.Viewport>\n </ToastPrimitive.Portal>\n )\n}\n\n// The toast manager is a module-level singleton, so every mounted <Toaster/>\n// renders the SAME shared toast list — duplicating every toast. This registry\n// elects a single primary Toaster (the first mounted); the rest render null.\n// Guards against a consumer accidentally rendering two, and against Storybook\n// autodocs rendering several stories — each with a Toaster — on one page.\nconst mountedToasters: symbol[] = []\nconst toasterListeners = new Set<() => void>()\n\nfunction notifyToasters() {\n for (const listener of toasterListeners) {\n listener()\n }\n}\n\nfunction useIsPrimaryToaster() {\n const keyRef = React.useRef<symbol | null>(null)\n if (keyRef.current === null) {\n keyRef.current = Symbol(\"toaster\")\n }\n const key = keyRef.current\n\n const isPrimary = React.useSyncExternalStore(\n React.useCallback((onChange: () => void) => {\n toasterListeners.add(onChange)\n return () => {\n toasterListeners.delete(onChange)\n }\n }, []),\n () => mountedToasters[0] === key,\n () => false,\n )\n\n React.useEffect(() => {\n mountedToasters.push(key)\n notifyToasters()\n return () => {\n const index = mountedToasters.indexOf(key)\n if (index !== -1) {\n mountedToasters.splice(index, 1)\n }\n notifyToasters()\n }\n }, [key])\n\n return isPrimary\n}\n\nfunction Toaster({ position, limit, timeout }: ToasterProps) {\n const isPrimary = useIsPrimaryToaster()\n if (!isPrimary) {\n return null\n }\n return (\n <ToastPrimitive.Provider\n toastManager={toastManager}\n limit={limit}\n timeout={timeout}\n >\n <ToasterContent position={position} />\n </ToastPrimitive.Provider>\n )\n}\n\nexport {\n toastRootClass,\n toastIconVariants,\n toastViewportVariants,\n toastManager,\n toast,\n Toaster,\n Toast,\n ToastIcon,\n ToastTitle,\n ToastDescription,\n ToastAction,\n ToastClose,\n}\n\nexport type {\n ToastType,\n ToastOptions,\n ToasterPosition,\n ToasterProps\n}"],"mappings":"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"}
1
+ {"version":3,"file":"toast.js","names":["ToastPrimitive"],"sources":["../src/toast.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Toast as ToastPrimitive } from \"@base-ui/react/toast\"\nimport { cva } from \"class-variance-authority\"\n\nimport { cn } from \"./lib/utils\"\nimport {\n CloseIcon,\n SuccessCircleIcon,\n InfoCircleIcon,\n AlertTriangleIcon,\n ErrorCircleIcon,\n LoaderIcon,\n} from \"./lib/internal-icons\"\n\n// ---------------------------------------------------------------------------\n// Styles\n// ---------------------------------------------------------------------------\n\nconst toastRootClass =\n \"bg-elevated text-contrast ring-contrast/10 rounded-md p-4 text-sm shadow-md ring-1 outline-none select-none\"\n\nconst toastIconVariants = cva(\n \"mt-0.5 size-4 shrink-0\",\n {\n variants: {\n type: {\n loading: \"text-muted animate-spin\",\n success: \"text-success\",\n info: \"text-info\",\n warning: \"text-warning\",\n error: \"text-destructive\",\n },\n },\n }\n)\n\nconst toastViewportVariants = cva(\n \"fixed z-[100] flex w-full outline-none sm:max-w-sm\",\n {\n variants: {\n position: {\n \"top-left\": \"top-4 left-4\",\n \"top-center\": \"top-4 right-0 left-0 mx-auto\",\n \"top-right\": \"top-4 right-4\",\n \"bottom-left\": \"bottom-4 left-4\",\n \"bottom-center\": \"bottom-4 right-0 left-0 mx-auto\",\n \"bottom-right\": \"bottom-4 right-4\",\n },\n },\n defaultVariants: {\n position: \"bottom-right\",\n },\n },\n)\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype ToastType = \"loading\" | \"success\" | \"info\" | \"warning\" | \"error\"\n\ntype ToastOptions = {\n title?: string\n description?: string\n type?: ToastType\n timeout?: number\n priority?: \"low\" | \"high\"\n actionProps?: React.ComponentPropsWithoutRef<\"button\">\n onClose?: () => void\n onRemove?: () => void\n}\n\ntype ToasterPosition =\n | \"top-left\"\n | \"top-center\"\n | \"top-right\"\n | \"bottom-left\"\n | \"bottom-center\"\n | \"bottom-right\"\n\ntype ToasterProps = {\n position?: ToasterPosition\n limit?: number\n timeout?: number\n}\n\ntype ToastProps = React.ComponentProps<typeof ToastPrimitive.Root>\ntype ToastTitleProps = React.ComponentProps<typeof ToastPrimitive.Title>\ntype ToastDescriptionProps = React.ComponentProps<typeof ToastPrimitive.Description>\ntype ToastActionProps = React.ComponentProps<typeof ToastPrimitive.Action>\ntype ToastCloseProps = React.ComponentProps<typeof ToastPrimitive.Close>\n\n// ---------------------------------------------------------------------------\n// Toast manager API\n// ---------------------------------------------------------------------------\n\nconst toastManager = ToastPrimitive.createToastManager()\n\nconst toast = Object.assign(\n (titleOrOptions: string | ToastOptions) => {\n const options =\n typeof titleOrOptions === \"string\"\n ? { title: titleOrOptions }\n : titleOrOptions\n return toastManager.add(options)\n },\n {\n success: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"success\", title }),\n\n error: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"error\", title }),\n\n warning: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"warning\", title }),\n\n info: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({ ...options, type: \"info\", title }),\n\n loading: (title: string, options?: Omit<ToastOptions, \"type\" | \"title\">) =>\n toastManager.add({\n ...options,\n type: \"loading\",\n title,\n timeout: options?.timeout ?? 0,\n }),\n\n dismiss: (id: string) => toastManager.close(id),\n\n update: (id: string, updates: Partial<ToastOptions>) =>\n toastManager.update(id, updates),\n\n promise: toastManager.promise.bind(toastManager),\n },\n)\n\n// ---------------------------------------------------------------------------\n// Icons\n// ---------------------------------------------------------------------------\n\nconst iconMap = {\n loading: LoaderIcon,\n success: SuccessCircleIcon,\n info: InfoCircleIcon,\n warning: AlertTriangleIcon,\n error: ErrorCircleIcon,\n} as const\n\nconst toastTypeLabels: Record<ToastType, string> = {\n loading: \"Loading\",\n success: \"Success\",\n info: \"Information\",\n warning: \"Warning\",\n error: \"Error\",\n}\n\nfunction ToastIcon({ type }: { type?: ToastType }) {\n if (!type) return null\n const Icon = iconMap[type]\n return (\n <>\n <Icon className={cn(toastIconVariants({ type }))} />\n {/* The icon is decorative (color/shape); announce the type in words so it\n * isn't conveyed by color alone. */}\n <span className=\"sr-only\">{toastTypeLabels[type]}</span>\n </>\n )\n}\n\n// ---------------------------------------------------------------------------\n// Toast primitives\n// ---------------------------------------------------------------------------\n\nfunction Toast({ className, ...props }: ToastProps) {\n return (\n <ToastPrimitive.Root\n data-slot=\"toast\"\n className={cn(toastRootClass, className)}\n {...props}\n />\n )\n}\n\nfunction ToastBody({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"toast-body\"\n className={cn(\"flex flex-1 flex-col gap-1\", className)}\n {...props}\n />\n )\n}\n\nfunction ToastTitle({ className, ...props }: ToastTitleProps) {\n return (\n <ToastPrimitive.Title\n data-slot=\"toast-title\"\n className={cn(\"text-sm font-medium\", className)}\n {...props}\n />\n )\n}\n\nfunction ToastDescription({ className, ...props }: ToastDescriptionProps) {\n return (\n <ToastPrimitive.Description\n data-slot=\"toast-description\"\n className={cn(\"text-muted text-sm\", className)}\n {...props}\n />\n )\n}\n\nfunction ToastAction({ className, ...props }: ToastActionProps) {\n return (\n <ToastPrimitive.Action\n data-slot=\"toast-action\"\n className={cn(\n \"hover:bg-secondary inline-flex h-7 items-center rounded-md border border-line px-2.5 text-xs font-medium transition-colors outline-none focus-visible:border-focus focus-visible:ring-focus/50 focus-visible:ring-3\",\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction ToastClose({ className, children, ...props }: ToastCloseProps) {\n return (\n <ToastPrimitive.Close\n data-slot=\"toast-close\"\n className={cn(\n \"text-muted hover:text-contrast shrink-0 rounded-md p-0.5 transition-colors outline-none focus-visible:ring-focus/50 focus-visible:ring-3\",\n className,\n )}\n {...props}\n >\n {children ?? (\n <>\n <CloseIcon className=\"size-4\" />\n <span className=\"sr-only\">Close</span>\n </>\n )}\n </ToastPrimitive.Close>\n )\n}\n\n// ---------------------------------------------------------------------------\n// 3D stacking\n// ---------------------------------------------------------------------------\n\n// Shared classes for both top and bottom positions\nconst STACKING_BASE = [\n // CSS custom properties\n \"[--toast-gap:0.5rem]\",\n \"[--toast-peek:0.5rem]\",\n \"[--toast-scale:calc(max(0,1-(var(--toast-index)*0.05)))]\",\n \"[--toast-shrink:calc(1-var(--toast-scale))]\",\n \"[--toast-stack-height:var(--toast-frontmost-height,var(--toast-height))]\",\n // Layout\n \"absolute w-full\",\n \"[z-index:calc(1000-var(--toast-index))]\",\n \"h-[var(--toast-stack-height)] data-[expanded]:h-[var(--toast-height)]\",\n // Depth blur — subtle defocus on behind toasts, cleared on expand\n \"[filter:blur(calc(var(--toast-index)*0.4px))]\",\n \"data-[expanded]:[filter:none]\",\n // Expanded (hover)\n \"data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-stack-offset-y)))]\",\n // Exit — shared\n \"data-[ending-style]:opacity-0\",\n \"data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]\",\n \"data-[expanded]:data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]\",\n \"data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--toast-stack-offset-y))]\",\n \"data-[expanded]:data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--toast-stack-offset-y))]\",\n \"data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]\",\n \"data-[expanded]:data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]\",\n \"data-[limited]:opacity-0\",\n \"[transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,filter_0.5s,height_0.15s]\",\n // Collapse the stacking motion under reduced motion — toasts appear and\n // restack instantly instead of sliding/scaling.\n \"motion-reduce:[transition:none]\",\n].join(\" \")\n\n// Direction-specific classes — sign flips for anchor, offset, transform, enter/exit\nconst STACKING_TOP = [\n \"[--toast-stack-offset-y:calc(var(--toast-offset-y)+calc(var(--toast-index)*var(--toast-gap))+var(--toast-swipe-movement-y))]\",\n \"top-0 origin-top\",\n \"[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-stack-height))))_scale(var(--toast-scale))]\",\n \"after:absolute after:bottom-full after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full after:content-['']\",\n \"data-[starting-style]:[transform:translateY(-150%)]\",\n \"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(-150%)]\",\n].join(\" \")\n\nconst STACKING_BOTTOM = [\n \"[--toast-stack-offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--toast-gap)*-1)+var(--toast-swipe-movement-y))]\",\n \"bottom-0 origin-bottom\",\n \"[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-stack-height))))_scale(var(--toast-scale))]\",\n \"after:absolute after:top-full after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full after:content-['']\",\n \"data-[starting-style]:[transform:translateY(150%)]\",\n \"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]\",\n].join(\" \")\n\nconst STACKING_TOP_CLASS = `${STACKING_BASE} ${STACKING_TOP}`\nconst STACKING_BOTTOM_CLASS = `${STACKING_BASE} ${STACKING_BOTTOM}`\n\n// ---------------------------------------------------------------------------\n// Toaster\n// ---------------------------------------------------------------------------\n\nfunction ToasterContent({ position = \"bottom-right\" }: Pick<ToasterProps, \"position\">) {\n const { toasts } = ToastPrimitive.useToastManager()\n const isTop = position.startsWith(\"top\")\n\n return (\n <ToastPrimitive.Portal>\n <ToastPrimitive.Viewport\n data-slot=\"toast-viewport\"\n className={cn(toastViewportVariants({ position }))}\n >\n {toasts.map((t) => (\n <Toast\n key={t.id}\n toast={t}\n swipeDirection={isTop ? [\"up\", \"right\"] : [\"down\", \"right\"]}\n className={isTop ? STACKING_TOP_CLASS : STACKING_BOTTOM_CLASS}\n >\n <ToastPrimitive.Content\n data-slot=\"toast-content\"\n className=\"flex items-start gap-3 overflow-hidden transition-opacity duration-200 data-[behind]:pointer-events-none data-[behind]:opacity-0 data-[expanded]:pointer-events-auto data-[expanded]:opacity-100\"\n >\n <ToastIcon type={t.type as ToastType | undefined} />\n\n <ToastBody>\n {t.title && <ToastTitle>{t.title}</ToastTitle>}\n {t.description && (\n <ToastDescription>{t.description}</ToastDescription>\n )}\n {t.actionProps && (\n <div className=\"mt-1.5\">\n <ToastAction {...t.actionProps} />\n </div>\n )}\n </ToastBody>\n\n <ToastClose />\n </ToastPrimitive.Content>\n </Toast>\n ))}\n </ToastPrimitive.Viewport>\n </ToastPrimitive.Portal>\n )\n}\n\n// The toast manager is a module-level singleton, so every mounted <Toaster/>\n// renders the SAME shared toast list — duplicating every toast. This registry\n// elects a single primary Toaster (the first mounted); the rest render null.\n// Guards against a consumer accidentally rendering two, and against Storybook\n// autodocs rendering several stories — each with a Toaster — on one page.\nconst mountedToasters: symbol[] = []\nconst toasterListeners = new Set<() => void>()\n\nfunction notifyToasters() {\n for (const listener of toasterListeners) {\n listener()\n }\n}\n\nfunction useIsPrimaryToaster() {\n const keyRef = React.useRef<symbol | null>(null)\n if (keyRef.current === null) {\n keyRef.current = Symbol(\"toaster\")\n }\n const key = keyRef.current\n\n const isPrimary = React.useSyncExternalStore(\n React.useCallback((onChange: () => void) => {\n toasterListeners.add(onChange)\n return () => {\n toasterListeners.delete(onChange)\n }\n }, []),\n () => mountedToasters[0] === key,\n () => false,\n )\n\n React.useEffect(() => {\n mountedToasters.push(key)\n notifyToasters()\n return () => {\n const index = mountedToasters.indexOf(key)\n if (index !== -1) {\n mountedToasters.splice(index, 1)\n }\n notifyToasters()\n }\n }, [key])\n\n return isPrimary\n}\n\nfunction Toaster({ position, limit, timeout }: ToasterProps) {\n const isPrimary = useIsPrimaryToaster()\n if (!isPrimary) {\n return null\n }\n return (\n <ToastPrimitive.Provider\n toastManager={toastManager}\n limit={limit}\n timeout={timeout}\n >\n <ToasterContent position={position} />\n </ToastPrimitive.Provider>\n )\n}\n\nexport {\n toastRootClass,\n toastIconVariants,\n toastViewportVariants,\n toastManager,\n toast,\n Toaster,\n Toast,\n ToastIcon,\n ToastTitle,\n ToastDescription,\n ToastAction,\n ToastClose,\n}\n\nexport type {\n ToastType,\n ToastOptions,\n ToasterPosition,\n ToasterProps\n}"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { cloneElement, isValidElement, useEffect, useId, useRef, useState } from "react";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
- import { useInView } from "motion/react";
4
+ import { useInView, useReducedMotion } from "motion/react";
5
5
  //#region src/typewriter.tsx
6
6
  function mergeRef(internalRef, externalRef) {
7
7
  return (el) => {
@@ -105,22 +105,24 @@ function Typewriter({ text, children, speed = .04, delay = 0, cursor = true, cur
105
105
  });
106
106
  }
107
107
  function TypingReveal({ text, children, elementRef, id, speed, delay, cursor, cursorChar, shouldAnimate }) {
108
+ const prefersReducedMotion = useReducedMotion();
108
109
  const flat = flattenText(text);
109
110
  const segments = typeof text === "string" ? [{ text }] : text;
110
111
  const [charIndex, setCharIndex] = useState(0);
111
112
  const [started, setStarted] = useState(false);
112
113
  const [done, setDone] = useState(false);
113
114
  useEffect(() => {
114
- if (!shouldAnimate || started) return;
115
+ if (prefersReducedMotion || !shouldAnimate || started) return;
115
116
  const timer = setTimeout(() => setStarted(true), delay * 1e3);
116
117
  return () => clearTimeout(timer);
117
118
  }, [
119
+ prefersReducedMotion,
118
120
  shouldAnimate,
119
121
  delay,
120
122
  started
121
123
  ]);
122
124
  useEffect(() => {
123
- if (!started || done) return;
125
+ if (prefersReducedMotion || !started || done) return;
124
126
  if (charIndex >= flat.length) {
125
127
  setDone(true);
126
128
  return;
@@ -128,6 +130,7 @@ function TypingReveal({ text, children, elementRef, id, speed, delay, cursor, cu
128
130
  const timer = setTimeout(() => setCharIndex((prev) => prev + 1), speed * 1e3);
129
131
  return () => clearTimeout(timer);
130
132
  }, [
133
+ prefersReducedMotion,
131
134
  started,
132
135
  charIndex,
133
136
  flat.length,
@@ -136,8 +139,8 @@ function TypingReveal({ text, children, elementRef, id, speed, delay, cursor, cu
136
139
  ]);
137
140
  if (!isValidElement(children)) return children;
138
141
  const existingRef = children.props.ref;
139
- const content = started ? buildSegmentContent(segments, charIndex) : null;
140
- const showCursor = cursor && started && !done;
142
+ const content = prefersReducedMotion ? buildSegmentContent(segments, flat.length) : started ? buildSegmentContent(segments, charIndex) : null;
143
+ const showCursor = !prefersReducedMotion && cursor && started && !done;
141
144
  return cloneElement(children, {
142
145
  ref: mergeRef(elementRef, existingRef),
143
146
  children: /* @__PURE__ */ jsxs(Fragment, { children: [content, showCursor && /* @__PURE__ */ jsx(BlinkingCursor, {
@@ -147,29 +150,35 @@ function TypingReveal({ text, children, elementRef, id, speed, delay, cursor, cu
147
150
  });
148
151
  }
149
152
  function SmoothReveal({ text, children, elementRef, id, delay, duration, cursor, cursorChar, shouldAnimate }) {
153
+ const prefersReducedMotion = useReducedMotion();
150
154
  const segments = typeof text === "string" ? [{ text }] : text;
151
155
  const [revealed, setRevealed] = useState(false);
152
156
  const [done, setDone] = useState(false);
153
157
  useEffect(() => {
154
- if (!shouldAnimate || revealed) return;
158
+ if (prefersReducedMotion || !shouldAnimate || revealed) return;
155
159
  const timer = setTimeout(() => setRevealed(true), delay * 1e3);
156
160
  return () => clearTimeout(timer);
157
161
  }, [
162
+ prefersReducedMotion,
158
163
  shouldAnimate,
159
164
  delay,
160
165
  revealed
161
166
  ]);
162
167
  useEffect(() => {
163
- if (!revealed) return;
168
+ if (prefersReducedMotion || !revealed) return;
164
169
  const timer = setTimeout(() => setDone(true), duration * 1e3);
165
170
  return () => clearTimeout(timer);
166
- }, [revealed, duration]);
171
+ }, [
172
+ prefersReducedMotion,
173
+ revealed,
174
+ duration
175
+ ]);
167
176
  if (!isValidElement(children)) return children;
168
177
  const childProps = children.props;
169
178
  const existingRef = childProps.ref;
170
179
  const existingStyle = childProps.style ?? {};
171
180
  const animName = `tw-smooth-${id}`;
172
- const showCursor = cursor && revealed && !done;
181
+ const showCursor = !prefersReducedMotion && cursor && revealed && !done;
173
182
  const fullContent = segments.map((seg, i) => seg.className ? /* @__PURE__ */ jsx("span", {
174
183
  className: seg.className,
175
184
  children: seg.text
@@ -180,7 +189,7 @@ function SmoothReveal({ text, children, elementRef, id, delay, duration, cursor,
180
189
  ...existingStyle,
181
190
  whiteSpace: "nowrap",
182
191
  overflow: "hidden",
183
- ...revealed ? { animation: `${animName} ${duration}s linear forwards` } : { maxWidth: 0 }
192
+ ...prefersReducedMotion ? { maxWidth: "100%" } : revealed ? { animation: `${animName} ${duration}s linear forwards` } : { maxWidth: 0 }
184
193
  },
185
194
  children: /* @__PURE__ */ jsxs(Fragment, { children: [
186
195
  /* @__PURE__ */ jsx("style", { children: `@keyframes ${animName} { from { max-width: 0; } to { max-width: 100%; } }` }),
@@ -1 +1 @@
1
- {"version":3,"file":"typewriter.js","names":[],"sources":["../src/typewriter.tsx"],"sourcesContent":["\"use client\"\n\nimport {\n type CSSProperties,\n type ReactElement,\n type Ref,\n cloneElement,\n isValidElement,\n useEffect,\n useId,\n useRef,\n useState,\n} from \"react\"\nimport { useInView } from \"motion/react\"\n\n// ── Types ────────────────────────────────────────────────────────────\n\ninterface TextSegment {\n text: string\n /** Optional className applied to this segment (e.g., bold, colored) */\n className?: string\n}\n\ninterface TypewriterProps {\n /** Simple string or styled segments for per-word/phrase styling */\n text: string | TextSegment[]\n /** Element to render into. Receives the animated text as children. */\n children: ReactElement\n /** Time per character in seconds. Default: 0.04 */\n speed?: number\n /** Delay before typing starts in seconds. Default: 0 */\n delay?: number\n /** Show a blinking cursor during typing. Default: true */\n cursor?: boolean\n /** Cursor character. Default: '|' */\n cursorChar?: string\n /** Trigger when scrolled into view instead of on mount. Default: false */\n onView?: boolean\n /** Trigger once. Default: true */\n once?: boolean\n /**\n * Reveal mode. Default: 'typing'\n * - 'typing': character-by-character like someone typing\n * - 'smooth': sliding mask reveal (cinematic feel)\n */\n variant?: \"typing\" | \"smooth\"\n /** Duration of the smooth reveal in seconds. Default: 2 */\n smoothDuration?: number\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────\n\nfunction mergeRef(\n internalRef: React.RefObject<HTMLElement | null>,\n externalRef?: Ref<HTMLElement>,\n) {\n return (el: HTMLElement | null) => {\n ;(internalRef as { current: HTMLElement | null }).current = el\n if (typeof externalRef === \"function\") externalRef(el)\n else if (externalRef && typeof externalRef === \"object\") {\n ;(externalRef as { current: HTMLElement | null }).current = el\n }\n }\n}\n\n/** Flatten text prop into a single string for character counting */\nfunction flattenText(text: string | TextSegment[]): string {\n if (typeof text === \"string\") return text\n return text.map((s) => s.text).join(\"\")\n}\n\n/** Build rendered content from segments up to a character index */\nfunction buildSegmentContent(\n segments: TextSegment[],\n charIndex: number,\n): React.ReactNode[] {\n const nodes: React.ReactNode[] = []\n let remaining = charIndex\n\n for (let i = 0; i < segments.length; i++) {\n const seg = segments[i]!\n if (remaining <= 0) break\n\n const visibleChars = seg.text.slice(0, remaining)\n remaining -= visibleChars.length\n\n if (seg.className) {\n nodes.push(\n <span key={i} className={seg.className}>\n {visibleChars}\n </span>,\n )\n } else {\n nodes.push(visibleChars)\n }\n }\n\n return nodes\n}\n\n// ── Cursor ───────────────────────────────────────────────────────────\n\nfunction BlinkingCursor({ char, id }: { char: string; id: string }) {\n const name = `tw-blink-${id}`\n return (\n <>\n <span\n aria-hidden\n style={{\n animation: `${name} 0.8s step-end infinite`,\n fontWeight: \"normal\",\n }}\n >\n {char}\n </span>\n <style>{`@keyframes ${name} { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }`}</style>\n </>\n )\n}\n\n// ── Typewriter ───────────────────────────────────────────────────────\n\n/**\n * Text reveal animation with two modes:\n *\n * **typing** (default) — character-by-character like someone typing.\n * **smooth** — cinematic sliding mask reveal.\n *\n * Supports plain strings or styled segments for per-word coloring.\n * Zero wrapper divs — renders into the child element via cloneElement.\n *\n * @example\n * ```tsx\n * // Simple string\n * <Typewriter text=\"Welcome to the future.\">\n * <h1 className=\"text-4xl font-bold\" />\n * </Typewriter>\n *\n * // Per-word styling\n * <Typewriter text={[\n * { text: \"Build on \" },\n * { text: \"Intuition\", className: \"text-primary font-bold\" },\n * ]}>\n * <h1 className=\"text-4xl\" />\n * </Typewriter>\n *\n * // Smooth reveal\n * <Typewriter text=\"Cinematic reveal.\" variant=\"smooth\" smoothDuration={1.5}>\n * <h1 className=\"text-4xl font-bold\" />\n * </Typewriter>\n * ```\n */\nfunction Typewriter({\n text,\n children,\n speed = 0.04,\n delay = 0,\n cursor = true,\n cursorChar = \"|\",\n onView = false,\n once = true,\n variant = \"typing\",\n smoothDuration = 2,\n}: TypewriterProps) {\n const ref = useRef<HTMLElement>(null)\n const id = useId().replace(/:/g, \"\")\n const isInView = useInView(ref, { once, margin: \"-50px\" })\n\n const shouldAnimate = onView ? isInView : true\n\n if (variant === \"smooth\") {\n return (\n <SmoothReveal\n text={text}\n elementRef={ref}\n id={id}\n delay={delay}\n duration={smoothDuration}\n cursor={cursor}\n cursorChar={cursorChar}\n shouldAnimate={shouldAnimate}\n >\n {children}\n </SmoothReveal>\n )\n }\n\n return (\n <TypingReveal\n text={text}\n elementRef={ref}\n id={id}\n speed={speed}\n delay={delay}\n cursor={cursor}\n cursorChar={cursorChar}\n shouldAnimate={shouldAnimate}\n >\n {children}\n </TypingReveal>\n )\n}\n\n// ── Typing Reveal ────────────────────────────────────────────────────\n\nfunction TypingReveal({\n text,\n children,\n elementRef,\n id,\n speed,\n delay,\n cursor,\n cursorChar,\n shouldAnimate,\n}: {\n text: string | TextSegment[]\n children: ReactElement\n elementRef: React.RefObject<HTMLElement | null>\n id: string\n speed: number\n delay: number\n cursor: boolean\n cursorChar: string\n shouldAnimate: boolean\n}) {\n const flat = flattenText(text)\n const segments = typeof text === \"string\" ? [{ text }] : text\n const [charIndex, setCharIndex] = useState(0)\n const [started, setStarted] = useState(false)\n const [done, setDone] = useState(false)\n\n useEffect(() => {\n if (!shouldAnimate || started) return\n const timer = setTimeout(() => setStarted(true), delay * 1000)\n return () => clearTimeout(timer)\n }, [shouldAnimate, delay, started])\n\n useEffect(() => {\n if (!started || done) return\n if (charIndex >= flat.length) {\n setDone(true)\n return\n }\n const timer = setTimeout(() => setCharIndex((prev) => prev + 1), speed * 1000)\n return () => clearTimeout(timer)\n }, [started, charIndex, flat.length, speed, done])\n\n if (!isValidElement(children)) return children\n\n const childProps = children.props as Record<string, unknown>\n const existingRef = (childProps as { ref?: Ref<HTMLElement> }).ref\n\n const content = started ? buildSegmentContent(segments, charIndex) : null\n const showCursor = cursor && started && !done\n\n return cloneElement(children, {\n ref: mergeRef(elementRef, existingRef),\n children: (\n <>\n {content}\n {showCursor && <BlinkingCursor char={cursorChar} id={id} />}\n </>\n ),\n } as Record<string, unknown>)\n}\n\n// ── Smooth Reveal ────────────────────────────────────────────────────\n\nfunction SmoothReveal({\n text,\n children,\n elementRef,\n id,\n delay,\n duration,\n cursor,\n cursorChar,\n shouldAnimate,\n}: {\n text: string | TextSegment[]\n children: ReactElement\n elementRef: React.RefObject<HTMLElement | null>\n id: string\n delay: number\n duration: number\n cursor: boolean\n cursorChar: string\n shouldAnimate: boolean\n}) {\n const segments = typeof text === \"string\" ? [{ text }] : text\n const [revealed, setRevealed] = useState(false)\n const [done, setDone] = useState(false)\n\n useEffect(() => {\n if (!shouldAnimate || revealed) return\n const timer = setTimeout(() => setRevealed(true), delay * 1000)\n return () => clearTimeout(timer)\n }, [shouldAnimate, delay, revealed])\n\n useEffect(() => {\n if (!revealed) return\n const timer = setTimeout(() => setDone(true), duration * 1000)\n return () => clearTimeout(timer)\n }, [revealed, duration])\n\n if (!isValidElement(children)) return children\n\n const childProps = children.props as Record<string, unknown>\n const existingRef = (childProps as { ref?: Ref<HTMLElement> }).ref\n const existingStyle = (childProps.style ?? {}) as CSSProperties\n\n const animName = `tw-smooth-${id}`\n const showCursor = cursor && revealed && !done\n\n // Full text is always in the DOM — the mask clip animates to reveal it\n const fullContent = segments.map((seg, i) =>\n seg.className ? (\n <span key={i} className={seg.className}>\n {seg.text}\n </span>\n ) : (\n seg.text\n ),\n )\n\n return cloneElement(children, {\n ref: mergeRef(elementRef, existingRef),\n style: {\n ...existingStyle,\n whiteSpace: \"nowrap\" as const,\n overflow: \"hidden\" as const,\n ...(revealed\n ? {\n animation: `${animName} ${duration}s linear forwards`,\n }\n : {\n maxWidth: 0,\n }),\n },\n children: (\n <>\n <style>{`@keyframes ${animName} { from { max-width: 0; } to { max-width: 100%; } }`}</style>\n {fullContent}\n {showCursor && <BlinkingCursor char={cursorChar} id={id} />}\n </>\n ),\n } as Record<string, unknown>)\n}\n\n// ── Exports ──────────────────────────────────────────────────────────\n\nexport { Typewriter }\nexport type { TypewriterProps, TextSegment }\n"],"mappings":"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"}
1
+ {"version":3,"file":"typewriter.js","names":[],"sources":["../src/typewriter.tsx"],"sourcesContent":["\"use client\"\n\nimport {\n type CSSProperties,\n type ReactElement,\n type Ref,\n cloneElement,\n isValidElement,\n useEffect,\n useId,\n useRef,\n useState,\n} from \"react\"\nimport { useInView, useReducedMotion } from \"motion/react\"\n\n// ── Types ────────────────────────────────────────────────────────────\n\ninterface TextSegment {\n text: string\n /** Optional className applied to this segment (e.g., bold, colored) */\n className?: string\n}\n\ninterface TypewriterProps {\n /** Simple string or styled segments for per-word/phrase styling */\n text: string | TextSegment[]\n /** Element to render into. Receives the animated text as children. */\n children: ReactElement\n /** Time per character in seconds. Default: 0.04 */\n speed?: number\n /** Delay before typing starts in seconds. Default: 0 */\n delay?: number\n /** Show a blinking cursor during typing. Default: true */\n cursor?: boolean\n /** Cursor character. Default: '|' */\n cursorChar?: string\n /** Trigger when scrolled into view instead of on mount. Default: false */\n onView?: boolean\n /** Trigger once. Default: true */\n once?: boolean\n /**\n * Reveal mode. Default: 'typing'\n * - 'typing': character-by-character like someone typing\n * - 'smooth': sliding mask reveal (cinematic feel)\n */\n variant?: \"typing\" | \"smooth\"\n /** Duration of the smooth reveal in seconds. Default: 2 */\n smoothDuration?: number\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────\n\nfunction mergeRef(\n internalRef: React.RefObject<HTMLElement | null>,\n externalRef?: Ref<HTMLElement>,\n) {\n return (el: HTMLElement | null) => {\n ;(internalRef as { current: HTMLElement | null }).current = el\n if (typeof externalRef === \"function\") externalRef(el)\n else if (externalRef && typeof externalRef === \"object\") {\n ;(externalRef as { current: HTMLElement | null }).current = el\n }\n }\n}\n\n/** Flatten text prop into a single string for character counting */\nfunction flattenText(text: string | TextSegment[]): string {\n if (typeof text === \"string\") return text\n return text.map((s) => s.text).join(\"\")\n}\n\n/** Build rendered content from segments up to a character index */\nfunction buildSegmentContent(\n segments: TextSegment[],\n charIndex: number,\n): React.ReactNode[] {\n const nodes: React.ReactNode[] = []\n let remaining = charIndex\n\n for (let i = 0; i < segments.length; i++) {\n const seg = segments[i]!\n if (remaining <= 0) break\n\n const visibleChars = seg.text.slice(0, remaining)\n remaining -= visibleChars.length\n\n if (seg.className) {\n nodes.push(\n <span key={i} className={seg.className}>\n {visibleChars}\n </span>,\n )\n } else {\n nodes.push(visibleChars)\n }\n }\n\n return nodes\n}\n\n// ── Cursor ───────────────────────────────────────────────────────────\n\nfunction BlinkingCursor({ char, id }: { char: string; id: string }) {\n const name = `tw-blink-${id}`\n return (\n <>\n <span\n aria-hidden\n style={{\n animation: `${name} 0.8s step-end infinite`,\n fontWeight: \"normal\",\n }}\n >\n {char}\n </span>\n <style>{`@keyframes ${name} { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }`}</style>\n </>\n )\n}\n\n// ── Typewriter ───────────────────────────────────────────────────────\n\n/**\n * Text reveal animation with two modes:\n *\n * **typing** (default) — character-by-character like someone typing.\n * **smooth** — cinematic sliding mask reveal.\n *\n * Supports plain strings or styled segments for per-word coloring.\n * Zero wrapper divs — renders into the child element via cloneElement.\n *\n * @example\n * ```tsx\n * // Simple string\n * <Typewriter text=\"Welcome to the future.\">\n * <h1 className=\"text-4xl font-bold\" />\n * </Typewriter>\n *\n * // Per-word styling\n * <Typewriter text={[\n * { text: \"Build on \" },\n * { text: \"Intuition\", className: \"text-primary font-bold\" },\n * ]}>\n * <h1 className=\"text-4xl\" />\n * </Typewriter>\n *\n * // Smooth reveal\n * <Typewriter text=\"Cinematic reveal.\" variant=\"smooth\" smoothDuration={1.5}>\n * <h1 className=\"text-4xl font-bold\" />\n * </Typewriter>\n * ```\n */\nfunction Typewriter({\n text,\n children,\n speed = 0.04,\n delay = 0,\n cursor = true,\n cursorChar = \"|\",\n onView = false,\n once = true,\n variant = \"typing\",\n smoothDuration = 2,\n}: TypewriterProps) {\n const ref = useRef<HTMLElement>(null)\n const id = useId().replace(/:/g, \"\")\n const isInView = useInView(ref, { once, margin: \"-50px\" })\n\n const shouldAnimate = onView ? isInView : true\n\n if (variant === \"smooth\") {\n return (\n <SmoothReveal\n text={text}\n elementRef={ref}\n id={id}\n delay={delay}\n duration={smoothDuration}\n cursor={cursor}\n cursorChar={cursorChar}\n shouldAnimate={shouldAnimate}\n >\n {children}\n </SmoothReveal>\n )\n }\n\n return (\n <TypingReveal\n text={text}\n elementRef={ref}\n id={id}\n speed={speed}\n delay={delay}\n cursor={cursor}\n cursorChar={cursorChar}\n shouldAnimate={shouldAnimate}\n >\n {children}\n </TypingReveal>\n )\n}\n\n// ── Typing Reveal ────────────────────────────────────────────────────\n\nfunction TypingReveal({\n text,\n children,\n elementRef,\n id,\n speed,\n delay,\n cursor,\n cursorChar,\n shouldAnimate,\n}: {\n text: string | TextSegment[]\n children: ReactElement\n elementRef: React.RefObject<HTMLElement | null>\n id: string\n speed: number\n delay: number\n cursor: boolean\n cursorChar: string\n shouldAnimate: boolean\n}) {\n const prefersReducedMotion = useReducedMotion()\n const flat = flattenText(text)\n const segments = typeof text === \"string\" ? [{ text }] : text\n const [charIndex, setCharIndex] = useState(0)\n const [started, setStarted] = useState(false)\n const [done, setDone] = useState(false)\n\n useEffect(() => {\n if (prefersReducedMotion || !shouldAnimate || started) return\n const timer = setTimeout(() => setStarted(true), delay * 1000)\n return () => clearTimeout(timer)\n }, [prefersReducedMotion, shouldAnimate, delay, started])\n\n useEffect(() => {\n if (prefersReducedMotion || !started || done) return\n if (charIndex >= flat.length) {\n setDone(true)\n return\n }\n const timer = setTimeout(() => setCharIndex((prev) => prev + 1), speed * 1000)\n return () => clearTimeout(timer)\n }, [prefersReducedMotion, started, charIndex, flat.length, speed, done])\n\n if (!isValidElement(children)) return children\n\n const childProps = children.props as Record<string, unknown>\n const existingRef = (childProps as { ref?: Ref<HTMLElement> }).ref\n\n // Reduced motion: skip the character-by-character reveal and render the\n // full text immediately in its final resting shape (no cursor).\n const content = prefersReducedMotion\n ? buildSegmentContent(segments, flat.length)\n : started\n ? buildSegmentContent(segments, charIndex)\n : null\n const showCursor = !prefersReducedMotion && cursor && started && !done\n\n return cloneElement(children, {\n ref: mergeRef(elementRef, existingRef),\n children: (\n <>\n {content}\n {showCursor && <BlinkingCursor char={cursorChar} id={id} />}\n </>\n ),\n } as Record<string, unknown>)\n}\n\n// ── Smooth Reveal ────────────────────────────────────────────────────\n\nfunction SmoothReveal({\n text,\n children,\n elementRef,\n id,\n delay,\n duration,\n cursor,\n cursorChar,\n shouldAnimate,\n}: {\n text: string | TextSegment[]\n children: ReactElement\n elementRef: React.RefObject<HTMLElement | null>\n id: string\n delay: number\n duration: number\n cursor: boolean\n cursorChar: string\n shouldAnimate: boolean\n}) {\n const prefersReducedMotion = useReducedMotion()\n const segments = typeof text === \"string\" ? [{ text }] : text\n const [revealed, setRevealed] = useState(false)\n const [done, setDone] = useState(false)\n\n useEffect(() => {\n if (prefersReducedMotion || !shouldAnimate || revealed) return\n const timer = setTimeout(() => setRevealed(true), delay * 1000)\n return () => clearTimeout(timer)\n }, [prefersReducedMotion, shouldAnimate, delay, revealed])\n\n useEffect(() => {\n if (prefersReducedMotion || !revealed) return\n const timer = setTimeout(() => setDone(true), duration * 1000)\n return () => clearTimeout(timer)\n }, [prefersReducedMotion, revealed, duration])\n\n if (!isValidElement(children)) return children\n\n const childProps = children.props as Record<string, unknown>\n const existingRef = (childProps as { ref?: Ref<HTMLElement> }).ref\n const existingStyle = (childProps.style ?? {}) as CSSProperties\n\n const animName = `tw-smooth-${id}`\n const showCursor = !prefersReducedMotion && cursor && revealed && !done\n\n // Full text is always in the DOM — the mask clip animates to reveal it\n const fullContent = segments.map((seg, i) =>\n seg.className ? (\n <span key={i} className={seg.className}>\n {seg.text}\n </span>\n ) : (\n seg.text\n ),\n )\n\n return cloneElement(children, {\n ref: mergeRef(elementRef, existingRef),\n style: {\n ...existingStyle,\n whiteSpace: \"nowrap\" as const,\n overflow: \"hidden\" as const,\n // Reduced motion: skip the mask animation and pin to the reveal's\n // final resting state (fully shown) with no animation or cursor.\n ...(prefersReducedMotion\n ? {\n maxWidth: \"100%\" as const,\n }\n : revealed\n ? {\n animation: `${animName} ${duration}s linear forwards`,\n }\n : {\n maxWidth: 0,\n }),\n },\n children: (\n <>\n <style>{`@keyframes ${animName} { from { max-width: 0; } to { max-width: 100%; } }`}</style>\n {fullContent}\n {showCursor && <BlinkingCursor char={cursorChar} id={id} />}\n </>\n ),\n } as Record<string, unknown>)\n}\n\n// ── Exports ──────────────────────────────────────────────────────────\n\nexport { Typewriter }\nexport type { TypewriterProps, TextSegment }\n"],"mappings":";;;;;AAoDA,SAAS,SACP,aACA,aACA;AACA,SAAQ,OAA2B;AAC/B,cAAgD,UAAU;AAC5D,MAAI,OAAO,gBAAgB,WAAY,aAAY,GAAG;WAC7C,eAAe,OAAO,gBAAgB,SAC3C,aAAgD,UAAU;;;;AAMlE,SAAS,YAAY,MAAsC;AACzD,KAAI,OAAO,SAAS,SAAU,QAAO;AACrC,QAAO,KAAK,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,GAAG;;;AAIzC,SAAS,oBACP,UACA,WACmB;CACnB,MAAM,QAA2B,EAAE;CACnC,IAAI,YAAY;AAEhB,MAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;EACxC,MAAM,MAAM,SAAS;AACrB,MAAI,aAAa,EAAG;EAEpB,MAAM,eAAe,IAAI,KAAK,MAAM,GAAG,UAAU;AACjD,eAAa,aAAa;AAE1B,MAAI,IAAI,UACN,OAAM,KACJ,oBAAC,QAAD;GAAc,WAAW,IAAI;aAC1B;GACI,EAFI,EAEJ,CACR;MAED,OAAM,KAAK,aAAa;;AAI5B,QAAO;;AAKT,SAAS,eAAe,EAAE,MAAM,MAAoC;CAClE,MAAM,OAAO,YAAY;AACzB,QACE,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,QAAD;EACE,eAAA;EACA,OAAO;GACL,WAAW,GAAG,KAAK;GACnB,YAAY;GACb;YAEA;EACI,CAAA,EACP,oBAAC,SAAD,EAAA,UAAQ,cAAc,KAAK,oDAA2D,CAAA,CACrF,EAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCP,SAAS,WAAW,EAClB,MACA,UACA,QAAQ,KACR,QAAQ,GACR,SAAS,MACT,aAAa,KACb,SAAS,OACT,OAAO,MACP,UAAU,UACV,iBAAiB,KACC;CAClB,MAAM,MAAM,OAAoB,KAAK;CACrC,MAAM,KAAK,OAAO,CAAC,QAAQ,MAAM,GAAG;CACpC,MAAM,WAAW,UAAU,KAAK;EAAE;EAAM,QAAQ;EAAS,CAAC;CAE1D,MAAM,gBAAgB,SAAS,WAAW;AAE1C,KAAI,YAAY,SACd,QACE,oBAAC,cAAD;EACQ;EACN,YAAY;EACR;EACG;EACP,UAAU;EACF;EACI;EACG;EAEd;EACY,CAAA;AAInB,QACE,oBAAC,cAAD;EACQ;EACN,YAAY;EACR;EACG;EACA;EACC;EACI;EACG;EAEd;EACY,CAAA;;AAMnB,SAAS,aAAa,EACpB,MACA,UACA,YACA,IACA,OACA,OACA,QACA,YACA,iBAWC;CACD,MAAM,uBAAuB,kBAAkB;CAC/C,MAAM,OAAO,YAAY,KAAK;CAC9B,MAAM,WAAW,OAAO,SAAS,WAAW,CAAC,EAAE,MAAM,CAAC,GAAG;CACzD,MAAM,CAAC,WAAW,gBAAgB,SAAS,EAAE;CAC7C,MAAM,CAAC,SAAS,cAAc,SAAS,MAAM;CAC7C,MAAM,CAAC,MAAM,WAAW,SAAS,MAAM;AAEvC,iBAAgB;AACd,MAAI,wBAAwB,CAAC,iBAAiB,QAAS;EACvD,MAAM,QAAQ,iBAAiB,WAAW,KAAK,EAAE,QAAQ,IAAK;AAC9D,eAAa,aAAa,MAAM;IAC/B;EAAC;EAAsB;EAAe;EAAO;EAAQ,CAAC;AAEzD,iBAAgB;AACd,MAAI,wBAAwB,CAAC,WAAW,KAAM;AAC9C,MAAI,aAAa,KAAK,QAAQ;AAC5B,WAAQ,KAAK;AACb;;EAEF,MAAM,QAAQ,iBAAiB,cAAc,SAAS,OAAO,EAAE,EAAE,QAAQ,IAAK;AAC9E,eAAa,aAAa,MAAM;IAC/B;EAAC;EAAsB;EAAS;EAAW,KAAK;EAAQ;EAAO;EAAK,CAAC;AAExE,KAAI,CAAC,eAAe,SAAS,CAAE,QAAO;CAGtC,MAAM,cADa,SAAS,MACmC;CAI/D,MAAM,UAAU,uBACZ,oBAAoB,UAAU,KAAK,OAAO,GAC1C,UACE,oBAAoB,UAAU,UAAU,GACxC;CACN,MAAM,aAAa,CAAC,wBAAwB,UAAU,WAAW,CAAC;AAElE,QAAO,aAAa,UAAU;EAC5B,KAAK,SAAS,YAAY,YAAY;EACtC,UACE,qBAAA,UAAA,EAAA,UAAA,CACG,SACA,cAAc,oBAAC,gBAAD;GAAgB,MAAM;GAAgB;GAAM,CAAA,CAC1D,EAAA,CAAA;EAEN,CAA4B;;AAK/B,SAAS,aAAa,EACpB,MACA,UACA,YACA,IACA,OACA,UACA,QACA,YACA,iBAWC;CACD,MAAM,uBAAuB,kBAAkB;CAC/C,MAAM,WAAW,OAAO,SAAS,WAAW,CAAC,EAAE,MAAM,CAAC,GAAG;CACzD,MAAM,CAAC,UAAU,eAAe,SAAS,MAAM;CAC/C,MAAM,CAAC,MAAM,WAAW,SAAS,MAAM;AAEvC,iBAAgB;AACd,MAAI,wBAAwB,CAAC,iBAAiB,SAAU;EACxD,MAAM,QAAQ,iBAAiB,YAAY,KAAK,EAAE,QAAQ,IAAK;AAC/D,eAAa,aAAa,MAAM;IAC/B;EAAC;EAAsB;EAAe;EAAO;EAAS,CAAC;AAE1D,iBAAgB;AACd,MAAI,wBAAwB,CAAC,SAAU;EACvC,MAAM,QAAQ,iBAAiB,QAAQ,KAAK,EAAE,WAAW,IAAK;AAC9D,eAAa,aAAa,MAAM;IAC/B;EAAC;EAAsB;EAAU;EAAS,CAAC;AAE9C,KAAI,CAAC,eAAe,SAAS,CAAE,QAAO;CAEtC,MAAM,aAAa,SAAS;CAC5B,MAAM,cAAe,WAA0C;CAC/D,MAAM,gBAAiB,WAAW,SAAS,EAAE;CAE7C,MAAM,WAAW,aAAa;CAC9B,MAAM,aAAa,CAAC,wBAAwB,UAAU,YAAY,CAAC;CAGnE,MAAM,cAAc,SAAS,KAAK,KAAK,MACrC,IAAI,YACF,oBAAC,QAAD;EAAc,WAAW,IAAI;YAC1B,IAAI;EACA,EAFI,EAEJ,GAEP,IAAI,KAEP;AAED,QAAO,aAAa,UAAU;EAC5B,KAAK,SAAS,YAAY,YAAY;EACtC,OAAO;GACL,GAAG;GACH,YAAY;GACZ,UAAU;GAGV,GAAI,uBACA,EACE,UAAU,QACX,GACD,WACE,EACE,WAAW,GAAG,SAAS,GAAG,SAAS,oBACpC,GACD,EACE,UAAU,GACX;GACR;EACD,UACE,qBAAA,UAAA,EAAA,UAAA;GACE,oBAAC,SAAD,EAAA,UAAQ,cAAc,SAAS,sDAA6D,CAAA;GAC3F;GACA,cAAc,oBAAC,gBAAD;IAAgB,MAAM;IAAgB;IAAM,CAAA;GAC1D,EAAA,CAAA;EAEN,CAA4B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waveso/ui",
3
- "version": "0.7.2",
3
+ "version": "0.7.5",
4
4
  "description": "Wave UI component library built on Base UI and Tailwind CSS",
5
5
  "type": "module",
6
6
  "sideEffects": [