@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 +4 -0
- package/dist/index.cjs +150 -0
- package/dist/index.d.cts +90 -1
- package/dist/index.d.ts +90 -1
- package/dist/index.js +147 -1
- package/dist/styles/index.css +35 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/styles/index.css
CHANGED
|
@@ -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
|
}
|