@memelabui/ui 0.4.0 → 0.5.0

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/README.md CHANGED
@@ -121,6 +121,7 @@ function App() {
121
121
  | `Tooltip` | Hover/focus tooltip with portal positioning |
122
122
  | `Dropdown` | Compound menu (Trigger, Menu, Item, Separator) |
123
123
  | `MutationOverlay` | Saving/saved/error status overlay for cards |
124
+ | `NotificationBell` | Notification bell button with unread count badge and ping animation |
124
125
 
125
126
  ### Layout
126
127
 
@@ -147,6 +148,9 @@ function App() {
147
148
  | `useDisclosure` | Open/close state management |
148
149
  | `useMediaQuery` | Responsive media query listener |
149
150
  | `useDebounce` | Debounced value |
151
+ | `useHotkeys` | Global keyboard shortcuts with modifier support |
152
+ | `useIntersectionObserver` | IntersectionObserver for infinite scroll and lazy loading |
153
+ | `useSharedNow` | Reactive current-time for countdowns and "X ago" labels |
150
154
 
151
155
  ## Customization
152
156
 
package/dist/index.cjs CHANGED
@@ -130,6 +130,82 @@ function useDebounce(value, delayMs = 300) {
130
130
  }, [value, delayMs]);
131
131
  return debouncedValue;
132
132
  }
133
+ function matchModifiers(e, mods) {
134
+ const ctrl = mods?.ctrl ?? false;
135
+ const shift = mods?.shift ?? false;
136
+ const alt = mods?.alt ?? false;
137
+ const meta = mods?.meta ?? false;
138
+ return e.ctrlKey === ctrl && e.shiftKey === shift && e.altKey === alt && e.metaKey === meta;
139
+ }
140
+ function useHotkeys(bindings, options = {}) {
141
+ const { enabled = true } = options;
142
+ React.useEffect(() => {
143
+ if (!enabled) return;
144
+ const onKeyDown = (e) => {
145
+ for (const binding of bindings) {
146
+ if (e.key === binding.key && matchModifiers(e, binding.modifiers)) {
147
+ binding.handler(e);
148
+ }
149
+ }
150
+ };
151
+ document.addEventListener("keydown", onKeyDown);
152
+ return () => document.removeEventListener("keydown", onKeyDown);
153
+ }, [enabled, ...bindings]);
154
+ }
155
+ function useIntersectionObserver(options = {}) {
156
+ const { root = null, rootMargin = "0px", threshold = 0, enabled = true } = options;
157
+ const [entry, setEntry] = React.useState(null);
158
+ const nodeRef = React.useRef(null);
159
+ const observerRef = React.useRef(null);
160
+ React.useEffect(() => {
161
+ if (!enabled) {
162
+ setEntry(null);
163
+ return;
164
+ }
165
+ observerRef.current = new IntersectionObserver(
166
+ ([e]) => setEntry(e),
167
+ { root, rootMargin, threshold }
168
+ );
169
+ if (nodeRef.current) {
170
+ observerRef.current.observe(nodeRef.current);
171
+ }
172
+ return () => {
173
+ observerRef.current?.disconnect();
174
+ observerRef.current = null;
175
+ };
176
+ }, [enabled, root, rootMargin, JSON.stringify(threshold)]);
177
+ const ref = (node) => {
178
+ if (nodeRef.current) {
179
+ observerRef.current?.unobserve(nodeRef.current);
180
+ }
181
+ nodeRef.current = node;
182
+ if (node) {
183
+ observerRef.current?.observe(node);
184
+ }
185
+ };
186
+ return {
187
+ ref,
188
+ entry,
189
+ isIntersecting: entry?.isIntersecting ?? false
190
+ };
191
+ }
192
+ function useSharedNow(options = {}) {
193
+ const { interval = 1e3, untilMs, enabled = true } = options;
194
+ const [now, setNow] = React.useState(Date.now);
195
+ React.useEffect(() => {
196
+ if (!enabled) return;
197
+ if (untilMs !== void 0 && Date.now() >= untilMs) return;
198
+ const id = setInterval(() => {
199
+ const current = Date.now();
200
+ setNow(current);
201
+ if (untilMs !== void 0 && current >= untilMs) {
202
+ clearInterval(id);
203
+ }
204
+ }, interval);
205
+ return () => clearInterval(id);
206
+ }, [interval, untilMs, enabled]);
207
+ return now;
208
+ }
133
209
 
134
210
  // src/tokens/colors.ts
135
211
  var colors = {
@@ -3330,6 +3406,76 @@ function MutationOverlay({
3330
3406
  }
3331
3407
  );
3332
3408
  }
