npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@algenium/blocks

v1.22.0

Published

Shared UI components for Algenium applications

Readme

@algenium/blocks

Shared UI components for Algenium applications

A collection of reusable, accessible UI components built with React, Radix UI, and Tailwind CSS. Designed for use across all Algenium applications.

Installation

npm install @algenium/blocks
# or
pnpm add @algenium/blocks

Peer Dependencies

This package requires the following peer dependencies:

pnpm add react react-dom next-themes lucide-react motion

hls.js (>=1.5) is an optional peer dependency required only by VideoPlayer; install it in apps that use the player:

pnpm add hls.js

Configuration

Tailwind CSS

Configure your Tailwind CSS to include this package:

// tailwind.config.js
module.exports = {
	content: [
		// ... your other content paths
		"./node_modules/@algenium/blocks/dist/**/*.js",
	],
	// ... rest of your config
};

Components

ThemeSwitcher

A theme switcher component that integrates with next-themes. Supports light, dark, and system themes.

Features:

  • Multiple variants (default, mini)
  • Multiple sizes (sm, md, lg)
  • Multiple shapes (rounded, pill)
  • Internationalization support

Usage:

import { ThemeSwitcher } from "@algenium/blocks";

// Default usage
<ThemeSwitcher />;

// With custom labels (for i18n)
<ThemeSwitcher
	labels={{
		theme: "Theme",
		system: "System",
		light: "Light",
		dark: "Dark",
	}}
/>;

// Mini variant (icon only with dropdown)
<ThemeSwitcher variant="mini" />;

// Different sizes and shapes
<ThemeSwitcher size="sm" shape="pill" />;

Props:

interface ThemeSwitcherProps {
	variant?: "default" | "mini";
	size?: "sm" | "md" | "lg";
	shape?: "rounded" | "pill";
	showIcon?: boolean;
	labels?: ThemeSwitcherLabels;
	className?: string;
}

LanguageSwitcher

A language switcher component with support for multiple languages and display variants.

Features:

  • Multiple variants (default, mini)
  • Multiple sizes (sm, md, lg)
  • Dropdown positioning control
  • Language flag/icon support
  • Internationalization support

Usage:

import { LanguageSwitcher } from "@algenium/blocks";

const languages = [
	{ key: "en", label: "EN", nativeName: "English" },
	{ key: "es", label: "ES", nativeName: "Español" },
];

<LanguageSwitcher
	languages={languages}
	currentLanguage="en"
	onLanguageChange={(key) => setLanguage(key)}
	labels={{ language: "Language" }}
/>;

// Mini variant
<LanguageSwitcher
	variant="mini"
	languages={languages}
	currentLanguage="en"
	onLanguageChange={(key) => setLanguage(key)}
/>;

// With icon
<LanguageSwitcher
	languages={languages}
	currentLanguage="en"
	onLanguageChange={(key) => setLanguage(key)}
	showIcon
/>;

Props:

interface LanguageSwitcherProps {
	languages: Language[];
	currentLanguage: string;
	onLanguageChange: (languageKey: string) => void;
	variant?: "default" | "mini";
	size?: "sm" | "md" | "lg";
	dropdownAlign?: "start" | "center" | "end";
	showIcon?: boolean;
	labels?: LanguageSwitcherLabels;
	className?: string;
}

interface Language {
	key: string;
	label: string;
	nativeName: string;
}

AvatarEditor

An interactive avatar editor with zoom, rotation, and drag controls.

Features:

  • Image zoom via UI buttons and slider (0.5x - 3x)
  • Image rotation in 15° increments (0° - 360°)
  • Drag to reposition image
  • Real-time preview with circular crop
  • Customizable editor size
  • Optional grid overlay for alignment
  • Reset all transforms
  • Touch-optimized controls for mobile

Usage:

import { AvatarEditor } from "@algenium/blocks";

