@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/blocksPeer Dependencies
This package requires the following peer dependencies:
pnpm add react react-dom next-themes lucide-react motionhls.js (>=1.5) is an optional peer dependency required only by
VideoPlayer; install it in apps that use the player:
pnpm add hls.jsConfiguration
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+localeprops 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
statsprop renders live viewer / VOD reproduction counts- Loading / waiting / error states with retry; errors surfaced via
onError(no bundled reporting) and localized vialabels
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.
