@connectycube/react-ui-kit 0.0.11 → 0.0.13

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.
@@ -10,6 +10,9 @@
10
10
  "presence": {
11
11
  "lucide-react": "latest"
12
12
  },
13
+ "status-indicator": {
14
+ "@radix-ui/react-tooltip": "latest"
15
+ },
13
16
  "stream-view": {
14
17
  "lucide-react": "latest"
15
18
  },
@@ -1,8 +1,10 @@
1
1
  import { LoaderCircle } from 'lucide-react';
2
2
  import { cn } from './utils';
3
3
 
4
- function AnimatedLoader({ loading = true, className }) {
5
- return loading ? <LoaderCircle className={cn('animate-spin mx-auto', className)} /> : null;
4
+ function AnimatedLoader({ loading = true, ...props }) {
5
+ return loading ? (
6
+ <LoaderCircle strokeWidth={3} {...props} className={cn('animate-spin mx-auto text-gray-600', props?.className)} />
7
+ ) : null;
6
8
  }
7
9
 
8
10
  AnimatedLoader.displayName = 'AnimatedLoader';
@@ -1,29 +1,36 @@
1
1
  import { forwardRef, memo } from 'react';
2
- import * as AvatarPrimitive from '@radix-ui/react-avatar';
2
+ import { Avatar as AvatarRoot, Fallback as AvatarFallback, Image as AvatarImage } from '@radix-ui/react-avatar';
3
3
  import { PresenceBadge } from './presence';
4
4
  import { cn, getInitialsFromName } from './utils';
5
5
 
6
6
  function AvatarBase(
7
- { src, name = 'NA', online, presence, className, onlineClassName, presenceClassName, ...props },
7
+ { src, name = 'NA', online, presence, className, onlineProps, presenceProps, imageProps, fallbackProps, ...props },
8
8
  ref
9
9
  ) {
10
10
  const initials = getInitialsFromName(name);
11
11
 
12
12
  return (
13
- <AvatarPrimitive.Root
13
+ <AvatarRoot
14
14
  ref={ref}
15
- className={cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', className)}
16
15
  {...props}
16
+ className={cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', className)}
17
17
  >
18
- <AvatarPrimitive.Image src={src} className={cn('aspect-square size-full object-cover')} />
19
- <AvatarPrimitive.Fallback className={cn('bg-muted flex size-full items-center justify-center')}>
18
+ <AvatarImage {...imageProps} src={src} className={cn('aspect-square size-full object-cover')} />
19
+ <AvatarFallback {...fallbackProps} className={cn('bg-muted flex size-full items-center justify-center')}>
20
20
  {initials}
21
- </AvatarPrimitive.Fallback>
21
+ </AvatarFallback>
22
22
  {online && (
23
- <div className={cn('rounded-full border-2 bg-green-600 border-green-200 size-3.5', onlineClassName)} />
23
+ <div
24
+ {...onlineProps}
25
+ className={cn('rounded-full border-2 bg-green-600 border-green-200 size-3.5', onlineProps?.className)}
26
+ />
24
27
  )}
25
- <PresenceBadge status={presence} className={cn('absolute -bottom-0.5 -right-1', presenceClassName)} />
26
- </AvatarPrimitive.Root>
28
+ <PresenceBadge
29
+ status={presence}
30
+ {...presenceProps}
31
+ className={cn('absolute -bottom-0.5 -right-1', presenceProps?.className)}
32
+ />
33
+ </AvatarRoot>
27
34
  );
28
35
  }
29
36
 
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useImperativeHandle, memo, forwardRef }
2
2
  import { cn } from './utils';
3
3
 
4
4
  function DismissLayerBase(
5
- { active, onDismiss, disableClickOutside = false, disableEscKeyPress = false, disabled, className, ...props },
5
+ { active, onDismiss, disableClickOutside = false, disableEscKeyPress = false, disabled, ...props },
6
6
  ref
7
7
  ) {
8
8
  const innerRef = useRef(null);
@@ -41,11 +41,11 @@ function DismissLayerBase(
41
41
  return (
42
42
  <div
43
43
  ref={innerRef}
44
- onClick={handleClickOrTouch}
45
- onTouchStart={handleClickOrTouch}
46
- className={cn('fixed top-0 left-0 z-40 size-full bg-black/20', className)}
47
44
  aria-hidden
48
45
  {...props}
46
+ onClick={handleClickOrTouch}
47
+ onTouchStart={handleClickOrTouch}
48
+ className={cn('fixed top-0 left-0 z-40 size-full bg-black/20', props?.className)}
49
49
  />
50
50
  );
51
51
  }
@@ -1,13 +1,17 @@
1
1
  import { forwardRef, memo } from 'react';
2
2
  import { cn } from './utils';
3
3
 
4
- function PlaceholderTextBase({ title, titles = [], className }, ref) {
4
+ function PlaceholderTextBase({ title, titles = [], rowProps, ...props }, ref) {
5
5
  const rows = typeof title === 'string' ? [title, ...titles] : titles;
6
6
 
7
7
  return (
8
- <div ref={ref} className={cn('absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2', className)}>
8
+ <div
9
+ ref={ref}
10
+ {...props}
11
+ className={cn('absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2', props?.className)}
12
+ >
9
13
  {rows.map((row, index) => (
10
- <div key={`placeholder-text-${index}`} className="text-center">
14
+ <div key={`placeholder-text-${index}`} {...rowProps} className={cn('text-center', rowProps?.className)}>
11
15
  {row}
12
16
  </div>
13
17
  ))}
@@ -1,18 +1,23 @@
1
1
  import { memo } from 'react';
2
2
  import { CircleCheck, CircleMinus, CircleQuestionMark, Clock } from 'lucide-react';
3
- import { capitalize, cn, UserPresence } from './utils';
3
+ import { capitalize, cn } from './utils';
4
4
 
5
- function PresenceBadgeBase({ status, className, ...props }) {
5
+ function PresenceBadgeBase({ status, ...props }) {
6
6
  switch (status) {
7
- case UserPresence.AVAILABLE || 'available':
8
- return <CircleCheck className={cn('rounded-full text-white bg-green-600 size-4.5', className)} {...props} />;
9
- case UserPresence.BUSY || 'busy':
10
- return <CircleMinus className={cn('rounded-full text-white bg-red-600 size-4.5', className)} {...props} />;
11
- case UserPresence.AWAY || 'away':
12
- return <Clock className={cn('rounded-full text-white bg-yellow-500 size-4.5', className)} {...props} />;
13
- case UserPresence.UNKNOWN || 'unknown':
7
+ case 'available':
14
8
  return (
15
- <CircleQuestionMark className={cn('rounded-full text-white bg-gray-500 size-4.5', className)} {...props} />
9
+ <CircleCheck {...props} className={cn('rounded-full text-white bg-green-600 size-4.5', props?.className)} />
10
+ );
11
+ case 'busy':
12
+ return <CircleMinus {...props} className={cn('rounded-full text-white bg-red-600 size-4.5', props?.className)} />;
13
+ case 'away':
14
+ return <Clock {...props} className={cn('rounded-full text-white bg-yellow-500 size-4.5', props?.className)} />;
15
+ case 'unknown':
16
+ return (
17
+ <CircleQuestionMark
18
+ {...props}
19
+ className={cn('rounded-full text-white bg-gray-500 size-4.5', props?.className)}
20
+ />
16
21
  );
17
22
  default:
18
23
  return null;
@@ -23,13 +28,13 @@ const PresenceBadge = memo(PresenceBadgeBase);
23
28
 
24
29
  PresenceBadge.displayName = 'PresenceBadge';
25
30
 
26
- function PresenceBase({ badge = true, status, label, className, ...props }) {
31
+ function PresenceBase({ badge = true, status, label, badgeProps, labelProps, ...props }) {
27
32
  const presence = capitalize(label || status);
28
33
 
29
34
  return (
30
- <div className={cn('flex items-center gap-2', className)} {...props}>
31
- {badge && <PresenceBadge status={status} />}
32
- <span>{presence}</span>
35
+ <div {...props} className={cn('flex items-center gap-2', props?.className)}>
36
+ {badge && <PresenceBadge status={status} {...badgeProps} />}
37
+ <span {...labelProps}>{presence}</span>
33
38
  </div>
34
39
  );
35
40
  }
@@ -0,0 +1,76 @@
1
+ import { forwardRef, memo } from 'react';
2
+ import {
3
+ Provider as TooltipProvider,
4
+ Root as TooltipRoot,
5
+ Trigger as TooltipTrigger,
6
+ Portal as TooltipPortal,
7
+ Content as TooltipContent,
8
+ Arrow as TooltipArrow,
9
+ } from '@radix-ui/react-tooltip';
10
+ import { cn } from './utils';
11
+
12
+ function StatusIndicatorBase(
13
+ {
14
+ status = 'unknown',
15
+ statusColorConfig = {
16
+ unknown: 'bg-gray-500 border-gray-600',
17
+ },
18
+ statusProps,
19
+ tooltipProviderProps,
20
+ tooltipProps,
21
+ tooltipTriggerProps,
22
+ tooltipPortalProps,
23
+ tooltipContentProps,
24
+ tooltipArrowProps,
25
+ tooltip,
26
+ disabled,
27
+ className,
28
+ ...props
29
+ },
30
+ ref
31
+ ) {
32
+ if (disabled) return null;
33
+
34
+ return (
35
+ <div ref={ref} {...props} className={cn('absolute top-0 left-0', className)}>
36
+ <TooltipProvider {...tooltipProviderProps}>
37
+ <TooltipRoot {...tooltipProps}>
38
+ <TooltipTrigger asChild {...tooltipTriggerProps}>
39
+ <div
40
+ {...statusProps}
41
+ className={cn(
42
+ 'rounded-full size-4 border border-primary-foreground',
43
+ statusColorConfig[status],
44
+ statusProps?.className
45
+ )}
46
+ />
47
+ </TooltipTrigger>
48
+ <TooltipPortal {...tooltipPortalProps}>
49
+ <TooltipContent
50
+ {...tooltipContentProps}
51
+ className={cn(
52
+ 'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
53
+ tooltipContentProps?.className
54
+ )}
55
+ >
56
+ <span>{tooltip || status || 'unknown'}</span>
57
+ <TooltipArrow
58
+ {...tooltipArrowProps}
59
+ className={cn(
60
+ 'bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]',
61
+ tooltipArrowProps?.className
62
+ )}
63
+ />
64
+ </TooltipContent>
65
+ </TooltipPortal>
66
+ </TooltipRoot>
67
+ </TooltipProvider>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ const StatusIndicator = memo(forwardRef(StatusIndicatorBase));
73
+
74
+ StatusIndicator.displayName = 'StatusIndicator';
75
+
76
+ export { StatusIndicator };
@@ -38,8 +38,8 @@ function StreamViewBase({ id, stream, mirror, className, muted, ...props }, ref)
38
38
  autoPlay
39
39
  playsInline
40
40
  muted={isMuted}
41
- className={cn(defaultClassName, mirrorClassName, className)}
42
41
  {...props}
42
+ className={cn(defaultClassName, mirrorClassName, className)}
43
43
  />
44
44
  );
45
45
  }
@@ -77,18 +77,12 @@ function FullscreenStreamViewBase(
77
77
  navElement,
78
78
  hideIconElement,
79
79
  showIconElement,
80
- containerClassName,
81
- fullscreenButtonClassName,
82
- fullscreenButtonIconClassName,
83
- pipContainerClassName,
84
- pipButtonClassName,
85
- pipButtonIconClassName,
86
- containerProps,
87
80
  fullscreenButtonProps,
88
81
  fullscreenButtonIconProps,
89
- pipContainerProps,
82
+ pipProps,
90
83
  pipButtonProps,
91
84
  pipButtonIconProps,
85
+ ...props
92
86
  },
93
87
  ref
94
88
  ) {
@@ -146,45 +140,45 @@ function FullscreenStreamViewBase(
146
140
  return (
147
141
  <div
148
142
  ref={innerRef}
149
- className={cn('relative flex items-center justify-center size-full', containerClassName)}
150
- {...containerProps}
143
+ {...props}
144
+ className={cn('relative flex items-center justify-center size-full', props?.className)}
151
145
  >
152
146
  {element}
153
147
  <button
154
148
  onClick={toggleFullscreen}
149
+ {...fullscreenButtonProps}
155
150
  className={cn(
156
151
  'absolute top-2 right-2 p-1 rounded-md bg-black/50 text-white hover:bg-black/80 z-10 shadow-xs shadow-white/25',
157
- fullscreenButtonClassName
152
+ fullscreenButtonProps?.className
158
153
  )}
159
- {...fullscreenButtonProps}
160
154
  >
161
155
  {isFullscreen
162
- ? hideIconElement || <Minimize className={fullscreenButtonIconClassName} {...fullscreenButtonIconProps} />
163
- : showIconElement || <Maximize className={fullscreenButtonIconClassName} {...fullscreenButtonIconProps} />}
156
+ ? hideIconElement || <Minimize {...fullscreenButtonIconProps} />
157
+ : showIconElement || <Maximize {...fullscreenButtonIconProps} />}
164
158
  </button>
165
159
  <div className="absolute size-full p-2 flex flex-col justify-end items-center">
166
160
  {isFullscreen && pipElement && (
167
161
  <div className="relative size-full flex items-end justify-end">
168
162
  {isPictureInPicture && (
169
163
  <div
164
+ {...pipProps}
170
165
  className={cn(
171
166
  'max-w-1/4 max-h-1/4 aspect-4/3 overflow-hidden rounded-md shadow-md shadow-white/25',
172
- pipContainerClassName
167
+ pipProps?.className
173
168
  )}
174
- {...pipContainerProps}
175
169
  >
176
170
  {pipElement}
177
171
  </div>
178
172
  )}
179
173
  <button
180
174
  onClick={togglePictureInPicture}
175
+ {...pipButtonProps}
181
176
  className={cn(
182
177
  'absolute bottom-2 right-2 p-1 rounded-md bg-black/50 text-white hover:bg-black/80 z-10 shadow-xs shadow-white/25',
183
- pipButtonClassName
178
+ pipButtonProps?.className
184
179
  )}
185
- {...pipButtonProps}
186
180
  >
187
- <PictureInPicture2 className={pipButtonIconClassName} {...pipButtonIconProps} />
181
+ <PictureInPicture2 {...pipButtonIconProps} />
188
182
  </button>
189
183
  </div>
190
184
  )}
@@ -19,12 +19,3 @@ export function getInitialsFromName(name) {
19
19
  export function capitalize(str) {
20
20
  return typeof str === 'string' && str.length > 0 ? `${str[0]?.toUpperCase()}${str.slice(1)}` : '';
21
21
  }
22
-
23
- export let UserPresence = /*#__PURE__*/ (function (UserPresence) {
24
- UserPresence['AVAILABLE'] = 'available';
25
- UserPresence['BUSY'] = 'busy';
26
- UserPresence['AWAY'] = 'away';
27
- UserPresence['UNKNOWN'] = 'unknown';
28
-
29
- return UserPresence;
30
- })({});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@connectycube/react-ui-kit",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "Simple React UI Kit generator with TSX/JSX",
5
5
  "homepage": "https://github.com/ConnectyCube/react-ui-kit#readme",
6
6
  "bugs": {
@@ -61,6 +61,7 @@
61
61
  ],
62
62
  "dependencies": {
63
63
  "@radix-ui/react-avatar": "^1.1.11",
64
+ "@radix-ui/react-tooltip": "^1.2.8",
64
65
  "clsx": "^2.1.1",
65
66
  "execa": "^9.6.0",
66
67
  "fs-extra": "^11.3.2",
@@ -6,8 +6,10 @@ interface AnimatedLoaderProps extends LucideProps {
6
6
  loading?: boolean;
7
7
  }
8
8
 
9
- function AnimatedLoader({ loading = true, className }: AnimatedLoaderProps) {
10
- return loading ? <LoaderCircle className={cn('animate-spin mx-auto', className)} /> : null;
9
+ function AnimatedLoader({ loading = true, ...props }: AnimatedLoaderProps) {
10
+ return loading ? (
11
+ <LoaderCircle strokeWidth={3} {...props} className={cn('animate-spin mx-auto text-gray-600', props?.className)} />
12
+ ) : null;
11
13
  }
12
14
 
13
15
  AnimatedLoader.displayName = 'AnimatedLoader';
@@ -1,40 +1,62 @@
1
1
  import type React from 'react';
2
+ import type { AvatarProps as AvatarRootProps, AvatarFallbackProps, AvatarImageProps } from '@radix-ui/react-avatar';
2
3
  import type { PresenceStatus } from './presence';
4
+ import type { PresenceBadgeProps } from './presence';
3
5
  import { forwardRef, memo } from 'react';
4
- import * as AvatarPrimitive from '@radix-ui/react-avatar';
6
+ import { Avatar as AvatarRoot, Fallback as AvatarFallback, Image as AvatarImage } from '@radix-ui/react-avatar';
5
7
  import { PresenceBadge } from './presence';
6
8
  import { cn, getInitialsFromName } from './utils';
7
9
 
8
- interface AvatarProps extends React.ComponentProps<typeof AvatarPrimitive.Root> {
10
+ interface AvatarProps extends AvatarRootProps {
9
11
  src?: string;
10
12
  name?: string;
11
13
  online?: boolean;
12
14
  presence?: PresenceStatus;
13
- onlineClassName?: string;
14
- presenceClassName?: string;
15
+ onlineProps?: React.ComponentProps<'div'>;
16
+ presenceProps?: PresenceBadgeProps;
17
+ imageProps?: AvatarImageProps;
18
+ fallbackProps?: AvatarFallbackProps;
15
19
  }
16
20
 
17
21
  function AvatarBase(
18
- { src, name = 'NA', online, presence, className, onlineClassName, presenceClassName, ...props }: AvatarProps,
22
+ {
23
+ src,
24
+ name = 'NA',
25
+ online,
26
+ presence,
27
+ className,
28
+ onlineProps,
29
+ presenceProps,
30
+ imageProps,
31
+ fallbackProps,
32
+ ...props
33
+ }: AvatarProps,
19
34
  ref: React.Ref<HTMLDivElement>
20
35
  ) {
21
36
  const initials = getInitialsFromName(name);
22
37
 
23
38
  return (
24
- <AvatarPrimitive.Root
39
+ <AvatarRoot
25
40
  ref={ref}
26
- className={cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', className)}
27
41
  {...props}
42
+ className={cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', className)}
28
43
  >
29
- <AvatarPrimitive.Image src={src} className={cn('aspect-square size-full object-cover')} />
30
- <AvatarPrimitive.Fallback className={cn('bg-muted flex size-full items-center justify-center')}>
44
+ <AvatarImage {...imageProps} src={src} className={cn('aspect-square size-full object-cover')} />
45
+ <AvatarFallback {...fallbackProps} className={cn('bg-muted flex size-full items-center justify-center')}>
31
46
  {initials}
32
- </AvatarPrimitive.Fallback>
47
+ </AvatarFallback>
33
48
  {online && (
34
- <div className={cn('rounded-full border-2 bg-green-600 border-green-200 size-3.5', onlineClassName)} />
49
+ <div
50
+ {...onlineProps}
51
+ className={cn('rounded-full border-2 bg-green-600 border-green-200 size-3.5', onlineProps?.className)}
52
+ />
35
53
  )}
36
- <PresenceBadge status={presence} className={cn('absolute -bottom-0.5 -right-1', presenceClassName)} />
37
- </AvatarPrimitive.Root>
54
+ <PresenceBadge
55
+ status={presence}
56
+ {...presenceProps}
57
+ className={cn('absolute -bottom-0.5 -right-1', presenceProps?.className)}
58
+ />
59
+ </AvatarRoot>
38
60
  );
39
61
  }
40
62
 
@@ -0,0 +1,100 @@
1
+ import type React from 'react';
2
+ import type {
3
+ TooltipProviderProps,
4
+ TooltipProps,
5
+ TooltipTriggerProps,
6
+ TooltipPortalProps,
7
+ TooltipContentProps,
8
+ TooltipArrowProps,
9
+ } from '@radix-ui/react-tooltip';
10
+ import { forwardRef, memo } from 'react';
11
+ import {
12
+ Provider as TooltipProvider,
13
+ Root as TooltipRoot,
14
+ Trigger as TooltipTrigger,
15
+ Portal as TooltipPortal,
16
+ Content as TooltipContent,
17
+ Arrow as TooltipArrow,
18
+ } from '@radix-ui/react-tooltip';
19
+ import { cn } from './utils';
20
+
21
+ type StatusName = string | 'unknown';
22
+
23
+ interface StatusIndicatorProps extends React.ComponentProps<'div'> {
24
+ status?: StatusName;
25
+ statusColorConfig?: Record<StatusName, string>;
26
+ statusProps?: React.ComponentProps<'div'>;
27
+ tooltipProviderProps?: TooltipProviderProps;
28
+ tooltipProps?: TooltipProps;
29
+ tooltipTriggerProps?: TooltipTriggerProps;
30
+ tooltipPortalProps?: TooltipPortalProps;
31
+ tooltipContentProps?: TooltipContentProps;
32
+ tooltipArrowProps?: TooltipArrowProps;
33
+ tooltip?: string;
34
+ disabled?: boolean;
35
+ className?: string;
36
+ }
37
+
38
+ function StatusIndicatorBase(
39
+ {
40
+ status = 'unknown',
41
+ statusColorConfig = { unknown: 'bg-gray-500 border-gray-600' },
42
+ statusProps,
43
+ tooltipProviderProps,
44
+ tooltipProps,
45
+ tooltipTriggerProps,
46
+ tooltipPortalProps,
47
+ tooltipContentProps,
48
+ tooltipArrowProps,
49
+ tooltip,
50
+ disabled,
51
+ className,
52
+ ...props
53
+ }: StatusIndicatorProps,
54
+ ref: React.Ref<HTMLDivElement>
55
+ ) {
56
+ if (disabled) return null;
57
+
58
+ return (
59
+ <div ref={ref} {...props} className={cn('absolute top-0 left-0', className)}>
60
+ <TooltipProvider {...tooltipProviderProps}>
61
+ <TooltipRoot {...tooltipProps}>
62
+ <TooltipTrigger asChild {...tooltipTriggerProps}>
63
+ <div
64
+ {...statusProps}
65
+ className={cn(
66
+ 'rounded-full size-4 border border-primary-foreground',
67
+ statusColorConfig[status],
68
+ statusProps?.className
69
+ )}
70
+ />
71
+ </TooltipTrigger>
72
+ <TooltipPortal {...tooltipPortalProps}>
73
+ <TooltipContent
74
+ {...tooltipContentProps}
75
+ className={cn(
76
+ 'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
77
+ tooltipContentProps?.className
78
+ )}
79
+ >
80
+ <span>{tooltip || status || 'unknown'}</span>
81
+ <TooltipArrow
82
+ {...tooltipArrowProps}
83
+ className={cn(
84
+ 'bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]',
85
+ tooltipArrowProps?.className
86
+ )}
87
+ />
88
+ </TooltipContent>
89
+ </TooltipPortal>
90
+ </TooltipRoot>
91
+ </TooltipProvider>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ const StatusIndicator = memo(forwardRef(StatusIndicatorBase));
97
+
98
+ StatusIndicator.displayName = 'StatusIndicator';
99
+
100
+ export { StatusIndicator };
@@ -11,15 +11,7 @@ interface DismissLayerProps extends React.ComponentProps<'div'> {
11
11
  }
12
12
 
13
13
  function DismissLayerBase(
14
- {
15
- active,
16
- onDismiss,
17
- disableClickOutside = false,
18
- disableEscKeyPress = false,
19
- disabled,
20
- className,
21
- ...props
22
- }: DismissLayerProps,
14
+ { active, onDismiss, disableClickOutside = false, disableEscKeyPress = false, disabled, ...props }: DismissLayerProps,
23
15
  ref: React.Ref<HTMLDivElement>
24
16
  ) {
25
17
  const innerRef = useRef<HTMLDivElement>(null);
@@ -58,11 +50,11 @@ function DismissLayerBase(
58
50
  return (
59
51
  <div
60
52
  ref={innerRef}
61
- onClick={handleClickOrTouch}
62
- onTouchStart={handleClickOrTouch}
63
- className={cn('fixed top-0 left-0 z-40 size-full bg-black/20', className)}
64
53
  aria-hidden
65
54
  {...props}
55
+ onClick={handleClickOrTouch}
56
+ onTouchStart={handleClickOrTouch}
57
+ className={cn('fixed top-0 left-0 z-40 size-full bg-black/20', props?.className)}
66
58
  />
67
59
  );
68
60
  }
@@ -5,15 +5,23 @@ import { cn } from './utils';
5
5
  interface PlaceholderTextProps extends React.ComponentProps<'div'> {
6
6
  title?: string;
7
7
  titles?: string[];
8
+ rowProps?: React.ComponentProps<'div'>;
8
9
  }
9
10
 
10
- function PlaceholderTextBase({ title, titles = [], className }: PlaceholderTextProps, ref: React.Ref<HTMLDivElement>) {
11
+ function PlaceholderTextBase(
12
+ { title, titles = [], rowProps, ...props }: PlaceholderTextProps,
13
+ ref: React.Ref<HTMLDivElement>
14
+ ) {
11
15
  const rows = typeof title === 'string' ? [title, ...titles] : titles;
12
16
 
13
17
  return (
14
- <div ref={ref} className={cn('absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2', className)}>
18
+ <div
19
+ ref={ref}
20
+ {...props}
21
+ className={cn('absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2', props?.className)}
22
+ >
15
23
  {rows.map((row, index) => (
16
- <div key={`placeholder-text-${index}`} className="text-center">
24
+ <div key={`placeholder-text-${index}`} {...rowProps} className={cn('text-center', rowProps?.className)}>
17
25
  {row}
18
26
  </div>
19
27
  ))}
@@ -2,31 +2,38 @@ import type React from 'react';
2
2
  import type { LucideProps } from 'lucide-react';
3
3
  import { memo } from 'react';
4
4
  import { CircleCheck, CircleMinus, CircleQuestionMark, Clock } from 'lucide-react';
5
- import { capitalize, cn, UserPresence } from './utils';
5
+ import { capitalize, cn } from './utils';
6
6
 
7
- type PresenceStatus = UserPresence | 'available' | 'busy' | 'away' | 'unknown' | undefined;
8
-
9
- interface PresenceBadgeProps extends LucideProps {
10
- status: PresenceStatus;
11
- }
7
+ type PresenceStatus = 'available' | 'busy' | 'away' | 'unknown' | undefined;
12
8
 
13
9
  interface PresenceProps extends React.ComponentProps<'div'> {
14
10
  badge?: boolean;
15
11
  status: PresenceStatus;
16
12
  label: string;
13
+ badgeProps?: PresenceBadgeProps;
14
+ labelProps?: React.ComponentProps<'span'>;
17
15
  }
18
16
 
19
- function PresenceBadgeBase({ status, className, ...props }: PresenceBadgeProps) {
17
+ interface PresenceBadgeProps extends LucideProps {
18
+ status?: PresenceStatus;
19
+ }
20
+
21
+ function PresenceBadgeBase({ status, ...props }: PresenceBadgeProps) {
20
22
  switch (status) {
21
- case UserPresence.AVAILABLE || 'available':
22
- return <CircleCheck className={cn('rounded-full text-white bg-green-600 size-4.5', className)} {...props} />;
23
- case UserPresence.BUSY || 'busy':
24
- return <CircleMinus className={cn('rounded-full text-white bg-red-600 size-4.5', className)} {...props} />;
25
- case UserPresence.AWAY || 'away':
26
- return <Clock className={cn('rounded-full text-white bg-yellow-500 size-4.5', className)} {...props} />;
27
- case UserPresence.UNKNOWN || 'unknown':
23
+ case 'available':
24
+ return (
25
+ <CircleCheck {...props} className={cn('rounded-full text-white bg-green-600 size-4.5', props?.className)} />
26
+ );
27
+ case 'busy':
28
+ return <CircleMinus {...props} className={cn('rounded-full text-white bg-red-600 size-4.5', props?.className)} />;
29
+ case 'away':
30
+ return <Clock {...props} className={cn('rounded-full text-white bg-yellow-500 size-4.5', props?.className)} />;
31
+ case 'unknown':
28
32
  return (
29
- <CircleQuestionMark className={cn('rounded-full text-white bg-gray-500 size-4.5', className)} {...props} />
33
+ <CircleQuestionMark
34
+ {...props}
35
+ className={cn('rounded-full text-white bg-gray-500 size-4.5', props?.className)}
36
+ />
30
37
  );
31
38
  default:
32
39
  return null;
@@ -37,13 +44,13 @@ const PresenceBadge = memo(PresenceBadgeBase);
37
44
 
38
45
  PresenceBadge.displayName = 'PresenceBadge';
39
46
 
40
- function PresenceBase({ badge = true, status, label, className, ...props }: PresenceProps) {
47
+ function PresenceBase({ badge = true, status, label, badgeProps, labelProps, ...props }: PresenceProps) {
41
48
  const presence = capitalize(label || status);
42
49
 
43
50
  return (
44
- <div className={cn('flex items-center gap-2', className)} {...props}>
45
- {badge && <PresenceBadge status={status} />}
46
- <span>{presence}</span>
51
+ <div {...props} className={cn('flex items-center gap-2', props?.className)}>
52
+ {badge && <PresenceBadge status={status} {...badgeProps} />}
53
+ <span {...labelProps}>{presence}</span>
47
54
  </div>
48
55
  );
49
56
  }
@@ -0,0 +1,100 @@
1
+ import type React from 'react';
2
+ import type {
3
+ TooltipProviderProps,
4
+ TooltipProps,
5
+ TooltipTriggerProps,
6
+ TooltipPortalProps,
7
+ TooltipContentProps,
8
+ TooltipArrowProps,
9
+ } from '@radix-ui/react-tooltip';
10
+ import { forwardRef, memo } from 'react';
11
+ import {
12
+ Provider as TooltipProvider,
13
+ Root as TooltipRoot,
14
+ Trigger as TooltipTrigger,
15
+ Portal as TooltipPortal,
16
+ Content as TooltipContent,
17
+ Arrow as TooltipArrow,
18
+ } from '@radix-ui/react-tooltip';
19
+ import { cn } from './utils';
20
+
21
+ type StatusName = string | 'unknown';
22
+
23
+ interface StatusIndicatorProps extends React.ComponentProps<'div'> {
24
+ status?: StatusName;
25
+ statusColorConfig?: Record<StatusName, string>;
26
+ statusProps?: React.ComponentProps<'div'>;
27
+ tooltipProviderProps?: TooltipProviderProps;
28
+ tooltipProps?: TooltipProps;
29
+ tooltipTriggerProps?: TooltipTriggerProps;
30
+ tooltipPortalProps?: TooltipPortalProps;
31
+ tooltipContentProps?: TooltipContentProps;
32
+ tooltipArrowProps?: TooltipArrowProps;
33
+ tooltip?: string;
34
+ disabled?: boolean;
35
+ className?: string;
36
+ }
37
+
38
+ function StatusIndicatorBase(
39
+ {
40
+ status = 'unknown',
41
+ statusColorConfig = { unknown: 'bg-gray-500 border-gray-600' },
42
+ statusProps,
43
+ tooltipProviderProps,
44
+ tooltipProps,
45
+ tooltipTriggerProps,
46
+ tooltipPortalProps,
47
+ tooltipContentProps,
48
+ tooltipArrowProps,
49
+ tooltip,
50
+ disabled,
51
+ className,
52
+ ...props
53
+ }: StatusIndicatorProps,
54
+ ref: React.Ref<HTMLDivElement>
55
+ ) {
56
+ if (disabled) return null;
57
+
58
+ return (
59
+ <div ref={ref} {...props} className={cn('absolute top-0 left-0', className)}>
60
+ <TooltipProvider {...tooltipProviderProps}>
61
+ <TooltipRoot {...tooltipProps}>
62
+ <TooltipTrigger asChild {...tooltipTriggerProps}>
63
+ <div
64
+ {...statusProps}
65
+ className={cn(
66
+ 'rounded-full size-4 border border-primary-foreground',
67
+ statusColorConfig[status],
68
+ statusProps?.className
69
+ )}
70
+ />
71
+ </TooltipTrigger>
72
+ <TooltipPortal {...tooltipPortalProps}>
73
+ <TooltipContent
74
+ {...tooltipContentProps}
75
+ className={cn(
76
+ 'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
77
+ tooltipContentProps?.className
78
+ )}
79
+ >
80
+ <span>{tooltip || status || 'unknown'}</span>
81
+ <TooltipArrow
82
+ {...tooltipArrowProps}
83
+ className={cn(
84
+ 'bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]',
85
+ tooltipArrowProps?.className
86
+ )}
87
+ />
88
+ </TooltipContent>
89
+ </TooltipPortal>
90
+ </TooltipRoot>
91
+ </TooltipProvider>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ const StatusIndicator = memo(forwardRef(StatusIndicatorBase));
97
+
98
+ StatusIndicator.displayName = 'StatusIndicator';
99
+
100
+ export { StatusIndicator, type StatusIndicatorProps };
@@ -48,8 +48,8 @@ function StreamViewBase(
48
48
  autoPlay
49
49
  playsInline
50
50
  muted={isMuted}
51
- className={cn(defaultClassName, mirrorClassName, className)}
52
51
  {...props}
52
+ className={cn(defaultClassName, mirrorClassName, className)}
53
53
  />
54
54
  );
55
55
  }
@@ -86,16 +86,10 @@ interface FullscreenStreamViewProps extends React.ComponentProps<'div'> {
86
86
  navElement?: React.ReactElement<HTMLElement>;
87
87
  hideIconElement?: React.ReactElement;
88
88
  showIconElement?: React.ReactElement;
89
- containerClassName?: string;
90
- fullscreenButtonClassName?: string;
91
- fullscreenButtonIconClassName?: string;
92
- pipContainerClassName?: string;
93
- pipButtonClassName?: string;
94
- pipButtonIconClassName?: string;
95
89
  containerProps?: React.ComponentProps<'div'>;
96
90
  fullscreenButtonProps?: React.ComponentProps<'button'>;
97
91
  fullscreenButtonIconProps?: LucideProps;
98
- pipContainerProps?: React.ComponentProps<'div'>;
92
+ pipProps?: React.ComponentProps<'div'>;
99
93
  pipButtonProps?: React.ComponentProps<'button'>;
100
94
  pipButtonIconProps?: LucideProps;
101
95
  }
@@ -114,18 +108,12 @@ function FullscreenStreamViewBase(
114
108
  navElement,
115
109
  hideIconElement,
116
110
  showIconElement,
117
- containerClassName,
118
- fullscreenButtonClassName,
119
- fullscreenButtonIconClassName,
120
- pipContainerClassName,
121
- pipButtonClassName,
122
- pipButtonIconClassName,
123
- containerProps,
124
111
  fullscreenButtonProps,
125
112
  fullscreenButtonIconProps,
126
- pipContainerProps,
113
+ pipProps,
127
114
  pipButtonProps,
128
115
  pipButtonIconProps,
116
+ ...props
129
117
  }: FullscreenStreamViewProps,
130
118
  ref: React.Ref<FullscreenStreamViewRef>
131
119
  ) {
@@ -183,45 +171,45 @@ function FullscreenStreamViewBase(
183
171
  return (
184
172
  <div
185
173
  ref={innerRef}
186
- className={cn('relative flex items-center justify-center size-full', containerClassName)}
187
- {...containerProps}
174
+ {...props}
175
+ className={cn('relative flex items-center justify-center size-full', props?.className)}
188
176
  >
189
177
  {element}
190
178
  <button
191
179
  onClick={toggleFullscreen}
180
+ {...fullscreenButtonProps}
192
181
  className={cn(
193
182
  'absolute top-2 right-2 p-1 rounded-md bg-black/50 text-white hover:bg-black/80 z-10 shadow-xs shadow-white/25',
194
- fullscreenButtonClassName
183
+ fullscreenButtonProps?.className
195
184
  )}
196
- {...fullscreenButtonProps}
197
185
  >
198
186
  {isFullscreen
199
- ? hideIconElement || <Minimize className={fullscreenButtonIconClassName} {...fullscreenButtonIconProps} />
200
- : showIconElement || <Maximize className={fullscreenButtonIconClassName} {...fullscreenButtonIconProps} />}
187
+ ? hideIconElement || <Minimize {...fullscreenButtonIconProps} />
188
+ : showIconElement || <Maximize {...fullscreenButtonIconProps} />}
201
189
  </button>
202
190
  <div className="absolute size-full p-2 flex flex-col justify-end items-center">
203
191
  {isFullscreen && pipElement && (
204
192
  <div className="relative size-full flex items-end justify-end">
205
193
  {isPictureInPicture && (
206
194
  <div
195
+ {...pipProps}
207
196
  className={cn(
208
197
  'max-w-1/4 max-h-1/4 aspect-4/3 overflow-hidden rounded-md shadow-md shadow-white/25',
209
- pipContainerClassName
198
+ pipProps?.className
210
199
  )}
211
- {...pipContainerProps}
212
200
  >
213
201
  {pipElement}
214
202
  </div>
215
203
  )}
216
204
  <button
217
205
  onClick={togglePictureInPicture}
206
+ {...pipButtonProps}
218
207
  className={cn(
219
208
  'absolute bottom-2 right-2 p-1 rounded-md bg-black/50 text-white hover:bg-black/80 z-10 shadow-xs shadow-white/25',
220
- pipButtonClassName
209
+ pipButtonProps?.className
221
210
  )}
222
- {...pipButtonProps}
223
211
  >
224
- <PictureInPicture2 className={pipButtonIconClassName} {...pipButtonIconProps} />
212
+ <PictureInPicture2 {...pipButtonIconProps} />
225
213
  </button>
226
214
  </div>
227
215
  )}
@@ -19,10 +19,3 @@ export function getInitialsFromName(name?: string): string {
19
19
  export function capitalize(str?: string): string {
20
20
  return typeof str === 'string' && str.length > 0 ? `${str[0]?.toUpperCase()}${str.slice(1)}` : '';
21
21
  }
22
-
23
- export enum UserPresence {
24
- AVAILABLE = 'available',
25
- BUSY = 'busy',
26
- AWAY = 'away',
27
- UNKNOWN = 'unknown',
28
- }