3409
+ var sizeClass6 = {
3410
+ sm: "w-8 h-8",
3411
+ md: "w-10 h-10",
3412
+ lg: "w-12 h-12"
3413
+ };
3414
+ var iconSizeClass = {
3415
+ sm: "w-4 h-4",
3416
+ md: "w-5 h-5",
3417
+ lg: "w-6 h-6"
3418
+ };
3419
+ var badgeSizeClass = {
3420
+ sm: "min-w-[16px] h-4 text-[10px] px-1",
3421
+ md: "min-w-[18px] h-[18px] text-[11px] px-1",
3422
+ lg: "min-w-[20px] h-5 text-xs px-1.5"
3423
+ };
3424
+ var DefaultBellIcon = ({ className }) => /* @__PURE__ */ jsxRuntime.jsxs(
3425
+ "svg",
3426
+ {
3427
+ xmlns: "http://www.w3.org/2000/svg",
3428
+ viewBox: "0 0 24 24",
3429
+ fill: "none",
3430
+ stroke: "currentColor",
3431
+ strokeWidth: 2,
3432
+ strokeLinecap: "round",
3433
+ strokeLinejoin: "round",
3434
+ className,
3435
+ "aria-hidden": "true",
3436
+ children: [
3437
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }),
3438
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" })
3439
+ ]
3440
+ }
3441
+ );
3442
+ var NotificationBell = React.forwardRef(
3443
+ function NotificationBell2({ icon, count, maxCount = 99, size = "md", ping, className, disabled, ...props }, ref) {
3444
+ const displayCount = count && count > maxCount ? `${maxCount}+` : count;
3445
+ const hasCount = count !== void 0 && count > 0;
3446
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3447
+ "button",
3448
+ {
3449
+ ref,
3450
+ type: "button",
3451
+ ...props,
3452
+ disabled,
3453
+ "aria-label": props["aria-label"] || `Notifications${hasCount ? ` (${count})` : ""}`,
3454
+ className: cn(
3455
+ "relative inline-flex items-center justify-center rounded-xl text-white/70 transition-colors hover:text-white hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent disabled:opacity-60 disabled:pointer-events-none",
3456
+ sizeClass6[size],
3457
+ className
3458
+ ),
3459
+ children: [
3460
+ icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: icon }) : /* @__PURE__ */ jsxRuntime.jsx(DefaultBellIcon, { className: iconSizeClass[size] }),
3461
+ hasCount && /* @__PURE__ */ jsxRuntime.jsxs(
3462
+ "span",
3463
+ {
3464
+ className: cn(
3465
+ "absolute top-0 right-0 flex items-center justify-center rounded-full bg-rose-500 text-white font-semibold leading-none",
3466
+ badgeSizeClass[size]
3467
+ ),
3468
+ children: [
3469
+ ping && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute inset-0 rounded-full bg-rose-500 animate-ping opacity-75" }),
3470
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative", children: displayCount })
3471
+ ]
3472
+ }
3473
+ )
3474
+ ]
3475
+ }
3476
+ );
3477
+ }
3478
+ );
3333
3479
 
3334
3480
  exports.ActiveFilterPills = ActiveFilterPills;
3335
3481
  exports.Alert = Alert;
@@ -3359,6 +3505,7 @@ exports.Input = Input;
3359
3505
  exports.Modal = Modal;
3360
3506
  exports.MutationOverlay = MutationOverlay;
3361
3507
  exports.Navbar = Navbar;
3508
+ exports.NotificationBell = NotificationBell;
3362
3509
  exports.PageShell = PageShell;
3363
3510
  exports.Pagination = Pagination;
3364
3511
  exports.Pill = Pill;
@@ -3398,5 +3545,8 @@ exports.getFocusableElements = getFocusableElements;
3398
3545
  exports.useClipboard = useClipboard;
3399
3546
  exports.useDebounce = useDebounce;
3400
3547
  exports.useDisclosure = useDisclosure;
3548
+ exports.useHotkeys = useHotkeys;
3549
+ exports.useIntersectionObserver = useIntersectionObserver;
3401
3550
  exports.useMediaQuery = useMediaQuery;
3551
+ exports.useSharedNow = useSharedNow;
3402
3552
  exports.useToast = useToast;
package/dist/index.d.cts CHANGED
@@ -31,6 +31,70 @@ declare function useMediaQuery(query: string): boolean;
31
31
 
32
32
  declare function useDebounce<T>(value: T, delayMs?: number): T;
33
33
 