<AvatarEditor
	value={avatarData}
	onChange={(editedImage) => {
		console.log("Edited image:", editedImage);
	}}
/>;

// With custom size and grid
<AvatarEditor
	value={avatarData}
	onChange={setAvatarData}
	size={280}
	showGrid={true}
/>;

// With custom output settings
<AvatarEditor
	value={avatarData}
	onChange={setAvatarData}
	outputSize={512}
	outputFormat="jpeg"
	outputQuality={0.95}
/>;

// With large controls for mobile/touch
<AvatarEditor
	value={avatarData}
	onChange={setAvatarData}
	controlSize="large"
/>;

Props:

interface AvatarEditorProps {
	value?: string | null;
	onChange?: (dataUrl: string | null) => void;
	size?: number;
	showGrid?: boolean;
	outputSize?: number;
	outputFormat?: "png" | "jpeg" | "webp";
	outputQuality?: number;
	controlSize?: "default" | "large";
	className?: string;
}

AvatarEditorDialog

A complete avatar editing experience with preview display and edit dialog.

Features:

  • Large avatar display with edit button
  • Opens fullscreen drawer on mobile, modal on desktop
  • Integrated AvatarEditor with all controls
  • Controlled component (value/onChange)
  • Optional async save handler with feedback
  • Success/error feedback messages
  • Customizable sizes and labels
  • Accessibility optimized

Usage:

import { AvatarEditorDialog } from "@algenium/blocks";

// Basic usage with value/onChange
<AvatarEditorDialog value={avatar} onChange={setAvatar} />;

// With async save handler
<AvatarEditorDialog
	value={avatar}
	onChange={setAvatar}
	onSave={async (dataUrl) => {
		const result = await uploadAvatar(dataUrl);
		return result.success;
	}}
	dialogTitle="Edit Profile Picture"
	acceptText="Save"
	cancelText="Cancel"
	successMessage="Avatar saved successfully!"
	errorMessage="Failed to save avatar"
/>;

// With custom sizes
<AvatarEditorDialog
	value={avatar}
	onChange={setAvatar}
	displaySize={120}
	editorSize={280}
	outputSize={512}
/>;

Props:

interface AvatarEditorDialogProps {
	value?: string | null;
	onChange?: (dataUrl: string | null) => void;
	onSave?: (dataUrl: string) => Promise<boolean> | boolean;
	displaySize?: number;
	editorSize?: number;
	outputSize?: number;
	placeholder?: string;
	editLabel?: string;
	dialogTitle?: string;
	acceptText?: string;
	cancelText?: string;
	successMessage?: string;
	errorMessage?: string;
	className?: string;
}

NotificationsWidget

A compact notification bell widget with popover display, sound alerts, and animations.

Features:

  • Bell icon with unread count badge
  • Popover with scrollable notification list
  • Multiple notification types (info, success, warning, error)
  • Sound notifications (chime, bell, pop, ding)
  • Pulse animations (ring, glow, bounce)
  • Mark as read/dismiss actions
  • Click-to-navigate support
  • Customizable colors, sizes, and sounds

Usage:

import { NotificationsWidget, Notification } from "@algenium/blocks";

const notifications: Notification[] = [
	{
		id: "1",
		title: "New message",
		message: "You have a new message from John",
		type: "info",
		timestamp: new Date(),
		read: false,
	},
	{
		id: "2",
		title: "Task completed",
		message: "Your export task finished successfully",
		type: "success",
		timestamp: new Date(Date.now() - 1000 * 60 * 5),
		read: false,
	},
];

<NotificationsWidget
	notifications={notifications}
	onMarkAsRead={(id) => markAsRead(id)}
	onMarkAllAsRead={() => markAllAsRead()}
	onDismiss={(id) => dismiss(id)}
	onClearAll={() => clearAll()}
	onNotificationClick={(notification) => {
		if (notification.href) {
			router.push(notification.href);
		}
	}}
	playSound
	soundType="chime"
	size="md"
	dotColor="red"
	pulseStyle="ring"
