diff --git a/components/index.tsx b/components/index.tsx
index a238f6d08..9d4efcef8 100644
--- a/components/index.tsx
+++ b/components/index.tsx
@@ -74,12 +74,46 @@ export {
type AccessDeniedProps,
} from './vanilla/access-denied'
+// Notification components
+export {
+ NotificationContainer,
+ NotificationItem,
+ useNotificationState,
+ notificationStyles,
+ type NotificationData,
+ type NotificationType,
+ type NotificationPosition,
+ type NotificationContainerProps,
+ type NotificationItemProps,
+} from './vanilla/notifications'
+
+// Status indicator components
+export {
+ StatusBadge,
+ ConnectionStatus,
+ BackendStatus,
+ statusIndicatorStyles,
+ type StatusVariant,
+ type BackendStatusType,
+ type StatusBadgeProps,
+ type ConnectionStatusProps,
+ type BackendStatusProps,
+} from './vanilla/status-indicators'
+
// =============================================================================
// RADIX COMPONENTS (Built on @radix-ui primitives)
// =============================================================================
-// Add Radix component exports here as they are created
-// export * from './radix'
+// Dialog components
+export {
+ KeyboardShortcutsContent,
+ ShortcutRow,
+ getPlatformModifier,
+ createShortcut,
+ type ShortcutItem,
+ type ShortcutCategory,
+ type KeyboardShortcutsDialogProps,
+} from './radix/dialogs/KeyboardShortcutsDialog'
// =============================================================================
// FAKEMUI COMPONENTS (Built on @metabuilder/fakemui)
@@ -145,11 +179,34 @@ export const allStyles = `
animation: skeleton-pulse 1.5s ease-in-out infinite;
}
+/* Notification animations */
+@keyframes notification-slide-in {
+ from {
+ opacity: 0;
+ transform: translateX(100%);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+/* Status indicator animations */
+@keyframes status-pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+@keyframes status-spin {
+ to { transform: rotate(360deg); }
+}
+
/* Accessibility: respect reduced motion preferences */
@media (prefers-reduced-motion: reduce) {
.empty-state-animated,
.skeleton-animate,
- .loading-spinner {
+ .loading-spinner,
+ [data-testid^="notification-"],
+ [data-testid="status-badge"] span {
animation: none;
}
}
diff --git a/components/radix/dialogs/KeyboardShortcutsDialog.tsx b/components/radix/dialogs/KeyboardShortcutsDialog.tsx
new file mode 100644
index 000000000..e558177df
--- /dev/null
+++ b/components/radix/dialogs/KeyboardShortcutsDialog.tsx
@@ -0,0 +1,237 @@
+/**
+ * Keyboard Shortcuts Dialog
+ * Generic dialog for displaying keyboard shortcuts - accepts shortcuts as props
+ *
+ * Usage:
+ * ```tsx
+ * const shortcuts = [
+ * {
+ * category: 'Navigation',
+ * items: [
+ * { keys: ['Ctrl', '1'], description: 'Go to Dashboard' },
+ * { keys: ['Ctrl', 'K'], description: 'Search' },
+ * ]
+ * },
+ * {
+ * category: 'Actions',
+ * items: [
+ * { keys: ['Ctrl', 'S'], description: 'Save' },
+ * ]
+ * }
+ * ]
+ *
+ *
+ * ```
+ */
+
+import React from 'react'
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+export interface ShortcutItem {
+ /** Key combination (e.g., ['Ctrl', 'S'] or ['⌘', 'Shift', 'G']) */
+ keys: string[]
+ /** Description of what the shortcut does */
+ description: string
+}
+
+export interface ShortcutCategory {
+ /** Category name (e.g., 'Navigation', 'Actions', 'Editor') */
+ category: string
+ /** Shortcuts in this category */
+ items: ShortcutItem[]
+}
+
+export interface KeyboardShortcutsDialogProps {
+ /** Whether dialog is open */
+ open: boolean
+ /** Called when open state changes */
+ onOpenChange: (open: boolean) => void
+ /** Grouped shortcuts to display */
+ shortcuts: ShortcutCategory[]
+ /** Dialog title */
+ title?: string
+ /** Dialog description */
+ description?: string
+ /** Icon component to show in header */
+ icon?: React.ReactNode
+ /** Custom className for dialog content */
+ className?: string
+}
+
+// =============================================================================
+// COMPONENTS
+// =============================================================================
+
+/**
+ * Single shortcut row showing keys and description
+ */
+export function ShortcutRow({ keys, description }: ShortcutItem) {
+ return (
+
+
+ {description}
+
+
+ {keys.map((key, index) => (
+
+ {key}
+
+ ))}
+
+
+ )
+}
+
+/**
+ * Keyboard Shortcuts Dialog - generic version that accepts shortcuts as props
+ *
+ * Note: This is a vanilla React component. For Radix Dialog integration,
+ * wrap this content in your project's Dialog components:
+ *
+ * ```tsx
+ * import { Dialog, DialogContent } from '@/components/ui/dialog'
+ *
+ *
+ * ```
+ */
+export function KeyboardShortcutsContent({
+ shortcuts,
+ title = 'Keyboard Shortcuts',
+ description = 'Speed up your workflow with these shortcuts',
+ icon,
+}: Omit) {
+ return (
+
+ {/* Header */}
+
+
+ {icon}
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+ {/* Shortcut categories */}
+
+ {shortcuts.map((category, categoryIndex) => (
+
+
+ {category.category}
+
+
+ {category.items.map((item, itemIndex) => (
+
+ ))}
+
+ {categoryIndex < shortcuts.length - 1 && (
+
+ )}
+
+ ))}
+
+
+ )
+}
+
+/**
+ * Helper to detect platform and return appropriate modifier key
+ */
+export function getPlatformModifier(): string {
+ if (typeof navigator !== 'undefined' && navigator.platform?.includes('Mac')) {
+ return '⌘'
+ }
+ return 'Ctrl'
+}
+
+/**
+ * Helper to create shortcuts with platform-aware modifier
+ */
+export function createShortcut(
+ modifiers: ('ctrl' | 'shift' | 'alt')[],
+ key: string,
+ description: string
+): ShortcutItem {
+ const ctrlKey = getPlatformModifier()
+ const keys = modifiers.map((mod) => {
+ switch (mod) {
+ case 'ctrl':
+ return ctrlKey
+ case 'shift':
+ return 'Shift'
+ case 'alt':
+ return typeof navigator !== 'undefined' && navigator.platform?.includes('Mac')
+ ? '⌥'
+ : 'Alt'
+ }
+ })
+ keys.push(key)
+
+ return { keys, description }
+}
+
+export default KeyboardShortcutsContent
diff --git a/components/radix/index.ts b/components/radix/index.ts
index 9004eddc1..8f86c787d 100644
--- a/components/radix/index.ts
+++ b/components/radix/index.ts
@@ -7,7 +7,18 @@
* Add Radix-based components here as they are created.
*/
-// Placeholder exports - add Radix components as they are created
+// Dialog components
+export {
+ KeyboardShortcutsContent,
+ ShortcutRow,
+ getPlatformModifier,
+ createShortcut,
+ type ShortcutItem,
+ type ShortcutCategory,
+ type KeyboardShortcutsDialogProps,
+} from './dialogs/KeyboardShortcutsDialog'
+
+// Placeholder exports - add more Radix components as they are created
// Example:
// export { Dialog, DialogContent, DialogHeader, DialogFooter } from './dialog'
// export { Button } from './button'
diff --git a/components/vanilla/index.ts b/components/vanilla/index.ts
index 7019ae395..6da7b4f2e 100644
--- a/components/vanilla/index.ts
+++ b/components/vanilla/index.ts
@@ -65,3 +65,29 @@ export {
accessDeniedStyles,
type AccessDeniedProps,
} from './access-denied'
+
+// Notification components
+export {
+ NotificationContainer,
+ NotificationItem,
+ useNotificationState,
+ notificationStyles,
+ type NotificationData,
+ type NotificationType,
+ type NotificationPosition,
+ type NotificationContainerProps,
+ type NotificationItemProps,
+} from './notifications'
+
+// Status indicator components
+export {
+ StatusBadge,
+ ConnectionStatus,
+ BackendStatus,
+ statusIndicatorStyles,
+ type StatusVariant,
+ type BackendStatusType,
+ type StatusBadgeProps,
+ type ConnectionStatusProps,
+ type BackendStatusProps,
+} from './status-indicators'
diff --git a/components/vanilla/notifications/index.tsx b/components/vanilla/notifications/index.tsx
new file mode 100644
index 000000000..22393cd70
--- /dev/null
+++ b/components/vanilla/notifications/index.tsx
@@ -0,0 +1,334 @@
+/**
+ * Notification Components
+ * Generic notification system - framework agnostic (no Redux dependency)
+ *
+ * Usage:
+ * ```tsx
+ * // Basic usage with local state
+ * const [notifications, setNotifications] = useState([])
+ *
+ * setNotifications(prev => prev.filter(n => n.id !== id))}
+ * />
+ *
+ * // With position
+ *
+ * ```
+ */
+
+import React, { useEffect, useCallback } from 'react'
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+export type NotificationType = 'success' | 'error' | 'warning' | 'info'
+
+export type NotificationPosition =
+ | 'top-left'
+ | 'top-center'
+ | 'top-right'
+ | 'bottom-left'
+ | 'bottom-center'
+ | 'bottom-right'
+
+export interface NotificationData {
+ id: string
+ type: NotificationType
+ message: string
+ title?: string
+ duration?: number // ms, 0 = no auto-dismiss
+}
+
+export interface NotificationContainerProps {
+ /** Array of notifications to display */
+ notifications: NotificationData[]
+ /** Called when a notification should be removed */
+ onClose: (id: string) => void
+ /** Position on screen */
+ position?: NotificationPosition
+ /** Maximum notifications to show at once */
+ maxVisible?: number
+ /** Custom className for container */
+ className?: string
+}
+
+export interface NotificationItemProps {
+ notification: NotificationData
+ onClose: () => void
+}
+
+// =============================================================================
+// STYLES
+// =============================================================================
+
+const typeStyles: Record = {
+ success: {
+ bg: 'rgba(46, 125, 50, 0.1)',
+ border: 'var(--color-success, #2e7d32)',
+ text: 'var(--color-success, #2e7d32)',
+ },
+ error: {
+ bg: 'rgba(211, 47, 47, 0.1)',
+ border: 'var(--color-error, #d32f2f)',
+ text: 'var(--color-error, #d32f2f)',
+ },
+ warning: {
+ bg: 'rgba(245, 127, 0, 0.1)',
+ border: 'var(--color-warning, #f57f00)',
+ text: 'var(--color-warning, #f57f00)',
+ },
+ info: {
+ bg: 'rgba(2, 136, 209, 0.1)',
+ border: 'var(--color-info, #0288d1)',
+ text: 'var(--color-info, #0288d1)',
+ },
+}
+
+const positionStyles: Record = {
+ 'top-left': { top: 16, left: 16 },
+ 'top-center': { top: 16, left: '50%', transform: 'translateX(-50%)' },
+ 'top-right': { top: 16, right: 16 },
+ 'bottom-left': { bottom: 16, left: 16 },
+ 'bottom-center': { bottom: 16, left: '50%', transform: 'translateX(-50%)' },
+ 'bottom-right': { bottom: 16, right: 16 },
+}
+
+// =============================================================================
+// ICONS
+// =============================================================================
+
+const SuccessIcon = () => (
+
+)
+
+const ErrorIcon = () => (
+
+)
+
+const WarningIcon = () => (
+
+)
+
+const InfoIcon = () => (
+
+)
+
+const CloseIcon = () => (
+
+)
+
+const typeIcons: Record = {
+ success: SuccessIcon,
+ error: ErrorIcon,
+ warning: WarningIcon,
+ info: InfoIcon,
+}
+
+// =============================================================================
+// COMPONENTS
+// =============================================================================
+
+/**
+ * Individual notification item
+ */
+export const NotificationItem: React.FC = ({ notification, onClose }) => {
+ const { type, message, title, duration } = notification
+ const styles = typeStyles[type]
+ const Icon = typeIcons[type]
+
+ useEffect(() => {
+ if (duration && duration > 0) {
+ const timer = setTimeout(onClose, duration)
+ return () => clearTimeout(timer)
+ }
+ }, [duration, onClose])
+
+ return (
+
+
+
+
+
+ {title && (
+
{title}
+ )}
+
{message}
+
+
+
+ )
+}
+
+/**
+ * Container for notifications - renders all active notifications
+ */
+export const NotificationContainer: React.FC = ({
+ notifications,
+ onClose,
+ position = 'top-right',
+ maxVisible = 5,
+ className,
+}) => {
+ const handleClose = useCallback(
+ (id: string) => {
+ onClose(id)
+ },
+ [onClose]
+ )
+
+ const visibleNotifications = notifications.slice(0, maxVisible)
+
+ if (visibleNotifications.length === 0) {
+ return null
+ }
+
+ return (
+
+ {visibleNotifications.map((notification) => (
+
+ handleClose(notification.id)}
+ />
+
+ ))}
+
+ )
+}
+
+// =============================================================================
+// STYLES (CSS-in-JS keyframes)
+// =============================================================================
+
+export const notificationStyles = `
+@keyframes notification-slide-in {
+ from {
+ opacity: 0;
+ transform: translateX(100%);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ [data-testid^="notification-"] {
+ animation: none;
+ }
+}
+`
+
+// =============================================================================
+// HOOK FOR NOTIFICATION STATE MANAGEMENT
+// =============================================================================
+
+/**
+ * Hook for managing notification state (optional - can use Redux instead)
+ *
+ * @example
+ * const { notifications, addNotification, removeNotification, clearAll } = useNotificationState()
+ *
+ * // Add a notification
+ * addNotification({ type: 'success', message: 'Saved!', duration: 3000 })
+ *
+ * // Render
+ *
+ */
+export function useNotificationState(initialNotifications: NotificationData[] = []) {
+ const [notifications, setNotifications] = React.useState(initialNotifications)
+
+ const addNotification = useCallback((notification: Omit) => {
+ const id = `notification-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
+ setNotifications((prev) => [...prev, { ...notification, id }])
+ return id
+ }, [])
+
+ const removeNotification = useCallback((id: string) => {
+ setNotifications((prev) => prev.filter((n) => n.id !== id))
+ }, [])
+
+ const clearAll = useCallback(() => {
+ setNotifications([])
+ }, [])
+
+ return {
+ notifications,
+ addNotification,
+ removeNotification,
+ clearAll,
+ }
+}
+
+export default NotificationContainer
diff --git a/components/vanilla/status-indicators/index.tsx b/components/vanilla/status-indicators/index.tsx
new file mode 100644
index 000000000..e80946902
--- /dev/null
+++ b/components/vanilla/status-indicators/index.tsx
@@ -0,0 +1,328 @@
+/**
+ * Status Indicator Components
+ * Generic components for displaying connection/backend status
+ *
+ * Usage:
+ * ```tsx
+ * // Basic connection status
+ *
+ *
+ * // Backend indicator with custom states
+ *
+ *
+ * // Custom status badge
+ * }
+ * />
+ * ```
+ */
+
+import React from 'react'
+
+// =============================================================================
+// TYPES
+// =============================================================================
+
+export type StatusVariant = 'success' | 'error' | 'warning' | 'info' | 'neutral'
+
+export type BackendStatusType = 'connected' | 'disconnected' | 'connecting' | 'error'
+
+export interface StatusBadgeProps {
+ /** Visual variant */
+ variant: StatusVariant
+ /** Label text */
+ label: string
+ /** Optional icon (renders before label) */
+ icon?: React.ReactNode
+ /** Show pulsing dot indicator */
+ showDot?: boolean
+ /** Additional className */
+ className?: string
+ /** Tooltip text */
+ tooltip?: string
+}
+
+export interface ConnectionStatusProps {
+ /** Whether connected */
+ isConnected: boolean
+ /** Label for connected state */
+ connectedLabel?: string
+ /** Label for disconnected state */
+ disconnectedLabel?: string
+ /** Show icon */
+ showIcon?: boolean
+ /** Additional className */
+ className?: string
+}
+
+export interface BackendStatusProps {
+ /** Current status */
+ status: BackendStatusType
+ /** Backend name/label */
+ label?: string
+ /** Show pulsing activity dot */
+ showDot?: boolean
+ /** Tooltip for connected state */
+ connectedTooltip?: string
+ /** Tooltip for disconnected state */
+ disconnectedTooltip?: string
+ /** Additional className */
+ className?: string
+}
+
+// =============================================================================
+// STYLES
+// =============================================================================
+
+const variantStyles: Record = {
+ success: {
+ bg: 'rgba(46, 125, 50, 0.1)',
+ border: 'rgba(46, 125, 50, 0.3)',
+ text: 'var(--color-success, #2e7d32)',
+ dot: 'var(--color-success, #2e7d32)',
+ },
+ error: {
+ bg: 'rgba(211, 47, 47, 0.1)',
+ border: 'rgba(211, 47, 47, 0.3)',
+ text: 'var(--color-error, #d32f2f)',
+ dot: 'var(--color-error, #d32f2f)',
+ },
+ warning: {
+ bg: 'rgba(245, 127, 0, 0.1)',
+ border: 'rgba(245, 127, 0, 0.3)',
+ text: 'var(--color-warning, #f57f00)',
+ dot: 'var(--color-warning, #f57f00)',
+ },
+ info: {
+ bg: 'rgba(2, 136, 209, 0.1)',
+ border: 'rgba(2, 136, 209, 0.3)',
+ text: 'var(--color-info, #0288d1)',
+ dot: 'var(--color-info, #0288d1)',
+ },
+ neutral: {
+ bg: 'rgba(128, 128, 128, 0.1)',
+ border: 'rgba(128, 128, 128, 0.3)',
+ text: 'var(--color-muted, #666)',
+ dot: 'var(--color-muted, #666)',
+ },
+}
+
+const statusToVariant: Record = {
+ connected: 'success',
+ disconnected: 'neutral',
+ connecting: 'info',
+ error: 'error',
+}
+
+// =============================================================================
+// ICONS
+// =============================================================================
+
+const CloudIcon = () => (
+
+)
+
+const CloudOffIcon = () => (
+
+)
+
+const DatabaseIcon = () => (
+
+)
+
+const SpinnerIcon = () => (
+
+)
+
+// =============================================================================
+// COMPONENTS
+// =============================================================================
+
+/**
+ * Generic status badge component
+ */
+export const StatusBadge: React.FC = ({
+ variant,
+ label,
+ icon,
+ showDot,
+ className,
+ tooltip,
+}) => {
+ const styles = variantStyles[variant]
+
+ const badge = (
+
+ {icon && {icon}}
+ {label}
+ {showDot && (
+
+ )}
+
+ )
+
+ if (tooltip) {
+ return {badge}
+ }
+
+ return badge
+}
+
+/**
+ * Simple connected/disconnected status indicator
+ */
+export const ConnectionStatus: React.FC = ({
+ isConnected,
+ connectedLabel = 'Connected',
+ disconnectedLabel = 'Disconnected',
+ showIcon = true,
+ className,
+}) => {
+ return (
+ : ) : undefined}
+ showDot={isConnected}
+ className={className}
+ />
+ )
+}
+
+/**
+ * Backend status indicator with multiple states
+ */
+export const BackendStatus: React.FC = ({
+ status,
+ label,
+ showDot = true,
+ connectedTooltip = 'Connected to backend',
+ disconnectedTooltip = 'Using local storage',
+ className,
+}) => {
+ const variant = statusToVariant[status]
+
+ const getIcon = () => {
+ switch (status) {
+ case 'connected':
+ return
+ case 'disconnected':
+ return
+ case 'connecting':
+ return
+ case 'error':
+ return
+ }
+ }
+
+ const getLabel = () => {
+ if (label) return label
+ switch (status) {
+ case 'connected':
+ return 'Connected'
+ case 'disconnected':
+ return 'Local'
+ case 'connecting':
+ return 'Connecting...'
+ case 'error':
+ return 'Error'
+ }
+ }
+
+ const getTooltip = () => {
+ switch (status) {
+ case 'connected':
+ return connectedTooltip
+ case 'disconnected':
+ return disconnectedTooltip
+ case 'connecting':
+ return 'Establishing connection...'
+ case 'error':
+ return 'Connection failed'
+ }
+ }
+
+ return (
+
+ )
+}
+
+// =============================================================================
+// STYLES (CSS-in-JS keyframes)
+// =============================================================================
+
+export const statusIndicatorStyles = `
+@keyframes status-pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+@keyframes status-spin {
+ to { transform: rotate(360deg); }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ [data-testid="status-badge"] span {
+ animation: none;
+ }
+}
+`
+
+export default StatusBadge