pxengine 0.1.9 → 0.1.10

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.
Files changed (74) hide show
  1. package/README.md +79 -122
  2. package/dist/index.cjs +35586 -8241
  3. package/dist/index.d.cts +574 -33
  4. package/dist/index.d.ts +574 -33
  5. package/dist/index.js +35525 -8199
  6. package/dist/registry.json +1776 -113
  7. package/package.json +1 -1
  8. package/src/atoms/BadgeAtom.tsx +11 -3
  9. package/src/atoms/ButtonAtom.tsx +2 -2
  10. package/src/atoms/CalendarAtom.tsx +1 -1
  11. package/src/atoms/CardAtom.tsx +1 -1
  12. package/src/atoms/ChartAtom.tsx +190 -0
  13. package/src/atoms/CheckboxAtom.tsx +33 -0
  14. package/src/atoms/ContextMenuAtom.tsx +49 -0
  15. package/src/atoms/DrawerAtom.tsx +49 -0
  16. package/src/atoms/DropdownMenuAtom.tsx +49 -0
  17. package/src/atoms/InputAtom.tsx +49 -23
  18. package/src/atoms/InputOTPAtom.tsx +49 -0
  19. package/src/atoms/KbdAtom.tsx +25 -0
  20. package/src/atoms/LabelAtom.tsx +23 -0
  21. package/src/atoms/RadioGroupAtom.tsx +31 -0
  22. package/src/atoms/RatingAtom.tsx +37 -0
  23. package/src/atoms/ResizableAtom.tsx +51 -0
  24. package/src/atoms/SliderAtom.tsx +32 -0
  25. package/src/atoms/SwitchAtom.tsx +32 -0
  26. package/src/atoms/TextareaAtom.tsx +42 -0
  27. package/src/atoms/TimelineAtom.tsx +77 -0
  28. package/src/atoms/ToggleAtom.tsx +36 -0
  29. package/src/atoms/VideoAtom.tsx +34 -0
  30. package/src/atoms/index.ts +17 -0
  31. package/src/components/ui/resizable.tsx +5 -6
  32. package/src/molecules/creator-discovery/AudienceDemographicsCard/AudienceDemographicsCard.tsx +44 -0
  33. package/src/molecules/creator-discovery/AudienceDemographicsCard/index.ts +1 -0
  34. package/src/molecules/creator-discovery/AudienceMetricCard/AudienceMetricCard.tsx +50 -0
  35. package/src/molecules/creator-discovery/AudienceMetricCard/index.ts +1 -0
  36. package/src/molecules/creator-discovery/BrandAffinityGroup/BrandAffinityGroup.tsx +36 -0
  37. package/src/molecules/creator-discovery/BrandAffinityGroup/index.ts +1 -0
  38. package/src/molecules/creator-discovery/ContentPreviewGallery/ContentPreviewGallery.tsx +41 -0
  39. package/src/molecules/creator-discovery/ContentPreviewGallery/index.ts +1 -0
  40. package/src/molecules/creator-discovery/CreatorActionHeader/CreatorActionHeader.tsx +77 -0
  41. package/src/molecules/creator-discovery/CreatorActionHeader/index.ts +1 -0
  42. package/src/molecules/creator-discovery/CreatorGridCard/CreatorGridCard.tsx +104 -0
  43. package/src/molecules/creator-discovery/CreatorGridCard/index.ts +1 -0
  44. package/src/molecules/creator-discovery/CreatorProfileSummary/CreatorProfileSummary.tsx +65 -0
  45. package/src/molecules/creator-discovery/CreatorProfileSummary/index.ts +1 -0
  46. package/src/molecules/creator-discovery/GrowthChartCard/GrowthChartCard.tsx +58 -0
  47. package/src/molecules/creator-discovery/GrowthChartCard/index.ts +1 -0
  48. package/src/molecules/creator-discovery/PlatformIconGroup/PlatformIconGroup.tsx +72 -0
  49. package/src/molecules/creator-discovery/PlatformIconGroup/index.ts +1 -0
  50. package/src/molecules/creator-discovery/TopPostsGrid/TopPostsGrid.tsx +49 -0
  51. package/src/molecules/creator-discovery/TopPostsGrid/index.ts +1 -0
  52. package/src/molecules/creator-discovery/index.ts +10 -0
  53. package/src/molecules/generic/DataGrid/DataGrid.tsx +102 -0
  54. package/src/molecules/generic/DataGrid/index.ts +1 -0
  55. package/src/molecules/generic/EmptyState/EmptyState.tsx +61 -0
  56. package/src/molecules/generic/EmptyState/index.ts +1 -0
  57. package/src/molecules/generic/FileUpload/FileUpload.tsx +62 -0
  58. package/src/molecules/generic/FileUpload/index.ts +1 -0
  59. package/src/molecules/generic/FilterBar/FilterBar.tsx +54 -0
  60. package/src/molecules/generic/FilterBar/index.ts +1 -0
  61. package/src/molecules/generic/LoadingOverlay/LoadingOverlay.tsx +39 -0
  62. package/src/molecules/generic/LoadingOverlay/index.ts +1 -0
  63. package/src/molecules/generic/NotificationList/NotificationList.tsx +80 -0
  64. package/src/molecules/generic/NotificationList/index.ts +1 -0
  65. package/src/molecules/generic/StatsGrid/StatsGrid.tsx +80 -0
  66. package/src/molecules/generic/StatsGrid/index.ts +1 -0
  67. package/src/molecules/generic/StepWizard/StepWizard.tsx +67 -0
  68. package/src/molecules/generic/StepWizard/index.ts +1 -0
  69. package/src/molecules/generic/TagCloud/TagCloud.tsx +32 -0
  70. package/src/molecules/generic/TagCloud/index.ts +1 -0
  71. package/src/molecules/generic/index.ts +9 -0
  72. package/src/render/PXEngineRenderer.tsx +1 -1
  73. package/src/types/atoms.ts +150 -2
  74. package/src/types/molecules.ts +226 -1
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { Label } from "@/components/ui/label";
3
+ import { LabelAtomType } from "../types/atoms";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ /**
7
+ * LabelAtom
8
+ * Wraps shadcn Label
9
+ */
10
+ export const LabelAtom: React.FC<LabelAtomType> = ({
11
+ content,
12
+ htmlFor,
13
+ className,
14
+ }) => {
15
+ return (
16
+ <Label
17
+ htmlFor={htmlFor}
18
+ className={cn("text-sm font-semibold text-gray-700", className)}
19
+ >
20
+ {content}
21
+ </Label>
22
+ );
23
+ };
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
3
+ import { Label } from "@/components/ui/label";
4
+ import { RadioGroupAtomType } from "../types/atoms";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ /**
8
+ * RadioGroupAtom
9
+ * Wraps shadcn RadioGroup
10
+ */
11
+ export const RadioGroupAtom: React.FC<
12
+ RadioGroupAtomType & { onValueChange?: (value: string) => void }
13
+ > = ({ options, defaultValue, disabled, className, onValueChange }) => {
14
+ return (
15
+ <RadioGroup
16
+ defaultValue={defaultValue}
17
+ disabled={disabled}
18
+ onValueChange={onValueChange}
19
+ className={cn("grid gap-2", className)}
20
+ >
21
+ {options.map((option) => (
22
+ <div key={option.value} className="flex items-center space-x-2">
23
+ <RadioGroupItem value={option.value} id={option.value} />
24
+ <Label htmlFor={option.value} className="cursor-pointer">
25
+ {option.label}
26
+ </Label>
27
+ </div>
28
+ ))}
29
+ </RadioGroup>
30
+ );
31
+ };
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ import { Star } from "lucide-react";
3
+ import { RatingAtomType } from "../types/atoms";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ /**
7
+ * RatingAtom
8
+ * A star rating component for display or user input.
9
+ */
10
+ export const RatingAtom: React.FC<
11
+ RatingAtomType & { onChange?: (val: number) => void }
12
+ > = ({ value, max = 5, readonly = false, className, onChange }) => {
13
+ const [hoverValue, setHoverValue] = React.useState<number | null>(null);
14
+
15
+ return (
16
+ <div className={cn("flex items-center gap-1", className)}>
17
+ {Array.from({ length: max }, (_, i) => {
18
+ const starValue = i + 1;
19
+ const isActive = (hoverValue ?? value) >= starValue;
20
+
21
+ return (
22
+ <Star
23
+ key={i}
24
+ className={cn(
25
+ "h-5 w-5 transition-colors",
26
+ isActive ? "fill-yellow-400 text-yellow-400" : "text-gray-300",
27
+ !readonly && "cursor-pointer hover:scale-110",
28
+ )}
29
+ onClick={() => !readonly && onChange?.(starValue)}
30
+ onMouseEnter={() => !readonly && setHoverValue(starValue)}
31
+ onMouseLeave={() => !readonly && setHoverValue(null)}
32
+ />
33
+ );
34
+ })}
35
+ </div>
36
+ );
37
+ };
@@ -0,0 +1,51 @@
1
+ import React from "react";
2
+ import {
3
+ ResizableHandle,
4
+ ResizablePanel,
5
+ ResizablePanelGroup,
6
+ } from "@/components/ui/resizable";
7
+ import { ResizableAtomType, UIComponent } from "../types/schema";
8
+ import { cn } from "@/lib/utils";
9
+
10
+ interface Props extends ResizableAtomType {
11
+ renderComponent: (component: UIComponent, index?: number) => React.ReactNode;
12
+ }
13
+
14
+ /**
15
+ * ResizableAtom
16
+ * Wraps shadcn Resizable panels for complex layouts
17
+ */
18
+ export const ResizableAtom: React.FC<Props> = ({
19
+ direction,
20
+ panels,
21
+ className,
22
+ renderComponent,
23
+ }) => {
24
+ // Use a locally typed component to bypass DTS inference issues
25
+ const Group = ResizablePanelGroup as any;
26
+
27
+ return (
28
+ <Group
29
+ direction={direction}
30
+ className={cn(
31
+ "min-h-[200px] w-full rounded-lg border border-purple-100",
32
+ className,
33
+ )}
34
+ >
35
+ {panels.map((panel: any, i: number) => (
36
+ <React.Fragment key={i}>
37
+ <ResizablePanel defaultSize={panel.defaultSize} className="p-4">
38
+ {panel.children.map((child: any, childIdx: number) => (
39
+ <React.Fragment
40
+ key={child.id || `resizable-child-${i}-${childIdx}`}
41
+ >
42
+ {renderComponent(child)}
43
+ </React.Fragment>
44
+ ))}
45
+ </ResizablePanel>
46
+ {i < panels.length - 1 && <ResizableHandle withHandle />}
47
+ </React.Fragment>
48
+ ))}
49
+ </Group>
50
+ );
51
+ };
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import { Slider } from "@/components/ui/slider";
3
+ import { SliderAtomType } from "../types/atoms";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ /**
7
+ * SliderAtom
8
+ * Wraps shadcn Slider
9
+ */
10
+ export const SliderAtom: React.FC<
11
+ SliderAtomType & { onValueChange?: (value: number[]) => void }
12
+ > = ({
13
+ defaultValue = [50],
14
+ max = 100,
15
+ min = 0,
16
+ step = 1,
17
+ disabled,
18
+ className,
19
+ onValueChange,
20
+ }) => {
21
+ return (
22
+ <Slider
23
+ defaultValue={defaultValue}
24
+ max={max}
25
+ min={min}
26
+ step={step}
27
+ disabled={disabled}
28
+ onValueChange={onValueChange}
29
+ className={cn("w-full py-4", className)}
30
+ />
31
+ );
32
+ };
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import { Switch } from "@/components/ui/switch";
3
+ import { SwitchAtomType } from "../types/atoms";
4
+ import { Label } from "@/components/ui/label";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ /**
8
+ * SwitchAtom
9
+ * Wraps shadcn Switch with label support
10
+ */
11
+ export const SwitchAtom: React.FC<
12
+ SwitchAtomType & { onCheckedChange?: (checked: boolean) => void }
13
+ > = ({ id, label, checked, disabled, className, onCheckedChange }) => {
14
+ return (
15
+ <div className={cn("flex items-center space-x-2", className)}>
16
+ <Switch
17
+ id={id}
18
+ checked={checked}
19
+ disabled={disabled}
20
+ onCheckedChange={onCheckedChange}
21
+ />
22
+ {label && (
23
+ <Label
24
+ htmlFor={id}
25
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
26
+ >
27
+ {label}
28
+ </Label>
29
+ )}
30
+ </div>
31
+ );
32
+ };
@@ -0,0 +1,42 @@
1
+ import React from "react";
2
+ import { Textarea } from "@/components/ui/textarea";
3
+ import { InputAtomType } from "../types/atoms";
4
+ import { Label } from "@/components/ui/label";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ /**
8
+ * TextareaAtom
9
+ * Standalone textarea atom
10
+ */
11
+ export const TextareaAtom: React.FC<
12
+ InputAtomType & { onChange?: (val: string) => void }
13
+ > = ({
14
+ id,
15
+ label,
16
+ placeholder,
17
+ defaultValue,
18
+ disabled,
19
+ required,
20
+ className,
21
+ onChange,
22
+ }) => {
23
+ return (
24
+ <div className={cn("grid w-full gap-1.5", className)}>
25
+ {label && (
26
+ <Label htmlFor={id} className="font-semibold">
27
+ {label}
28
+ {required && <span className="text-red-500 ml-1">*</span>}
29
+ </Label>
30
+ )}
31
+ <Textarea
32
+ id={id}
33
+ placeholder={placeholder}
34
+ defaultValue={defaultValue}
35
+ disabled={disabled}
36
+ required={required}
37
+ onChange={(e) => onChange?.(e.target.value)}
38
+ className="min-h-[100px] border-purple-100 focus-visible:ring-purple-500 resize-none shadow-sm"
39
+ />
40
+ </div>
41
+ );
42
+ };
@@ -0,0 +1,77 @@
1
+ import React from "react";
2
+ import { TimelineAtomType } from "../types/atoms";
3
+ import { cn } from "@/lib/utils";
4
+ import * as Icons from "lucide-react";
5
+
6
+ /**
7
+ * TimelineAtom
8
+ * A vertical timeline primitive for processes and steps.
9
+ */
10
+ export const TimelineAtom: React.FC<TimelineAtomType> = ({
11
+ items,
12
+ className,
13
+ }) => {
14
+ return (
15
+ <div
16
+ className={cn(
17
+ "space-y-6 relative before:absolute before:inset-0 before:ml-5 before:-translate-x-px before:h-full before:w-0.5 before:bg-gradient-to-b before:from-purple-500 before:via-indigo-500 before:to-gray-200 before:pointer-events-none",
18
+ className,
19
+ )}
20
+ >
21
+ {items.map((item, i) => {
22
+ const Icon = item.icon ? (Icons as any)[item.icon] : null;
23
+ const isCompleted = item.status === "completed";
24
+ const isActive = item.status === "active";
25
+
26
+ return (
27
+ <div key={i} className="relative flex items-start gap-6 group">
28
+ <div
29
+ className={cn(
30
+ "flex items-center justify-center w-10 h-10 rounded-full border-4 border-white shadow-sm shrink-0 z-10 transition-all group-hover:scale-110",
31
+ isCompleted
32
+ ? "bg-purple-600 text-white"
33
+ : isActive
34
+ ? "bg-indigo-600 text-white ring-4 ring-indigo-50"
35
+ : "bg-gray-100 text-gray-400",
36
+ )}
37
+ >
38
+ {Icon ? (
39
+ <Icon className="w-5 h-5" />
40
+ ) : isCompleted ? (
41
+ <Icons.Check className="w-5 h-5" />
42
+ ) : (
43
+ <div className="w-2 h-2 rounded-full bg-current" />
44
+ )}
45
+ </div>
46
+ <div className="flex flex-col gap-1 pt-0.5">
47
+ <div className="flex items-center gap-2">
48
+ <h4
49
+ className={cn(
50
+ "font-bold text-sm",
51
+ isCompleted
52
+ ? "text-gray-900"
53
+ : isActive
54
+ ? "text-indigo-600"
55
+ : "text-gray-500",
56
+ )}
57
+ >
58
+ {item.title}
59
+ </h4>
60
+ {item.timestamp && (
61
+ <span className="text-[10px] text-muted-foreground bg-gray-50 px-1.5 py-0.5 rounded-md font-medium border border-gray-100">
62
+ {item.timestamp}
63
+ </span>
64
+ )}
65
+ </div>
66
+ {item.description && (
67
+ <p className="text-xs text-muted-foreground leading-relaxed max-w-sm">
68
+ {item.description}
69
+ </p>
70
+ )}
71
+ </div>
72
+ </div>
73
+ );
74
+ })}
75
+ </div>
76
+ );
77
+ };
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { Toggle } from "@/components/ui/toggle";
3
+ import { ToggleAtomType } from "../types/atoms";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ /**
7
+ * ToggleAtom
8
+ * Wraps shadcn Toggle
9
+ */
10
+ export const ToggleAtom: React.FC<
11
+ ToggleAtomType & { onPressedChange?: (pressed: boolean) => void }
12
+ > = ({
13
+ label,
14
+ pressed,
15
+ disabled,
16
+ size = "md",
17
+ variant = "default",
18
+ className,
19
+ onPressedChange,
20
+ }) => {
21
+ return (
22
+ <Toggle
23
+ pressed={pressed}
24
+ disabled={disabled}
25
+ size={size as any}
26
+ variant={variant as any}
27
+ onPressedChange={onPressedChange}
28
+ className={cn(
29
+ "data-[state=on]:bg-purple-100 data-[state=on]:text-purple-900",
30
+ className,
31
+ )}
32
+ >
33
+ {label}
34
+ </Toggle>
35
+ );
36
+ };
@@ -0,0 +1,34 @@
1
+ import React from "react";
2
+ import { VideoAtomType } from "../types/atoms";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ /**
6
+ * VideoAtom
7
+ * A simple, styled video player for content previews.
8
+ */
9
+ export const VideoAtom: React.FC<VideoAtomType> = ({
10
+ src,
11
+ poster,
12
+ autoplay = false,
13
+ controls = true,
14
+ muted = false,
15
+ loop = false,
16
+ className,
17
+ }) => {
18
+ return (
19
+ <div
20
+ className={cn("relative overflow-hidden rounded-2xl bg-black", className)}
21
+ >
22
+ <video
23
+ src={src}
24
+ poster={poster}
25
+ autoPlay={autoplay}
26
+ controls={controls}
27
+ muted={muted}
28
+ loop={loop}
29
+ className="h-full w-full object-cover"
30
+ playsInline
31
+ />
32
+ </div>
33
+ );
34
+ };
@@ -30,3 +30,20 @@ export * from "./CommandAtom";
30
30
  export * from "./FormInputAtom";