34
+ type HotkeyModifiers = {
35
+ ctrl?: boolean;
36
+ shift?: boolean;
37
+ alt?: boolean;
38
+ meta?: boolean;
39
+ };
40
+ type HotkeyBinding = {
41
+ key: string;
42
+ modifiers?: HotkeyModifiers;
43
+ handler: (e: KeyboardEvent) => void;
44
+ };
45
+ type UseHotkeysOptions = {
46
+ enabled?: boolean;
47
+ };
48
+ /**
49
+ * Global keyboard shortcut hook.
50
+ *
51
+ * @example
52
+ * useHotkeys([
53
+ * { key: 'Escape', handler: () => close() },
54
+ * { key: 's', modifiers: { ctrl: true }, handler: (e) => { e.preventDefault(); save(); } },
55
+ * ]);
56
+ */
57
+ declare function useHotkeys(bindings: HotkeyBinding[], options?: UseHotkeysOptions): void;
58
+
59
+ type UseIntersectionObserverOptions = {
60
+ root?: Element | null;
61
+ rootMargin?: string;
62
+ threshold?: number | number[];
63
+ enabled?: boolean;
64
+ };
65
+ type UseIntersectionObserverReturn = {
66
+ ref: (node: Element | null) => void;
67
+ entry: IntersectionObserverEntry | null;
68
+ isIntersecting: boolean;
69
+ };
70
+ /**
71
+ * IntersectionObserver hook for infinite scroll, lazy loading, etc.
72
+ *
73
+ * @example
74
+ * const { ref, isIntersecting } = useIntersectionObserver({ rootMargin: '200px' });
75
+ * useEffect(() => { if (isIntersecting) loadMore(); }, [isIntersecting]);
76
+ * return <div ref={ref} />;
77
+ */
78
+ declare function useIntersectionObserver(options?: UseIntersectionObserverOptions): UseIntersectionObserverReturn;
79
+
80
+ type UseSharedNowOptions = {
81
+ /** Tick interval in ms. Default: 1000 */
82
+ interval?: number;
83
+ /** Stop ticking after this timestamp (ms). Undefined = never stop. */
84
+ untilMs?: number;
85
+ /** Enable/disable. Default: true */
86
+ enabled?: boolean;
87
+ };
88
+ /**
89
+ * Reactive current-time hook that ticks on a shared interval.
90
+ * Useful for countdowns, "X minutes ago" labels, and CooldownRing.
91
+ *
92
+ * @example
93
+ * const now = useSharedNow({ interval: 1000 });
94
+ * const remaining = Math.max(0, deadline - now);
95
+ */
96
+ declare function useSharedNow(options?: UseSharedNowOptions): number;
97
+
34
98
  type Size = 'sm' | 'md' | 'lg';
35
99
 
36
100
  type AvatarSize = 'sm' | 'md' | 'lg' | 'xl';
@@ -630,4 +694,29 @@ type MutationOverlayProps = {
630
694
  };
631
695
  declare function MutationOverlay({ status, savingText, savedText, errorText, className, }: MutationOverlayProps): react_jsx_runtime.JSX.Element | null;
632
696
 
