@djangocfg/ui-nextjs 2.1.43 → 2.1.45
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/package.json +4 -4
- package/src/blocks/SplitHero/SplitHero.tsx +95 -0
- package/src/blocks/SplitHero/SplitHeroContent.tsx +117 -0
- package/src/blocks/SplitHero/SplitHeroMedia.tsx +66 -0
- package/src/blocks/SplitHero/index.ts +13 -0
- package/src/blocks/SplitHero/types.ts +75 -0
- package/src/blocks/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-nextjs",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.45",
|
|
4
4
|
"description": "Next.js UI component library with Radix UI primitives, Tailwind CSS styling, charts, and form components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"check": "tsc --noEmit"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@djangocfg/api": "^2.1.
|
|
62
|
-
"@djangocfg/ui-core": "^2.1.
|
|
61
|
+
"@djangocfg/api": "^2.1.45",
|
|
62
|
+
"@djangocfg/ui-core": "^2.1.45",
|
|
63
63
|
"@types/react": "^19.1.0",
|
|
64
64
|
"@types/react-dom": "^19.1.0",
|
|
65
65
|
"consola": "^3.4.2",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"vidstack": "next"
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
109
|
+
"@djangocfg/typescript-config": "^2.1.45",
|
|
110
110
|
"@types/node": "^24.7.2",
|
|
111
111
|
"eslint": "^9.37.0",
|
|
112
112
|
"tailwindcss-animate": "1.0.7",
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
import { SplitHeroContent } from './SplitHeroContent';
|
|
6
|
+
import { SplitHeroMedia } from './SplitHeroMedia';
|
|
7
|
+
import type { SplitHeroProps } from './types';
|
|
8
|
+
|
|
9
|
+
const MAX_WIDTH_CLASSES = {
|
|
10
|
+
sm: 'max-w-3xl',
|
|
11
|
+
md: 'max-w-4xl',
|
|
12
|
+
lg: 'max-w-5xl',
|
|
13
|
+
xl: 'max-w-6xl',
|
|
14
|
+
full: 'max-w-7xl',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const BACKGROUND_CLASSES = {
|
|
18
|
+
none: '',
|
|
19
|
+
muted: 'bg-muted/30',
|
|
20
|
+
gradient: 'bg-gradient-to-br from-primary/5 via-background to-secondary/5',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const SplitHero: React.FC<SplitHeroProps> = ({
|
|
24
|
+
badge,
|
|
25
|
+
title,
|
|
26
|
+
titleGradient,
|
|
27
|
+
subtitle,
|
|
28
|
+
features,
|
|
29
|
+
primaryAction,
|
|
30
|
+
secondaryAction,
|
|
31
|
+
media,
|
|
32
|
+
layout = 'text-left',
|
|
33
|
+
mobileOrder = 'text-first',
|
|
34
|
+
align = 'center',
|
|
35
|
+
maxWidth = 'xl',
|
|
36
|
+
background = 'none',
|
|
37
|
+
className,
|
|
38
|
+
}) => {
|
|
39
|
+
const isTextLeft = layout === 'text-left';
|
|
40
|
+
const isMediaFirst = mobileOrder === 'media-first';
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<section
|
|
44
|
+
className={cn(
|
|
45
|
+
'py-12 md:py-16 lg:py-20',
|
|
46
|
+
BACKGROUND_CLASSES[background],
|
|
47
|
+
className
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<div className={cn('mx-auto px-4', MAX_WIDTH_CLASSES[maxWidth])}>
|
|
51
|
+
<div
|
|
52
|
+
className={cn(
|
|
53
|
+
'grid gap-8 lg:gap-12 items-center',
|
|
54
|
+
media ? 'lg:grid-cols-2' : 'lg:grid-cols-1 max-w-3xl mx-auto'
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
{/* Content - order changes based on layout and mobile settings */}
|
|
58
|
+
<div
|
|
59
|
+
className={cn(
|
|
60
|
+
// Desktop order based on layout
|
|
61
|
+
isTextLeft ? 'lg:order-1' : 'lg:order-2',
|
|
62
|
+
// Mobile order based on mobileOrder
|
|
63
|
+
isMediaFirst ? 'order-2' : 'order-1'
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<SplitHeroContent
|
|
67
|
+
badge={badge}
|
|
68
|
+
title={title}
|
|
69
|
+
titleGradient={titleGradient}
|
|
70
|
+
subtitle={subtitle}
|
|
71
|
+
features={features}
|
|
72
|
+
primaryAction={primaryAction}
|
|
73
|
+
secondaryAction={secondaryAction}
|
|
74
|
+
align={align}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Media */}
|
|
79
|
+
{media && (
|
|
80
|
+
<div
|
|
81
|
+
className={cn(
|
|
82
|
+
// Desktop order based on layout
|
|
83
|
+
isTextLeft ? 'lg:order-2' : 'lg:order-1',
|
|
84
|
+
// Mobile order based on mobileOrder
|
|
85
|
+
isMediaFirst ? 'order-1' : 'order-2'
|
|
86
|
+
)}
|
|
87
|
+
>
|
|
88
|
+
<SplitHeroMedia media={media} />
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</section>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { ArrowRight, Sparkles } from 'lucide-react';
|
|
5
|
+
import { ButtonLink } from '@djangocfg/ui-core/components';
|
|
6
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
7
|
+
import type { SplitHeroBadge, SplitHeroAction, SplitHeroFeature } from './types';
|
|
8
|
+
|
|
9
|
+
interface SplitHeroContentProps {
|
|
10
|
+
badge?: SplitHeroBadge;
|
|
11
|
+
title: string;
|
|
12
|
+
titleGradient?: string;
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
features?: SplitHeroFeature[];
|
|
15
|
+
primaryAction?: SplitHeroAction;
|
|
16
|
+
secondaryAction?: SplitHeroAction;
|
|
17
|
+
align?: 'top' | 'center' | 'bottom';
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const SplitHeroContent: React.FC<SplitHeroContentProps> = ({
|
|
22
|
+
badge,
|
|
23
|
+
title,
|
|
24
|
+
titleGradient,
|
|
25
|
+
subtitle,
|
|
26
|
+
features = [],
|
|
27
|
+
primaryAction,
|
|
28
|
+
secondaryAction,
|
|
29
|
+
align = 'center',
|
|
30
|
+
className,
|
|
31
|
+
}) => {
|
|
32
|
+
const alignClasses = {
|
|
33
|
+
top: 'justify-start',
|
|
34
|
+
center: 'justify-center',
|
|
35
|
+
bottom: 'justify-end',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={cn('flex flex-col', alignClasses[align], className)}>
|
|
40
|
+
{/* Badge */}
|
|
41
|
+
{badge && (
|
|
42
|
+
<div className="inline-flex items-center gap-2 px-3 py-1.5 bg-primary/10 text-primary rounded-full text-sm font-medium mb-4 w-fit border border-primary/20">
|
|
43
|
+
{badge.icon || <Sparkles className="w-3.5 h-3.5" />}
|
|
44
|
+
{badge.text}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
{/* Title */}
|
|
49
|
+
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-foreground mb-4 leading-tight">
|
|
50
|
+
{title}
|
|
51
|
+
{titleGradient && (
|
|
52
|
+
<>
|
|
53
|
+
{' '}
|
|
54
|
+
<span className="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
|
55
|
+
{titleGradient}
|
|
56
|
+
</span>
|
|
57
|
+
</>
|
|
58
|
+
)}
|
|
59
|
+
</h1>
|
|
60
|
+
|
|
61
|
+
{/* Subtitle */}
|
|
62
|
+
{subtitle && (
|
|
63
|
+
<p className="text-lg text-muted-foreground mb-6 leading-relaxed">
|
|
64
|
+
{subtitle}
|
|
65
|
+
</p>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{/* Features */}
|
|
69
|
+
{features.length > 0 && (
|
|
70
|
+
<div className="flex flex-wrap gap-3 mb-6">
|
|
71
|
+
{features.map((feature, index) => (
|
|
72
|
+
<div
|
|
73
|
+
key={index}
|
|
74
|
+
className="flex items-center gap-2 text-sm text-muted-foreground"
|
|
75
|
+
>
|
|
76
|
+
{feature.icon && (
|
|
77
|
+
<span className="text-primary">{feature.icon}</span>
|
|
78
|
+
)}
|
|
79
|
+
<span>{feature.text}</span>
|
|
80
|
+
</div>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
{/* Actions */}
|
|
86
|
+
{(primaryAction || secondaryAction) && (
|
|
87
|
+
<div className="flex flex-wrap gap-3">
|
|
88
|
+
{primaryAction && (
|
|
89
|
+
<ButtonLink
|
|
90
|
+
href={primaryAction.href || '#'}
|
|
91
|
+
onClick={primaryAction.onClick}
|
|
92
|
+
variant={primaryAction.variant || 'default'}
|
|
93
|
+
size="lg"
|
|
94
|
+
className="group"
|
|
95
|
+
>
|
|
96
|
+
{primaryAction.label}
|
|
97
|
+
{primaryAction.icon || (
|
|
98
|
+
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
|
99
|
+
)}
|
|
100
|
+
</ButtonLink>
|
|
101
|
+
)}
|
|
102
|
+
{secondaryAction && (
|
|
103
|
+
<ButtonLink
|
|
104
|
+
href={secondaryAction.href || '#'}
|
|
105
|
+
onClick={secondaryAction.onClick}
|
|
106
|
+
variant={secondaryAction.variant || 'outline'}
|
|
107
|
+
size="lg"
|
|
108
|
+
>
|
|
109
|
+
{secondaryAction.label}
|
|
110
|
+
{secondaryAction.icon}
|
|
111
|
+
</ButtonLink>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
6
|
+
import { VideoPlayer } from '../../tools/VideoPlayer';
|
|
7
|
+
import type { SplitHeroMedia as SplitHeroMediaType } from './types';
|
|
8
|
+
|
|
9
|
+
interface SplitHeroMediaProps {
|
|
10
|
+
media: SplitHeroMediaType;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const SplitHeroMedia: React.FC<SplitHeroMediaProps> = ({
|
|
15
|
+
media,
|
|
16
|
+
className,
|
|
17
|
+
}) => {
|
|
18
|
+
const containerClass = cn(
|
|
19
|
+
'relative w-full rounded-xl overflow-hidden',
|
|
20
|
+
className
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
switch (media.type) {
|
|
24
|
+
case 'image':
|
|
25
|
+
return (
|
|
26
|
+
<div className={containerClass}>
|
|
27
|
+
<div className="relative aspect-[4/3] w-full">
|
|
28
|
+
<Image
|
|
29
|
+
src={media.src}
|
|
30
|
+
alt={media.alt || ''}
|
|
31
|
+
fill
|
|
32
|
+
className="object-cover"
|
|
33
|
+
sizes="(max-width: 768px) 100vw, 50vw"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
case 'video':
|
|
40
|
+
return (
|
|
41
|
+
<div className={containerClass}>
|
|
42
|
+
<VideoPlayer
|
|
43
|
+
source={{
|
|
44
|
+
url: media.url,
|
|
45
|
+
title: media.title,
|
|
46
|
+
poster: media.poster,
|
|
47
|
+
}}
|
|
48
|
+
theme="modern"
|
|
49
|
+
aspectRatio={16 / 9}
|
|
50
|
+
autoplay={media.autoplay}
|
|
51
|
+
muted={media.muted ?? media.autoplay}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
case 'custom':
|
|
57
|
+
return (
|
|
58
|
+
<div className={containerClass}>
|
|
59
|
+
{media.content}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
default:
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { SplitHero } from './SplitHero';
|
|
2
|
+
export { SplitHeroContent } from './SplitHeroContent';
|
|
3
|
+
export { SplitHeroMedia } from './SplitHeroMedia';
|
|
4
|
+
export type {
|
|
5
|
+
SplitHeroProps,
|
|
6
|
+
SplitHeroMedia as SplitHeroMediaType,
|
|
7
|
+
SplitHeroMediaImage,
|
|
8
|
+
SplitHeroMediaVideo,
|
|
9
|
+
SplitHeroMediaCustom,
|
|
10
|
+
SplitHeroBadge,
|
|
11
|
+
SplitHeroAction,
|
|
12
|
+
SplitHeroFeature,
|
|
13
|
+
} from './types';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SplitHeroBadge {
|
|
4
|
+
icon?: ReactNode;
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SplitHeroAction {
|
|
9
|
+
label: string;
|
|
10
|
+
href?: string;
|
|
11
|
+
onClick?: () => void;
|
|
12
|
+
icon?: ReactNode;
|
|
13
|
+
variant?: 'default' | 'outline' | 'ghost' | 'secondary';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SplitHeroFeature {
|
|
17
|
+
icon?: ReactNode;
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Image media type */
|
|
22
|
+
export interface SplitHeroMediaImage {
|
|
23
|
+
type: 'image';
|
|
24
|
+
src: string;
|
|
25
|
+
alt?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Video media type - uses VideoPlayer */
|
|
29
|
+
export interface SplitHeroMediaVideo {
|
|
30
|
+
type: 'video';
|
|
31
|
+
url: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
poster?: string;
|
|
34
|
+
autoplay?: boolean;
|
|
35
|
+
muted?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Custom component media type */
|
|
39
|
+
export interface SplitHeroMediaCustom {
|
|
40
|
+
type: 'custom';
|
|
41
|
+
content: ReactNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type SplitHeroMedia = SplitHeroMediaImage | SplitHeroMediaVideo | SplitHeroMediaCustom;
|
|
45
|
+
|
|
46
|
+
export interface SplitHeroProps {
|
|
47
|
+
/** Badge above title */
|
|
48
|
+
badge?: SplitHeroBadge;
|
|
49
|
+
/** Main title */
|
|
50
|
+
title: string;
|
|
51
|
+
/** Gradient part of title (optional) */
|
|
52
|
+
titleGradient?: string;
|
|
53
|
+
/** Subtitle text */
|
|
54
|
+
subtitle?: string;
|
|
55
|
+
/** Feature list */
|
|
56
|
+
features?: SplitHeroFeature[];
|
|
57
|
+
/** Primary CTA button */
|
|
58
|
+
primaryAction?: SplitHeroAction;
|
|
59
|
+
/** Secondary CTA button */
|
|
60
|
+
secondaryAction?: SplitHeroAction;
|
|
61
|
+
/** Media content - image, video, or custom component */
|
|
62
|
+
media?: SplitHeroMedia;
|
|
63
|
+
/** Layout direction */
|
|
64
|
+
layout?: 'text-left' | 'text-right';
|
|
65
|
+
/** Mobile stack order */
|
|
66
|
+
mobileOrder?: 'media-first' | 'text-first';
|
|
67
|
+
/** Content vertical alignment */
|
|
68
|
+
align?: 'top' | 'center' | 'bottom';
|
|
69
|
+
/** Container max width */
|
|
70
|
+
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
71
|
+
/** Background style */
|
|
72
|
+
background?: 'none' | 'muted' | 'gradient';
|
|
73
|
+
/** Custom className */
|
|
74
|
+
className?: string;
|
|
75
|
+
}
|
package/src/blocks/index.ts
CHANGED