31
31
  export * from "./FormSelectAtom";
32
32
  export * from "./FormTextareaAtom";
33
+ export * from "./CheckboxAtom";
34
+ export * from "./SwitchAtom";
35
+ export * from "./LabelAtom";
36
+ export * from "./TextareaAtom";
37
+ export * from "./ToggleAtom";
38
+ export * from "./SliderAtom";
39
+ export * from "./RadioGroupAtom";
40
+ export * from "./DropdownMenuAtom";
41
+ export * from "./ContextMenuAtom";
42
+ export * from "./DrawerAtom";
43
+ export * from "./InputOTPAtom";
44
+ export * from "./KbdAtom";
45
+ export * from "./ResizableAtom";
46
+ export * from "./ChartAtom";
47
+ export * from "./VideoAtom";
48
+ export * from "./RatingAtom";
49
+ export * from "./TimelineAtom";
@@ -5,16 +5,15 @@ import * as ResizablePrimitive from "react-resizable-panels";
5
5
 
6
6
  import { cn } from "@/lib/utils";
7
7
 
8
- const ResizablePanelGroup = ({
9
- className,
10
- ...props
11
- }: React.ComponentProps<typeof ResizablePrimitive.Group>) => (
8
+ const ResizablePanelGroup = (
9
+ props: React.ComponentProps<typeof ResizablePrimitive.Group>,
10
+ ) => (
12
11
  <ResizablePrimitive.Group
12
+ {...props}
13
13
  className={cn(
14
14
  "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
15
- className,
15
+ props.className,
16
16
  )}
17
- {...props}
18
17
  />
19
18
  );
20
19
 
@@ -0,0 +1,44 @@
1
+ import React from "react";
2
+ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
3
+ import { ChartAtom } from "../../../atoms/ChartAtom";
4
+ import { AudienceDemographicsCardMolecule } from "../../../types/molecules";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ /**
8
+ * AudienceDemographicsCard
9
+ * Visualizes audience data using appropriate chart types.
10
+ */
11
+ export const AudienceDemographicsCard: React.FC<
12
+ AudienceDemographicsCardMolecule
13
+ > = ({ title, data, config, demographicType, className }) => {
14
+ const chartType = demographicType === "location" ? "bar" : "pie";
15
+
16
+ return (
17
+ <Card
18
+ className={cn(
19
+ "rounded-[32px] border-purple-50 shadow-sm overflow-hidden",
20
+ className,
21
+ )}
22
+ >
23
+ <CardHeader className="pb-2">
24
+ <CardTitle className="text-lg font-bold text-gray-900">
25
+ {title}
26
+ </CardTitle>
27
+ </CardHeader>
28
+ <CardContent>
29
+ <div className="h-[250px] w-full">
30
+ <ChartAtom
31
+ type="chart"
32
+ id="audience-demographics"
33
+ chartType={chartType}
34
+ data={data}
35
+ config={config}
36
+ XAxisKey="name"
37
+ YAxisKey="value"
38
+ showLegend={true}
39
+ />
40
+ </div>
41
+ </CardContent>
42
+ </Card>
43
+ );
44
+ };
@@ -0,0 +1 @@
1
+ export * from "./AudienceDemographicsCard";
@@ -0,0 +1,50 @@
1
+ import React from "react";
2
+ import { Card, CardHeader, CardTitle, CardContent } from "@/components";
3
+ import { ProgressAtom } from "../../../atoms/ProgressAtom";
4
+ import { AudienceMetricCardMolecule } from "../../../types/molecules";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ /**
8
+ * AudienceMetricCard
9
+ * A card displaying specific audience metrics with progress bars.
10
+ */
11
+ export const AudienceMetricCard: React.FC<AudienceMetricCardMolecule> = ({
12
+ title,
13
+ metrics,
14
+ className,
15
+ }) => {
16
+ return (
17
+ <Card
18
+ className={cn(
19
+ "bg-white border-purple-50 rounded-3xl overflow-hidden",
20
+ className,
21
+ )}
22
+ >
23
+ <CardHeader className="pb-2">
24
+ <CardTitle className="text-lg font-bold text-gray-900">
25
+ {title}
26
+ </CardTitle>
27
+ </CardHeader>
28
+ <CardContent className="space-y-4">
29
+ {metrics.map((metric, i) => (
30
+ <div key={i} className="space-y-1.5">
31
+ <div className="flex justify-between items-center text-sm">
32
+ <span className="font-medium text-gray-700">{metric.label}</span>
33
+ <span className="font-bold text-gray-900">{metric.value}</span>
34
+ </div>
35
+ {metric.percentage !== undefined && (
36
+ <ProgressAtom
37
+ id={`progress-${i}`}
38
+ type="progress"
39
+ value={metric.percentage}
40
+ className="h-1.5 bg-purple-50"
41
+ /* @ts-ignore - custom className for progress indicator */
42
+ indicatorClassName="bg-gradient-to-r from-purple-500 to-indigo-500"
43
+ />
44
+ )}
45
+ </div>
46
+ ))}
47
+ </CardContent>
48
+ </Card>
49
+ );
50
+ };
@@ -0,0 +1 @@
1
+ export * from "./AudienceMetricCard";
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { BrandAffinityGroupMolecule } from "../../../types/molecules";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ /**
6
+ * BrandAffinityGroup
7
+ * Visual group of associated brand logos.
8
+ */
9
+ export const BrandAffinityGroup: React.FC<BrandAffinityGroupMolecule> = ({
10
+ brands,
11
+ className,
12
+ }) => {
13
+ return (
14
+ <div className={cn("flex flex-col gap-3", className)}>
15
+ <h5 className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
16
+ Recent Collaborations
17
+ </h5>
18
+ <div className="flex flex-wrap gap-4 items-center">
19
+ {brands.map((brand, i) => (
20
+ <div key={i} className="group relative">
21
+ <div className="h-10 w-10 overflow-hidden rounded-xl bg-white border border-gray-100 p-1.5 flex items-center justify-center grayscale hover:grayscale-0 transition-all duration-300 hover:scale-110 shadow-sm hover:shadow-md">
22
+ <img
23
+ src={brand.logoSrc}
24
+ alt={brand.name}
25
+ className="max-h-full max-w-full object-contain"
26
+ />
27
+ </div>
28
+ <div className="absolute -bottom-6 left-1/2 -translate-x-1/2 px-2 py-0.5 bg-gray-900 text-white text-[10px] rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 pointer-events-none">
29
+ {brand.name}
30
+ </div>
31
+ </div>
32
+ ))}
33
+ </div>
34
+ </div>
35
+ );
36
+ };
@@ -0,0 +1 @@
1
+ export * from "./BrandAffinityGroup";
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+ import { ContentPreviewGalleryMolecule } from "../../../types/molecules";
3
+ import { cn } from "@/lib/utils";
4
+ import { Play } from "lucide-react";
5
+
6
+ /**
7
+ * ContentPreviewGallery
8
+ * Grid/Gallery of thumbnail previews.
9
+ */
10
+ export const ContentPreviewGallery: React.FC<ContentPreviewGalleryMolecule> = ({
11
+ items,
12
+ className,
13
+ }) => {
14
+ return (
15
+ <div className={cn("grid grid-cols-2 gap-2", className)}>
16
+ {items.map((item, i) => (
17
+ <a
18
+ key={i}
19
+ href={item.url || "#"}
20
+ target="_blank"
21
+ rel="noopener noreferrer"
22
+ className="group relative aspect-square overflow-hidden rounded-2xl bg-gray-100"
23
+ >
24
+ <img
25
+ src={item.thumbnail}
26
+ alt="Content preview"
27
+ className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110"
28
+ />
29
+ {item.type === "video" && (
30
+ <div className="absolute inset-0 flex items-center justify-center bg-black/10 group-hover:bg-black/20 transition-colors">
31
+ <div className="rounded-full bg-white/30 backdrop-blur-md p-3 text-white ring-1 ring-white/50">
32
+ <Play className="h-6 w-6 fill-white" />
33
+ </div>
34
+ </div>
35
+ )}
36
+ <div className="absolute inset-0 ring-1 ring-inset ring-black/10 rounded-2xl" />
37
+ </a>
38
+ ))}
39
+ </div>
40
+ );
41
+ };
@@ -0,0 +1 @@
1
+ export * from "./ContentPreviewGallery";