633
- export { ActiveFilterPills, type ActiveFilterPillsProps, Alert, type AlertProps, type AlertVariant, Avatar, type AvatarProps, type AvatarSize, Badge, type BadgeProps, type BadgeSize, type BadgeVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, type CardProps, type CardVariant, Checkbox, type CheckboxProps, CollapsibleSection, type CollapsibleSectionProps, ColorInput, type ColorInputProps, ConfirmDialog, type ConfirmDialogProps, type ConfirmDialogVariant, CooldownRing, type CooldownRingProps, type CooldownRingSize, CopyField, type CopyFieldProps, DashboardLayout, type DashboardLayoutProps, Divider, type DividerProps, DotIndicator, type DotIndicatorProps, DropZone, type DropZoneProps, Dropdown, DropdownItem, type DropdownItemProps, DropdownMenu, type DropdownMenuProps, type DropdownProps, DropdownSeparator, type DropdownSeparatorProps, DropdownTrigger, type DropdownTriggerProps, EmptyState, type EmptyStateProps, type FilterPill, FormField, type FormFieldProps, IconButton, type IconButtonProps, Input, type InputProps, Modal, type ModalProps, MutationOverlay, type MutationOverlayProps, type MutationOverlayStatus, Navbar, type NavbarProps, PageShell, type PageShellProps, type PageShellVariant, Pagination, type PaginationProps, Pill, ProgressBar, type ProgressBarProps, type ProgressBarVariant, ProgressButton, type ProgressButtonProps, RadioGroup, type RadioGroupProps, RadioItem, type RadioItemProps, SearchInput, type SearchInputProps, SectionCard, type SectionCardProps, Select, type SelectProps, Sidebar, type SidebarProps, type Size, Skeleton, type SkeletonProps, Slider, type SliderProps, Spinner, type SpinnerProps, type SpinnerSize, StageProgress, type StageProgressProps, StatCard, type StatCardProps, type StatCardTrend, type Step, Stepper, type StepperProps, Tab, TabList, type TabListProps, TabPanel, type TabPanelProps, type TabProps, Table, TableBody, type TableBodyProps, TableCell, type TableCellProps, TableHead, type TableHeadProps, TableHeader, type TableHeaderProps, type TableProps, TableRow, type TableRowProps, Tabs, type TabsProps, type TabsVariant, TagInput, type TagInputProps, Textarea, type TextareaProps, type ToastData, type ToastPosition, ToastProvider, type ToastProviderProps, type ToastVariant, Toggle, type ToggleProps, type ToggleSize, Tooltip, type TooltipPlacement, type TooltipProps, type UseClipboardReturn, type UseDisclosureReturn, cn, focusSafely, getFocusableElements, useClipboard, useDebounce, useDisclosure, useMediaQuery, useToast };
697
+ type NotificationBellProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'> & {
698
+ /** Icon to display (bell SVG, etc.). If omitted, renders a default bell. */
699
+ icon?: ReactNode;
700
+ /** Unread count. 0 or undefined hides the badge. */
701
+ count?: number;
702
+ /** Max count to display before showing "N+". Default: 99 */
703
+ maxCount?: number;
704
+ /** Size variant */
705
+ size?: 'sm' | 'md' | 'lg';
706
+ /** Whether to show a ping animation on the badge */
707
+ ping?: boolean;
708
+ };
709
+ declare const NotificationBell: react.ForwardRefExoticComponent<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {
710
+ /** Icon to display (bell SVG, etc.). If omitted, renders a default bell. */
711
+ icon?: ReactNode;
712
+ /** Unread count. 0 or undefined hides the badge. */
713
+ count?: number;
714
+ /** Max count to display before showing "N+". Default: 99 */
715
+ maxCount?: number;
716
+ /** Size variant */
717
+ size?: "sm" | "md" | "lg";
718
+ /** Whether to show a ping animation on the badge */
719
+ ping?: boolean;
720
+ } & react.RefAttributes<HTMLButtonElement>>;
721
+
722
+ export { ActiveFilterPills, type ActiveFilterPillsProps, Alert, type AlertProps, type AlertVariant, Avatar, type AvatarProps, type AvatarSize, Badge, type BadgeProps, type BadgeSize, type BadgeVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, type CardProps, type CardVariant, Checkbox, type CheckboxProps, CollapsibleSection, type CollapsibleSectionProps, ColorInput, type ColorInputProps, ConfirmDialog, type ConfirmDialogProps, type ConfirmDialogVariant, CooldownRing, type CooldownRingProps, type CooldownRingSize, CopyField, type CopyFieldProps, DashboardLayout, type DashboardLayoutProps, Divider, type DividerProps, DotIndicator, type DotIndicatorProps, DropZone, type DropZoneProps, Dropdown, DropdownItem, type DropdownItemProps, DropdownMenu, type DropdownMenuProps, type DropdownProps, DropdownSeparator, type DropdownSeparatorProps, DropdownTrigger, type DropdownTriggerProps, EmptyState, type EmptyStateProps, type FilterPill, FormField, type FormFieldProps, type HotkeyBinding, type HotkeyModifiers, IconButton, type IconButtonProps, Input, type InputProps, Modal, type ModalProps, MutationOverlay, type MutationOverlayProps, type MutationOverlayStatus, Navbar, type NavbarProps, NotificationBell, type NotificationBellProps, PageShell, type PageShellProps, type PageShellVariant, Pagination, type PaginationProps, Pill, ProgressBar, type ProgressBarProps, type ProgressBarVariant, ProgressButton, type ProgressButtonProps, RadioGroup, type RadioGroupProps, RadioItem, type RadioItemProps, SearchInput, type SearchInputProps, SectionCard, type SectionCardProps, Select, type SelectProps, Sidebar, type SidebarProps, type Size, Skeleton, type SkeletonProps, Slider, type SliderProps, Spinner, type SpinnerProps, type SpinnerSize, StageProgress, type StageProgressProps, StatCard, type StatCardProps, type StatCardTrend, type Step, Stepper, type StepperProps, Tab, TabList, type TabListProps, TabPanel, type TabPanelProps, type TabProps, Table, TableBody, type TableBodyProps, TableCell, type TableCellProps, TableHead, type TableHeadProps, TableHeader, type TableHeaderProps, type TableProps, TableRow, type TableRowProps, Tabs, type TabsProps, type TabsVariant, TagInput, type TagInputProps, Textarea, type TextareaProps, type ToastData, type ToastPosition, ToastProvider, type ToastProviderProps, type ToastVariant, Toggle, type ToggleProps, type ToggleSize, Tooltip, type TooltipPlacement, type TooltipProps, type UseClipboardReturn, type UseDisclosureReturn, type UseHotkeysOptions, type UseIntersectionObserverOptions, type UseIntersectionObserverReturn, type UseSharedNowOptions, cn, focusSafely, getFocusableElements, useClipboard, useDebounce, useDisclosure, useHotkeys, useIntersectionObserver, useMediaQuery, useSharedNow, useToast };
package/dist/index.d.ts CHANGED
@@ -31,6 +31,70 @@ declare function useMediaQuery(query: string): boolean;
31
31
 
32
32
  declare function useDebounce<T>(value: T, delayMs?: number): T;
33
33
 