/>;

// Minimal usage
<NotificationsWidget
	notifications={notifications}
	onMarkAsRead={(id) => markAsRead(id)}
/>;

// Custom configuration
<NotificationsWidget
	notifications={notifications}
	onMarkAsRead={(id) => markAsRead(id)}
	size="lg"
	dotColor="primary"
	soundType="bell"
	pulseStyle="glow"
	maxVisible={10}
	title="Alerts"
	emptyMessage="No alerts"
/>;

Props:

interface NotificationsWidgetProps {
	notifications: Notification[];
	onMarkAsRead?: (id: string) => void;
	onMarkAllAsRead?: () => void;
	onDismiss?: (id: string) => void;
	onClearAll?: () => void;
	onNotificationClick?: (notification: Notification) => void;
	size?: "sm" | "md" | "lg";
	maxVisible?: number; // Default: 5
	playSound?: boolean; // Default: true
	soundUrl?: string; // Custom sound file URL
	soundType?: "chime" | "bell" | "pop" | "ding" | "none"; // Default: "chime"
	soundCooldown?: number; // Default: 2000ms
	className?: string;
	emptyMessage?: string; // Default: "No notifications"
	title?: string; // Default: "Notifications"
	dotColor?: "red" | "blue" | "green" | "amber" | "purple" | "primary"; // Default: "red"
	showPulse?: boolean; // Default: true
	pulseStyle?: "ring" | "glow" | "bounce" | "none"; // Default: "ring"
}

interface Notification {
	id: string;
	title: string;
	message?: string;
	type?: "info" | "success" | "warning" | "error"; // Default: "info"
	timestamp: Date;
	read?: boolean;
	href?: string; // Optional link for click navigation
}

EmojiPicker

A zero-dependency emoji picker with bilingual (English + Spanish) search, category navigation, a persisted "frequently used" section, a global skin-tone preference, and desktop-first keyboard navigation. The emoji dataset (~1,900 emoji, Unicode ≤ 15.1) is generated at build time from Unicode CLDR annotations and committed to the repo, so consuming apps ship no extra runtime dependencies. Because the package is sideEffects: false and tree-shaken, apps that never import EmojiPicker don't bundle the data at all.

Features:

  • Bilingual search: "corazón", "corazon" and "heart" all match ❤️
  • 9 categories with an icon tab bar and scroll-to-section
  • "Frequently used" recents, persisted in localStorage
  • Global skin-tone selector (applied to capable emoji), persisted
  • Keyboard navigation: arrows move, Enter/Space selects
  • labels + locale props for i18n; search is always bilingual

Usage:

import { useRef, useState } from "react";
import { EmojiPickerPopover } from "@algenium/blocks";

function Composer() {
	const ref = useRef<HTMLTextAreaElement>(null);
	const [value, setValue] = useState("");

	function insert(emoji: string) {
		const el = ref.current;
		if (!el) return setValue((v) => v + emoji);
		const start = el.selectionStart ?? value.length;
		const end = el.selectionEnd ?? value.length;
		setValue(value.slice(0, start) + emoji + value.slice(end));
	}

	return (
		<div className="flex items-end gap-2">
			<textarea
				ref={ref}
				value={value}
				onChange={(e) => setValue(e.target.value)}
			/>
			<EmojiPickerPopover locale="es" onSelect={insert} />
		</div>
	);
}

Use the standalone panel when you want to place it inside your own anchor:

import { EmojiPicker } from "@algenium/blocks";

<EmojiPicker locale="es" onSelect={(emoji) => insert(emoji)} />;

Props (EmojiPicker):

interface EmojiPickerProps {
	onSelect: (emoji: string) => void;
	locale?: "en" | "es"; // Section titles + skin-tone names. Default "en"
	labels?: EmojiPickerLabels; // Overrides individual strings / category titles
	className?: string;
	recentLimit?: number; // Default 24
	storageKey?: string | null; // localStorage namespace; null disables persistence
}