34
+ type HotkeyModifiers = {
35
+ ctrl?: boolean;
36
+ shift?: boolean;
37
+ alt?: boolean;
38
+ meta?: boolean;
39
+ };
40
+ type HotkeyBinding = {
41
+ key: string;
42
+ modifiers?: HotkeyModifiers;
43
+ handler: (e: KeyboardEvent) => void;
44
+ };
45
+ type UseHotkeysOptions = {
46
+ enabled?: boolean;
47
+ };
48
+ /**
49
+ * Global keyboard shortcut hook.
50
+ *
51
+ * @example
52
+ * useHotkeys([
53
+ * { key: 'Escape', handler: () => close() },
54
+ * { key: 's', modifiers: { ctrl: true }, handler: (e) => { e.preventDefault(); save(); } },
55
+ * ]);
56
+ */
57
+ declare function useHotkeys(bindings: HotkeyBinding[], options?: UseHotkeysOptions): void;
58
+
59
+ type UseIntersectionObserverOptions = {
60
+ root?: Element | null;
61
+ rootMargin?: string;
62
+ threshold?: number | number[];
63
+ enabled?: boolean;
64
+ };
65
+ type UseIntersectionObserverReturn = {
66
+ ref: (node: Element | null) => void;
67
+ entry: IntersectionObserverEntry | null;
68
+ isIntersecting: boolean;
69
+ };
70
+ /**
71
+ * IntersectionObserver hook for infinite scroll, lazy loading, etc.
72
+ *
73
+ * @example
74
+ * const { ref, isIntersecting } = useIntersectionObserver({ rootMargin: '200px' });
75
+ * useEffect(() => { if (isIntersecting) loadMore(); }, [isIntersecting]);
76
+ * return <div ref={ref} />;
77
+ */
78
+ declare function useIntersectionObserver(options?: UseIntersectionObserverOptions): UseIntersectionObserverReturn;
79
+
80
+ type UseSharedNowOptions = {
81
+ /** Tick interval in ms. Default: 1000 */
82
+ interval?: number;
83
+ /** Stop ticking after this timestamp (ms). Undefined = never stop. */
84
+ untilMs?: number;
85
+ /** Enable/disable. Default: true */
86
+ enabled?: boolean;
87
+ };
88
+ /**
89
+ * Reactive current-time hook that ticks on a shared interval.
90
+ * Useful for countdowns, "X minutes ago" labels, and CooldownRing.
91
+ *
92
+ * @example
93
+ * const now = useSharedNow({ interval: 1000 });
94
+ * const remaining = Math.max(0, deadline - now);
95
+ */
96
+ declare function useSharedNow(options?: UseSharedNowOptions): number;
97
+
34
98
  type Size = 'sm' | 'md' | 'lg';
35
99
 
36
100
  type AvatarSize = 'sm' | 'md' | 'lg' | 'xl';
@@ -630,4 +694,29 @@ type MutationOverlayProps = {
630
694
  };
631
695
  declare function MutationOverlay({ status, savingText, savedText, errorText, className, }: MutationOverlayProps): react_jsx_runtime.JSX.Element | null;
632
696
 
633
- export { ActiveFilterPills, type ActiveFilterPillsProps, Alert, type AlertProps, type AlertVariant, Avatar, type AvatarProps, type AvatarSize, Badge, type BadgeProps, type BadgeSize, type BadgeVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, type CardProps, type CardVariant, Checkbox, type CheckboxProps, CollapsibleSection, type CollapsibleSectionProps, ColorInput, type ColorInputProps, ConfirmDialog, type ConfirmDialogProps, type ConfirmDialogVariant, CooldownRing, type CooldownRingProps, type CooldownRingSize, CopyField, type CopyFieldProps, DashboardLayout, type DashboardLayoutProps, Divider, type DividerProps, DotIndicator, type DotIndicatorProps, DropZone, type DropZoneProps, Dropdown, DropdownItem, type DropdownItemProps, DropdownMenu, type DropdownMenuProps, type DropdownProps, DropdownSeparator, type DropdownSeparatorProps, DropdownTrigger, type DropdownTriggerProps, EmptyState, type EmptyStateProps, type FilterPill, FormField, type FormFieldProps, IconButton, type IconButtonProps, Input, type InputProps, Modal, type ModalProps, MutationOverlay, type MutationOverlayProps, type MutationOverlayStatus, Navbar, type NavbarProps, PageShell, type PageShellProps, type PageShellVariant, Pagination, type PaginationProps, Pill, ProgressBar, type ProgressBarProps, type ProgressBarVariant, ProgressButton, type ProgressButtonProps, RadioGroup, type RadioGroupProps, RadioItem, type RadioItemProps, SearchInput, type SearchInputProps, SectionCard, type SectionCardProps, Select, type SelectProps, Sidebar, type SidebarProps, type Size, Skeleton, type SkeletonProps, Slider, type SliderProps, Spinner, type SpinnerProps, type SpinnerSize, StageProgress, type StageProgressProps, StatCard, type StatCardProps, type StatCardTrend, type Step, Stepper, type StepperProps, Tab, TabList, type TabListProps, TabPanel, type TabPanelProps, type TabProps, Table, TableBody, type TableBodyProps, TableCell, type TableCellProps, TableHead, type TableHeadProps, TableHeader, type TableHeaderProps, type TableProps, TableRow, type TableRowProps, Tabs, type TabsProps, type TabsVariant, TagInput, type TagInputProps, Textarea, type TextareaProps, type ToastData, type ToastPosition, ToastProvider, type ToastProviderProps, type ToastVariant, Toggle, type ToggleProps, type ToggleSize, Tooltip, type TooltipPlacement, type TooltipProps, type UseClipboardReturn, type UseDisclosureReturn, cn, focusSafely, getFocusableElements, useClipboard, useDebounce, useDisclosure, useMediaQuery, useToast };
697
+ type NotificationBellProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'> & {
698
+ /** Icon to display (bell SVG, etc.). If omitted, renders a default bell. */
699
+ icon?: ReactNode;
700
+ /** Unread count. 0 or undefined hides the badge. */
701
+ count?: number;
702
+ /** Max count to display before showing "N+". Default: 99 */
703
+ maxCount?: number;
704
+ /** Size variant */
705
+ size?: 'sm' | 'md' | 'lg';
706
+ /** Whether to show a ping animation on the badge */
707
+ ping?: boolean;
708
+ };
709
+ declare const NotificationBell: react.ForwardRefExoticComponent<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {
710
+ /** Icon to display (bell SVG, etc.). If omitted, renders a default bell. */
711
+ icon?: ReactNode;
712
+ /** Unread count. 0 or undefined hides the badge. */
713
+ count?: number;
714
+ /** Max count to display before showing "N+". Default: 99 */
715
+ maxCount?: number;
716
+ /** Size variant */
717
+ size?: "sm" | "md" | "lg";
718
+ /** Whether to show a ping animation on the badge */
719
+ ping?: boolean;
720
+ } & react.RefAttributes<HTMLButtonElement>>;
721
+
722
+ export { ActiveFilterPills, type ActiveFilterPillsProps, Alert, type AlertProps, type AlertVariant, Avatar, type AvatarProps, type AvatarSize, Badge, type BadgeProps, type BadgeSize, type BadgeVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, type CardProps, type CardVariant, Checkbox, type CheckboxProps, CollapsibleSection, type CollapsibleSectionProps, ColorInput, type ColorInputProps, ConfirmDialog, type ConfirmDialogProps, type ConfirmDialogVariant, CooldownRing, type CooldownRingProps, type CooldownRingSize, CopyField, type CopyFieldProps, DashboardLayout, type DashboardLayoutProps, Divider, type DividerProps, DotIndicator, type DotIndicatorProps, DropZone, type DropZoneProps, Dropdown, DropdownItem, type DropdownItemProps, DropdownMenu, type DropdownMenuProps, type DropdownProps, DropdownSeparator, type DropdownSeparatorProps, DropdownTrigger, type DropdownTriggerProps, EmptyState, type EmptyStateProps, type FilterPill, FormField, type FormFieldProps, type HotkeyBinding, type HotkeyModifiers, IconButton, type IconButtonProps, Input, type InputProps, Modal, type ModalProps, MutationOverlay, type MutationOverlayProps, type MutationOverlayStatus, Navbar, type NavbarProps, NotificationBell, type NotificationBellProps, PageShell, type PageShellProps, type PageShellVariant, Pagination, type PaginationProps, Pill, ProgressBar, type ProgressBarProps, type ProgressBarVariant, ProgressButton, type ProgressButtonProps, RadioGroup, type RadioGroupProps, RadioItem, type RadioItemProps, SearchInput, type SearchInputProps, SectionCard, type SectionCardProps, Select, type SelectProps, Sidebar, type SidebarProps, type Size, Skeleton, type SkeletonProps, Slider, type SliderProps, Spinner, type SpinnerProps, type SpinnerSize, StageProgress, type StageProgressProps, StatCard, type StatCardProps, type StatCardTrend, type Step, Stepper, type StepperProps, Tab, TabList, type TabListProps, TabPanel, type TabPanelProps, type TabProps, Table, TableBody, type TableBodyProps, TableCell, type TableCellProps, TableHead, type TableHeadProps, TableHeader, type TableHeaderProps, type TableProps, TableRow, type TableRowProps, Tabs, type TabsProps, type TabsVariant, TagInput, type TagInputProps, Textarea, type TextareaProps, type ToastData, type ToastPosition, ToastProvider, type ToastProviderProps, type ToastVariant, Toggle, type ToggleProps, type ToggleSize, Tooltip, type TooltipPlacement, type TooltipProps, type UseClipboardReturn, type UseDisclosureReturn, type UseHotkeysOptions, type UseIntersectionObserverOptions, type UseIntersectionObserverReturn, type UseSharedNowOptions, cn, focusSafely, getFocusableElements, useClipboard, useDebounce, useDisclosure, useHotkeys, useIntersectionObserver, useMediaQuery, useSharedNow, useToast };
package/dist/index.js CHANGED
@@ -124,6 +124,82 @@ function useDebounce(value, delayMs = 300) {
124
124
  }, [value, delayMs]);
125
125
  return debouncedValue;
126
126
  }