EmojiPickerPopover accepts every EmojiPicker prop plus triggerLabel, triggerClassName, side, align, open/onOpenChange (controlled), and keepOpenOnSelect.


VideoPlayer

A standardized HLS video player with a fully custom control bar (no native controls), for both live streams and on-demand recordings. hls.js is an optional peer dependency loaded on demand via dynamic import — apps that already depend on it (app, admin, media) bundle nothing new, and Safari falls back to native HLS.

Features:

  • Click-to-play poster (attaches HLS only after the user gesture)
  • Play/pause, mute + hover volume slider, keyboard shortcuts
  • Resolution picker (Auto + hls.js levels), playback speed (VOD)
  • Picture-in-Picture and fullscreen (menus stay usable in fullscreen)
  • VOD: seekbar with buffered ranges + current/total time
  • Live: red pulsing badge, no seekbar, and a "go live" affordance when the playhead drifts behind the edge
  • stats prop renders live viewer / VOD reproduction counts
  • Loading / waiting / error states with retry; errors surfaced via onError (no bundled reporting) and localized via labels

Usage:

import { VideoPlayer, usePlaybackStats } from "@algenium/blocks";

function Recording({ mediaId, src, poster }: Props) {
	const stats = usePlaybackStats(
		`https://media.example.com/playback/${mediaId}/stats`,
	);

	return (
		<VideoPlayer
			src={src}
			mode="vod"
			poster={poster}
			stats={stats}
			labels={{ live: "EN VIVO", formatViews: (n) => `${n} reproducciones` }}
			onFirstPlay={() => {
				void fetch(`https://media.example.com/playback/${mediaId}/view`, {
					method: "POST",
				});
			}}
			onError={(e) => reportToSentry(e)}
		/>
	);
}

For live playback pass mode="live" and, typically, autoPlay muted.

Props:

interface VideoPlayerProps {
	src: string;
	mode: "live" | "vod";
	poster?: string;
	autoPlay?: boolean;
	muted?: boolean;
	stats?: { viewers?: number; views?: number };
	labels?: VideoPlayerLabels;
	showQualitySelector?: boolean; // Default true
	onPlayingChange?: (playing: boolean) => void;
	onFirstPlay?: () => void; // Fires once — wire your view beacon here
	onError?: (error: DescribedPlaybackError) => void;
	className?: string;
}

Also exported: useHlsPlayback (the attach/detach hook), usePlaybackStats (polls a stats endpoint), createLiveHlsConfig / createVodHlsConfig, and describePlaybackError.


UI Primitives

The package also exports underlying UI primitives that can be used independently:

Button

import { Button } from "@algenium/blocks";

<Button variant="default">Click me</Button>;
<Button variant="outline" size="sm">
	Small
</Button>;
<Button variant="ghost" size="icon">
	<Icon />
</Button>;

Dialog

import {
	Dialog,
	DialogTrigger,
	DialogContent,
	DialogHeader,
	DialogTitle,
	DialogDescription,
	DialogFooter,
} from "@algenium/blocks";

<Dialog>
	<DialogTrigger>Open</DialogTrigger>
	<DialogContent>
		<DialogHeader>
			<DialogTitle>Title</DialogTitle>
			<DialogDescription>Description</DialogDescription>
		</DialogHeader>
		{/* Content */}
		<DialogFooter>{/* Actions */}</DialogFooter>
	</DialogContent>
</Dialog>;

Drawer

import {
	Drawer,
	DrawerTrigger,
	DrawerContent,
	DrawerHeader,
	DrawerTitle,
} from "@algenium/blocks";

<Drawer>
	<DrawerTrigger>Open</DrawerTrigger>
	<DrawerContent>
		<DrawerHeader>
			<DrawerTitle>Title</DrawerTitle>
		</DrawerHeader>
		{/* Content */}
	</DrawerContent>