127
+ function matchModifiers(e, mods) {
128
+ const ctrl = mods?.ctrl ?? false;
129
+ const shift = mods?.shift ?? false;
130
+ const alt = mods?.alt ?? false;
131
+ const meta = mods?.meta ?? false;
132
+ return e.ctrlKey === ctrl && e.shiftKey === shift && e.altKey === alt && e.metaKey === meta;
133
+ }
134
+ function useHotkeys(bindings, options = {}) {
135
+ const { enabled = true } = options;
136
+ useEffect(() => {
137
+ if (!enabled) return;
138
+ const onKeyDown = (e) => {
139
+ for (const binding of bindings) {
140
+ if (e.key === binding.key && matchModifiers(e, binding.modifiers)) {
141
+ binding.handler(e);
142
+ }
143
+ }
144
+ };
145
+ document.addEventListener("keydown", onKeyDown);
146
+ return () => document.removeEventListener("keydown", onKeyDown);
147
+ }, [enabled, ...bindings]);
148
+ }
149
+ function useIntersectionObserver(options = {}) {
150
+ const { root = null, rootMargin = "0px", threshold = 0, enabled = true } = options;
151
+ const [entry, setEntry] = useState(null);
152
+ const nodeRef = useRef(null);
153
+ const observerRef = useRef(null);
154
+ useEffect(() => {
155
+ if (!enabled) {
156
+ setEntry(null);
157
+ return;
158
+ }
159
+ observerRef.current = new IntersectionObserver(
160
+ ([e]) => setEntry(e),
161
+ { root, rootMargin, threshold }
162
+ );
163
+ if (nodeRef.current) {
164
+ observerRef.current.observe(nodeRef.current);
165
+ }
166
+ return () => {
167
+ observerRef.current?.disconnect();
168
+ observerRef.current = null;
169
+ };
170
+ }, [enabled, root, rootMargin, JSON.stringify(threshold)]);
171
+ const ref = (node) => {
172
+ if (nodeRef.current) {
173
+ observerRef.current?.unobserve(nodeRef.current);
174
+ }
175
+ nodeRef.current = node;
176
+ if (node) {
177
+ observerRef.current?.observe(node);
178
+ }
179
+ };
180
+ return {
181
+ ref,
182
+ entry,
183
+ isIntersecting: entry?.isIntersecting ?? false
184
+ };
185
+ }
186
+ function useSharedNow(options = {}) {
187
+ const { interval = 1e3, untilMs, enabled = true } = options;
188
+ const [now, setNow] = useState(Date.now);
189
+ useEffect(() => {
190
+ if (!enabled) return;
191
+ if (untilMs !== void 0 && Date.now() >= untilMs) return;
192
+ const id = setInterval(() => {
193
+ const current = Date.now();
194
+ setNow(current);
195
+ if (untilMs !== void 0 && current >= untilMs) {
196
+ clearInterval(id);
197
+ }
198
+ }, interval);
199
+ return () => clearInterval(id);
200
+ }, [interval, untilMs, enabled]);
201
+ return now;
202
+ }
127
203
 
128
204
  // src/tokens/colors.ts
129
205
  var colors = {
@@ -3324,5 +3400,75 @@ function MutationOverlay({
3324
3400
  }
3325
3401
  );
3326
3402
  }
3403
+ var sizeClass6 = {
3404
+ sm: "w-8 h-8",
3405
+ md: "w-10 h-10",
3406
+ lg: "w-12 h-12"
3407
+ };
3408
+ var iconSizeClass = {
3409
+ sm: "w-4 h-4",
3410
+ md: "w-5 h-5",
3411
+ lg: "w-6 h-6"
3412
+ };
3413
+ var badgeSizeClass = {
3414
+ sm: "min-w-[16px] h-4 text-[10px] px-1",
3415
+ md: "min-w-[18px] h-[18px] text-[11px] px-1",
3416
+ lg: "min-w-[20px] h-5 text-xs px-1.5"
3417
+ };
3418
+ var DefaultBellIcon = ({ className }) => /* @__PURE__ */ jsxs(
3419
+ "svg",
3420
+ {
3421
+ xmlns: "http://www.w3.org/2000/svg",
3422
+ viewBox: "0 0 24 24",
3423
+ fill: "none",
3424
+ stroke: "currentColor",
3425
+ strokeWidth: 2,
3426
+ strokeLinecap: "round",
3427
+ strokeLinejoin: "round",
3428
+ className,
3429
+ "aria-hidden": "true",
3430
+ children: [
3431
+ /* @__PURE__ */ jsx("path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }),
3432
+ /* @__PURE__ */ jsx("path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" })
3433
+ ]
3434
+ }
3435
+ );
3436
+ var NotificationBell = forwardRef(
3437
+ function NotificationBell2({ icon, count, maxCount = 99, size = "md", ping, className, disabled, ...props }, ref) {
3438
+ const displayCount = count && count > maxCount ? `${maxCount}+` : count;
3439
+ const hasCount = count !== void 0 && count > 0;
3440
+ return /* @__PURE__ */ jsxs(
3441
+ "button",
3442
+ {
3443
+ ref,
3444
+ type: "button",
3445
+ ...props,
3446
+ disabled,
3447
+ "aria-label": props["aria-label"] || `Notifications${hasCount ? ` (${count})` : ""}`,
3448
+ className: cn(
3449
+ "relative inline-flex items-center justify-center rounded-xl text-white/70 transition-colors hover:text-white hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent disabled:opacity-60 disabled:pointer-events-none",
3450
+ sizeClass6[size],
3451
+ className
3452
+ ),
3453
+ children: [
3454
+ icon ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: icon }) : /* @__PURE__ */ jsx(DefaultBellIcon, { className: iconSizeClass[size] }),
3455
+ hasCount && /* @__PURE__ */ jsxs(
3456
+ "span",
3457
+ {
3458
+ className: cn(
3459
+ "absolute top-0 right-0 flex items-center justify-center rounded-full bg-rose-500 text-white font-semibold leading-none",
3460
+ badgeSizeClass[size]
3461
+ ),
3462
+ children: [
3463
+ ping && /* @__PURE__ */ jsx("span", { className: "absolute inset-0 rounded-full bg-rose-500 animate-ping opacity-75" }),
3464
+ /* @__PURE__ */ jsx("span", { className: "relative", children: displayCount })
3465
+ ]
3466
+ }
3467
+ )
3468
+ ]
3469
+ }
3470
+ );
3471
+ }
3472
+ );
3327
3473
 