</Drawer>;

DropdownMenu

import {
	DropdownMenu,
	DropdownMenuTrigger,
	DropdownMenuContent,
	DropdownMenuItem,
} from "@algenium/blocks";

<DropdownMenu>
	<DropdownMenuTrigger>Open</DropdownMenuTrigger>
	<DropdownMenuContent>
		<DropdownMenuItem>Item 1</DropdownMenuItem>
		<DropdownMenuItem>Item 2</DropdownMenuItem>
	</DropdownMenuContent>
</DropdownMenu>;

Popover

import { Popover, PopoverTrigger, PopoverContent } from "@algenium/blocks";

<Popover>
	<PopoverTrigger>Open</PopoverTrigger>
	<PopoverContent>Content here</PopoverContent>
</Popover>;

ScrollArea

import { ScrollArea } from "@algenium/blocks";

<ScrollArea className="h-[200px]">{/* Scrollable content */}</ScrollArea>;

Slider

import { Slider } from "@algenium/blocks";

<Slider
	value={[50]}
	onValueChange={(value) => setValue(value[0])}
	max={100}
	step={1}
/>;

Toggle

import { Toggle } from "@algenium/blocks";

<Toggle pressed={isPressed} onPressedChange={setIsPressed}>
	Toggle me
</Toggle>;

Tooltip

import {
	Tooltip,
	TooltipTrigger,
	TooltipContent,
	TooltipProvider,
} from "@algenium/blocks";

<TooltipProvider>
	<Tooltip>
		<TooltipTrigger>Hover me</TooltipTrigger>
		<TooltipContent>Tooltip text</TooltipContent>
	</Tooltip>
</TooltipProvider>;

Utilities

cn (Class Name utility)

A utility function for conditionally merging Tailwind CSS classes.

import { cn } from "@algenium/blocks";

const className = cn("base-class", condition && "conditional-class", {
	"another-class": someCondition,
});

TypeScript Support

All components are fully typed with TypeScript. Import types as needed:

import type {
	ThemeSwitcherProps,
	LanguageSwitcherProps,
	Language,
	AvatarEditorProps,
	AvatarEditorDialogProps,
	NotificationsWidgetProps,
	Notification,
	NotificationType,
} from "@algenium/blocks";

Best Practices

Internationalization

All text-based components accept label props for i18n:

import { useTranslation } from "your-i18n-library";

const { t } = useTranslation();

<ThemeSwitcher
	labels={{
		theme: t("theme"),
		light: t("light"),
		dark: t("dark"),
		system: t("system"),
	}}
/>;

Theme Integration

Components use CSS variables for theming. Ensure your app provides these variables:

:root {
	--background: 0 0% 100%;
	--foreground: 222.2 84% 4.9%;
	--primary: 222.2 47.4% 11.2%;
	--muted: 210 40% 96.1%;
	--border: 214.3 31.8% 91.4%;
	/* ... other variables */
}

Notification Management

For NotificationsWidget, implement a notification management system:

// Example notification manager
const [notifications, setNotifications] = useState<Notification[]>([]);

const markAsRead = (id: string) => {
	setNotifications((prev) =>
		prev.map((n) => (n.id === id ? { ...n, read: true } : n)),
	);
};

const dismiss = (id: string) => {
	setNotifications((prev) => prev.filter((n) => n.id !== id));
};

const addNotification = (notification: Omit<Notification, "id">) => {
	const id = crypto.randomUUID();
	setNotifications((prev) => [{ ...notification, id }, ...prev]);
};

Contributing

This package is maintained as part of the Algenium monorepo. See the main repository for contribution guidelines.

License

Proprietary - Algenium © 2026

Version

Current version: 1.0.0-rc.4

Support

For issues or questions, contact the Algenium development team or open an issue in the repository.