3328
- export { ActiveFilterPills, Alert, Avatar, Badge, Button, Card, Checkbox, CollapsibleSection, ColorInput, ConfirmDialog, CooldownRing, CopyField, DashboardLayout, Divider, DotIndicator, DropZone, Dropdown, DropdownItem, DropdownMenu, DropdownSeparator, DropdownTrigger, EmptyState, FormField, IconButton, Input, Modal, MutationOverlay, Navbar, PageShell, Pagination, Pill, ProgressBar, ProgressButton, RadioGroup, RadioItem, SearchInput, SectionCard, Select, Sidebar, Skeleton, Slider, Spinner, StageProgress, StatCard, Stepper, Tab, TabList, TabPanel, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TagInput, Textarea, ToastProvider, Toggle, Tooltip, cn, colors, focusSafely, getFocusableElements, useClipboard, useDebounce, useDisclosure, useMediaQuery, useToast };
3474
+ export { ActiveFilterPills, Alert, Avatar, Badge, Button, Card, Checkbox, CollapsibleSection, ColorInput, ConfirmDialog, CooldownRing, CopyField, DashboardLayout, Divider, DotIndicator, DropZone, Dropdown, DropdownItem, DropdownMenu, DropdownSeparator, DropdownTrigger, EmptyState, FormField, IconButton, Input, Modal, MutationOverlay, Navbar, NotificationBell, PageShell, Pagination, Pill, ProgressBar, ProgressButton, RadioGroup, RadioItem, SearchInput, SectionCard, Select, Sidebar, Skeleton, Slider, Spinner, StageProgress, StatCard, Stepper, Tab, TabList, TabPanel, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TagInput, Textarea, ToastProvider, Toggle, Tooltip, cn, colors, focusSafely, getFocusableElements, useClipboard, useDebounce, useDisclosure, useHotkeys, useIntersectionObserver, useMediaQuery, useSharedNow, useToast };
@@ -793,6 +793,9 @@ a {
793
793
  .left-3 {
794
794
  left: 0.75rem;
795
795
  }
796
+ .right-0 {
797
+ right: 0px;
798
+ }
796
799
  .right-2 {
797
800
  right: 0.5rem;
798
801
  }
@@ -993,6 +996,9 @@ a {
993
996
  .h-9 {
994
997
  height: 2.25rem;
995
998
  }
999
+ .h-\[18px\] {
1000
+ height: 18px;
1001
+ }
996
1002
  .h-\[2px\] {
997
1003
  height: 2px;
998
1004
  }
@@ -1123,6 +1129,15 @@ a {
1123
1129
  .min-w-\[120px\] {
1124
1130
  min-width: 120px;
1125
1131
  }
1132
+ .min-w-\[16px\] {
1133
+ min-width: 16px;
1134
+ }
1135
+ .min-w-\[18px\] {
1136
+ min-width: 18px;
1137
+ }
1138
+ .min-w-\[20px\] {
1139
+ min-width: 20px;
1140
+ }
1126
1141
  .max-w-2xl {
1127
1142
  max-width: 42rem;
1128
1143
  }
@@ -1257,6 +1272,15 @@ a {
1257
1272
  .animate-modal-pop {
1258
1273
  animation: ml-modal-pop 160ms cubic-bezier(0.22,1,0.36,1) both;
1259
1274
  }
1275
+ @keyframes ping {
1276
+ 75%, 100% {
1277
+ transform: scale(2);
1278
+ opacity: 0;
1279
+ }
1280
+ }
1281
+ .animate-ping {
1282
+ animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
1283
+ }
1260
1284
  @keyframes pulse {
1261
1285
  50% {
1262
1286
  opacity: .5;
@@ -1676,6 +1700,14 @@ a {
1676
1700
  .p-6 {
1677
1701
  padding: 1.5rem;
1678
1702
  }
1703
+ .px-1 {
1704
+ padding-left: 0.25rem;
1705
+ padding-right: 0.25rem;
1706
+ }
1707
+ .px-1\.5 {
1708
+ padding-left: 0.375rem;
1709
+ padding-right: 0.375rem;
1710
+ }
1679
1711
  .px-2 {
1680
1712
  padding-left: 0.5rem;
1681
1713
  padding-right: 0.5rem;
@@ -1782,6 +1814,9 @@ a {
1782
1814
  font-size: 1.875rem;
1783
1815
  line-height: 2.25rem;
1784
1816
  }
1817
+ .text-\[10px\] {
1818
+ font-size: 10px;
1819
+ }
1785
1820
  .text-\[11px\] {
1786
1821
  font-size: 11px;
1787
1822
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memelabui/ui",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "MemeLab shared UI component library — React + Tailwind + Glassmorphism",
5
5
  "type": "module",
6
6
  "sideEffects": [