@olwiba/ui 0.1.2 → 0.1.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olwiba/ui",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -3,7 +3,7 @@
3
3
  import * as React from 'react';
4
4
  import type { LucideIcon } from 'lucide-react';
5
5
  import { ArrowRight } from 'lucide-react';
6
- import { cn } from '@olwiba/cn';
6
+ import { cn, useUIVariant } from '@olwiba/cn';
7
7
 
8
8
  export interface FeatureCardProps extends React.HTMLAttributes<HTMLDivElement> {
9
9
  icon: LucideIcon;
@@ -13,16 +13,28 @@ export interface FeatureCardProps extends React.HTMLAttributes<HTMLDivElement> {
13
13
  }
14
14
 
15
15
  export function FeatureCard({ icon: Icon, title, description, href, className, ...props }: FeatureCardProps) {
16
- const content = (
16
+ const mode = useUIVariant();
17
+
18
+ const cardInner = (
17
19
  <div
18
20
  className={cn(
19
- 'group relative flex flex-col gap-4 rounded-2xl border bg-card p-6 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md',
21
+ 'group relative flex h-full flex-col gap-4 border bg-card p-6 transition-all duration-200',
22
+ mode === 'playful'
23
+ ? 'rounded-2xl rotate-[0.3deg]'
24
+ : mode === 'smooth'
25
+ ? 'rounded-3xl hover:-translate-y-0.5 hover:shadow-md'
26
+ : 'rounded-2xl hover:-translate-y-0.5 hover:shadow-md',
20
27
  href && 'cursor-pointer',
21
- className,
28
+ mode !== 'playful' && className,
22
29
  )}
23
- {...props}
30
+ {...(mode !== 'playful' ? props : {})}
24
31
  >
25
- <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10 text-primary transition-colors group-hover:bg-primary group-hover:text-primary-foreground">
32
+ <div
33
+ className={cn(
34
+ 'flex h-10 w-10 items-center justify-center bg-primary/10 text-primary transition-colors group-hover:bg-primary group-hover:text-primary-foreground',
35
+ mode === 'smooth' ? 'rounded-2xl' : 'rounded-xl',
36
+ )}
37
+ >
26
38
  <Icon className="size-5" />
27
39
  </div>
28
40
  <div className="space-y-1.5">
@@ -37,8 +49,21 @@ export function FeatureCard({ icon: Icon, title, description, href, className, .
37
49
  </div>
38
50
  );
39
51
 
52
+ const content =
53
+ mode === 'playful' ? (
54
+ <div className={cn('group/playful relative h-full', className)} {...props}>
55
+ <div
56
+ aria-hidden="true"
57
+ className="absolute inset-0 rounded-2xl bg-border transition-transform duration-200 translate-x-[5px] translate-y-[5px] -rotate-[0.5deg] group-hover/playful:-rotate-[1.5deg] group-hover/playful:translate-x-[6px] group-hover/playful:translate-y-[6px]"
58
+ />
59
+ {cardInner}
60
+ </div>
61
+ ) : (
62
+ cardInner
63
+ );
64
+
40
65
  if (href) {
41
- return <a href={href} className="block">{content}</a>;
66
+ return <a href={href} className="block h-full">{content}</a>;
42
67
  }
43
68
 
44
69
  return content;
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from 'react';
4
4
  import { Check, Minus } from 'lucide-react';
5
- import { Badge, Button, cn, Separator } from '@olwiba/cn';
5
+ import { Badge, Button, cn, Separator, useUIVariant } from '@olwiba/cn';
6
6
 
7
7
  export interface PricingFeature {
8
8
  label: string;
@@ -34,14 +34,21 @@ export function PricingCard({
34
34
  className,
35
35
  ...props
36
36
  }: PricingCardProps) {
37
- return (
37
+ const mode = useUIVariant();
38
+
39
+ const cardInner = (
38
40
  <div
39
41
  className={cn(
40
- 'relative flex flex-col rounded-2xl border p-6',
42
+ 'relative flex flex-col border p-6',
43
+ mode === 'playful'
44
+ ? 'rounded-2xl rotate-[0.3deg]'
45
+ : mode === 'smooth'
46
+ ? 'rounded-3xl'
47
+ : 'rounded-2xl',
41
48
  highlighted ? 'border-primary bg-primary/5 shadow-sm' : 'bg-card',
42
- className,
49
+ mode !== 'playful' && className,
43
50
  )}
44
- {...props}
51
+ {...(mode !== 'playful' ? props : {})}
45
52
  >
46
53
  {badge && (
47
54
  <div className="absolute -top-3 left-1/2 -translate-x-1/2">
@@ -87,4 +94,21 @@ export function PricingCard({
87
94
  </ul>
88
95
  </div>
89
96
  );
97
+
98
+ if (mode === 'playful') {
99
+ return (
100
+ <div className={cn('group/playful relative', className)} {...props}>
101
+ <div
102
+ aria-hidden="true"
103
+ className={cn(
104
+ 'absolute inset-0 rounded-2xl transition-transform duration-200 translate-x-[5px] translate-y-[5px] -rotate-[0.5deg] group-hover/playful:-rotate-[1.5deg] group-hover/playful:translate-x-[6px] group-hover/playful:translate-y-[6px]',
105
+ highlighted ? 'bg-primary/20' : 'bg-border',
106
+ )}
107
+ />
108
+ {cardInner}
109
+ </div>
110
+ );
111
+ }
112
+
113
+ return cardInner;
90
114
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from 'react';
4
4
  import { Quote, Star } from 'lucide-react';
5
- import { Avatar, AvatarFallback, AvatarImage, cn } from '@olwiba/cn';
5
+ import { Avatar, AvatarFallback, AvatarImage, cn, useUIVariant } from '@olwiba/cn';
6
6
 
7
7
  export interface TestimonialCardProps extends React.HTMLAttributes<HTMLDivElement> {
8
8
  quote: string;
@@ -25,10 +25,20 @@ export function TestimonialCard({
25
25
  className,
26
26
  ...props
27
27
  }: TestimonialCardProps) {
28
- return (
28
+ const mode = useUIVariant();
29
+
30
+ const cardInner = (
29
31
  <div
30
- className={cn('flex flex-col gap-4 rounded-2xl border bg-card p-6', className)}
31
- {...props}
32
+ className={cn(
33
+ 'flex flex-col gap-4 border bg-card p-6',
34
+ mode === 'playful'
35
+ ? 'rounded-2xl rotate-[0.3deg]'
36
+ : mode === 'smooth'
37
+ ? 'rounded-3xl'
38
+ : 'rounded-2xl',
39
+ mode !== 'playful' && className,
40
+ )}
41
+ {...(mode !== 'playful' ? props : {})}
32
42
  >
33
43
  <Quote className="size-4 text-muted-foreground/40" />
34
44
  {rating != null && (
@@ -48,9 +58,9 @@ export function TestimonialCard({
48
58
  )}
49
59
  <p className="flex-1 text-sm leading-relaxed">{quote}</p>
50
60
  <div className="flex items-center gap-3">
51
- <Avatar className="size-9 rounded-xl">
61
+ <Avatar className="size-9">
52
62
  <AvatarImage src={avatar} alt={name} />
53
- <AvatarFallback className="rounded-xl text-xs">{initials ?? name.slice(0, 2).toUpperCase()}</AvatarFallback>
63
+ <AvatarFallback className="text-xs">{initials ?? name.slice(0, 2).toUpperCase()}</AvatarFallback>
54
64
  </Avatar>
55
65
  <div>
56
66
  <div className="text-sm font-medium leading-tight">{name}</div>
@@ -61,4 +71,18 @@ export function TestimonialCard({
61
71
  </div>
62
72
  </div>
63
73
  );
74
+
75
+ if (mode === 'playful') {
76
+ return (
77
+ <div className={cn('group/playful relative', className)} {...props}>
78
+ <div
79
+ aria-hidden="true"
80
+ className="absolute inset-0 rounded-2xl bg-border transition-transform duration-200 translate-x-[5px] translate-y-[5px] -rotate-[0.5deg] group-hover/playful:-rotate-[1.5deg] group-hover/playful:translate-x-[6px] group-hover/playful:translate-y-[6px]"
81
+ />
82
+ {cardInner}
83
+ </div>
84
+ );
85
+ }
86
+
87
+ return cardInner;
64
88
  }
package/src/index.ts CHANGED
@@ -54,6 +54,7 @@ export {
54
54
  export { SectionTitle, type SectionTitleProps } from './marketing/SectionTitle';
55
55
  export { HeroSection, type HeroSectionProps } from './marketing/HeroSection';
56
56
  export { FeaturesSection, type FeaturesSectionProps } from './marketing/FeaturesSection';
57
+ export { CarouselSection, type CarouselSectionProps } from './marketing/CarouselSection';
57
58
  export { CtaSection, type CtaSectionProps } from './marketing/CtaSection';
58
59
  export { PricingSection, type PricingSectionProps, type PricingPlan } from './marketing/PricingSection';
59
60
  export { TestimonialsSection, type TestimonialsSectionProps } from './marketing/TestimonialsSection';
@@ -0,0 +1,126 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import type { LucideIcon } from 'lucide-react';
5
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
6
+ import { cn, useUIVariant } from '@olwiba/cn';
7
+ import { FeatureCard } from '../components/FeatureCard';
8
+ import { SectionTitle } from './SectionTitle';
9
+ import { FadeIn } from '../motion/FadeIn';
10
+
11
+ export interface CarouselSectionProps {
12
+ title?: string;
13
+ description?: string;
14
+ badge?: string;
15
+ features: Array<{ icon: LucideIcon; title: string; description: string; href?: string }>;
16
+ }
17
+
18
+ /**
19
+ * Horizontally scrolling card carousel for feature-style content.
20
+ * Scroll-snap based with prev/next controls — no carousel dependency.
21
+ */
22
+ export function CarouselSection({
23
+ title = 'Explore the platform',
24
+ description,
25
+ badge = 'Features',
26
+ features,
27
+ }: CarouselSectionProps) {
28
+ const mode = useUIVariant();
29
+ const trackRef = React.useRef<HTMLDivElement>(null);
30
+ const [canPrev, setCanPrev] = React.useState(false);
31
+ const [canNext, setCanNext] = React.useState(true);
32
+
33
+ const updateScrollState = React.useCallback(() => {
34
+ const track = trackRef.current;
35
+ if (!track) return;
36
+ setCanPrev(track.scrollLeft > 8);
37
+ setCanNext(track.scrollLeft < track.scrollWidth - track.clientWidth - 8);
38
+ }, []);
39
+
40
+ React.useEffect(() => {
41
+ const track = trackRef.current;
42
+ if (!track) return;
43
+ updateScrollState();
44
+ track.addEventListener('scroll', updateScrollState, { passive: true });
45
+ const observer = new ResizeObserver(updateScrollState);
46
+ observer.observe(track);
47
+ return () => {
48
+ track.removeEventListener('scroll', updateScrollState);
49
+ observer.disconnect();
50
+ };
51
+ }, [updateScrollState]);
52
+
53
+ const scrollByCard = (direction: 1 | -1) => {
54
+ const track = trackRef.current;
55
+ if (!track) return;
56
+ const card = track.querySelector<HTMLElement>('[data-carousel-item]');
57
+ const step = card ? card.offsetWidth + 24 : track.clientWidth * 0.8;
58
+ track.scrollBy({ left: direction * step, behavior: 'smooth' });
59
+ };
60
+
61
+ const sectionClasses = cn(
62
+ 'overflow-hidden bg-card',
63
+ mode === 'smooth' && 'rounded-3xl border',
64
+ mode === 'playful' && 'rounded-2xl border-primary/25 border',
65
+ !mode && 'rounded-2xl border',
66
+ );
67
+
68
+ const controlClasses = cn(
69
+ 'flex size-9 items-center justify-center border bg-background text-foreground transition-colors',
70
+ 'hover:bg-muted disabled:opacity-40 disabled:hover:bg-background',
71
+ mode === 'smooth' ? 'rounded-2xl' : 'rounded-xl',
72
+ );
73
+
74
+ return (
75
+ <section className={sectionClasses}>
76
+ <div className="px-6 py-14 sm:px-10 sm:py-20">
77
+ <div className="mx-auto max-w-5xl">
78
+ <SectionTitle title={title} description={description} badge={badge} />
79
+
80
+ <FadeIn direction="up">
81
+ <div className="mt-10 flex items-center justify-end gap-2">
82
+ <button
83
+ type="button"
84
+ aria-label="Previous"
85
+ className={controlClasses}
86
+ onClick={() => scrollByCard(-1)}
87
+ disabled={!canPrev}
88
+ >
89
+ <ChevronLeft className="size-4" />
90
+ </button>
91
+ <button
92
+ type="button"
93
+ aria-label="Next"
94
+ className={controlClasses}
95
+ onClick={() => scrollByCard(1)}
96
+ disabled={!canNext}
97
+ >
98
+ <ChevronRight className="size-4" />
99
+ </button>
100
+ </div>
101
+
102
+ <div
103
+ ref={trackRef}
104
+ className="mt-4 flex snap-x snap-mandatory gap-6 overflow-x-auto pb-4 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
105
+ >
106
+ {features.map((feature) => (
107
+ <div
108
+ key={feature.title}
109
+ data-carousel-item
110
+ className="w-[280px] shrink-0 snap-start sm:w-[320px]"
111
+ >
112
+ <FeatureCard
113
+ icon={feature.icon}
114
+ title={feature.title}
115
+ description={feature.description}
116
+ href={feature.href}
117
+ />
118
+ </div>
119
+ ))}
120
+ </div>
121
+ </FadeIn>
122
+ </div>
123
+ </div>
124
+ </section>
125
+ );
126
+ }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { ArrowRight, Sparkles } from 'lucide-react';
4
- import { Button } from '@olwiba/cn';
4
+ import { Button, cn, useUIVariant } from '@olwiba/cn';
5
5
  import { FadeIn } from '../motion/FadeIn';
6
6
  import type { AppShellRenderLink } from '../app/AppShell';
7
7
 
@@ -26,13 +26,20 @@ export function CtaSection({
26
26
  footnote,
27
27
  renderLink = defaultRenderLink,
28
28
  }: CtaSectionProps) {
29
+ const mode = useUIVariant()
30
+ const sectionClasses = cn(
31
+ 'overflow-hidden bg-card',
32
+ mode === 'smooth' && 'rounded-3xl border',
33
+ mode === 'playful' && 'rounded-2xl border-primary/25 border',
34
+ !mode && 'rounded-2xl border',
35
+ )
29
36
  return (
30
- <section className="overflow-hidden rounded-2xl border bg-card">
37
+ <section className={sectionClasses}>
31
38
  <div className="relative px-6 py-16 sm:px-10 sm:py-24">
32
39
  <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,hsl(var(--primary)/0.15),transparent_70%)]" />
33
40
  <FadeIn direction="up">
34
41
  <div className="relative mx-auto max-w-2xl text-center">
35
- <div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-2xl bg-primary/10 text-primary">
42
+ <div className={cn('mb-4 inline-flex h-10 w-10 items-center justify-center bg-primary/10 text-primary', mode === 'smooth' ? 'rounded-3xl' : 'rounded-2xl')}>
36
43
  <Sparkles className="size-5" />
37
44
  </div>
38
45
  <h2 className="text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@olwiba/cn';
3
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, cn, useUIVariant } from '@olwiba/cn';
4
4
  import { SectionTitle } from './SectionTitle';
5
5
  import { FadeIn } from '../motion/FadeIn';
6
6
 
@@ -17,8 +17,15 @@ export function FaqSection({
17
17
  badge = 'FAQ',
18
18
  items,
19
19
  }: FaqSectionProps) {
20
+ const mode = useUIVariant()
21
+ const sectionClasses = cn(
22
+ 'overflow-hidden bg-card',
23
+ mode === 'smooth' && 'rounded-3xl border',
24
+ mode === 'playful' && 'rounded-2xl border-primary/25 border',
25
+ !mode && 'rounded-2xl border',
26
+ )
20
27
  return (
21
- <section className="overflow-hidden rounded-2xl border bg-card">
28
+ <section className={sectionClasses}>
22
29
  <div className="px-6 py-14 sm:px-10 sm:py-20">
23
30
  <div className="mx-auto max-w-3xl">
24
31
  <SectionTitle title={title} description={description} badge={badge} />
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import type { LucideIcon } from 'lucide-react';
4
+ import { cn, useUIVariant } from '@olwiba/cn';
4
5
  import { FeatureCard } from '../components/FeatureCard';
5
6
  import { SectionTitle } from './SectionTitle';
6
7
  import { StaggerChildren } from '../motion/StaggerChildren';
@@ -18,8 +19,15 @@ export function FeaturesSection({
18
19
  badge = 'Features',
19
20
  features,
20
21
  }: FeaturesSectionProps) {
22
+ const mode = useUIVariant()
23
+ const sectionClasses = cn(
24
+ 'overflow-hidden bg-card',
25
+ mode === 'smooth' && 'rounded-3xl border',
26
+ mode === 'playful' && 'rounded-2xl border-primary/25 border',
27
+ !mode && 'rounded-2xl border',
28
+ )
21
29
  return (
22
- <section className="overflow-hidden rounded-2xl border bg-card">
30
+ <section className={sectionClasses}>
23
31
  <div className="px-6 py-14 sm:px-10 sm:py-20">
24
32
  <div className="mx-auto max-w-4xl">
25
33
  <SectionTitle title={title} description={description} badge={badge} />
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import * as React from 'react';
4
- import { Badge, cn } from '@olwiba/cn';
4
+ import { Badge, Button, cn, useUIVariant } from '@olwiba/cn';
5
5
  import { PricingCard, type PricingFeature } from '../components/PricingCard';
6
6
  import { StaggerChildren } from '../motion/StaggerChildren';
7
7
  import type { AppShellRenderLink } from '../app/AppShell';
@@ -42,9 +42,16 @@ export function PricingSection({
42
42
  footnote,
43
43
  }: PricingSectionProps) {
44
44
  const [annual, setAnnual] = React.useState(false);
45
+ const mode = useUIVariant()
46
+ const sectionClasses = cn(
47
+ 'overflow-hidden bg-card',
48
+ mode === 'smooth' && 'rounded-3xl border',
49
+ mode === 'playful' && 'rounded-2xl border-primary/25 border',
50
+ !mode && 'rounded-2xl border',
51
+ )
45
52
 
46
53
  return (
47
- <section className="overflow-hidden rounded-2xl border bg-card">
54
+ <section className={sectionClasses}>
48
55
  <div className="px-6 py-14 sm:px-10 sm:py-20">
49
56
  <div className="mx-auto max-w-5xl">
50
57
  {/* Header */}
@@ -63,7 +70,9 @@ export function PricingSection({
63
70
 
64
71
  {/* Billing toggle */}
65
72
  <div className="mt-6 inline-flex items-center gap-3 rounded-full border bg-muted p-1">
66
- <button
73
+ <Button
74
+ variant="ghost"
75
+ size="sm"
67
76
  onClick={() => setAnnual(false)}
68
77
  className={cn(
69
78
  'rounded-full px-4 py-1.5 text-sm font-medium transition-colors',
@@ -71,8 +80,10 @@ export function PricingSection({
71
80
  )}
72
81
  >
73
82
  Monthly
74
- </button>
75
- <button
83
+ </Button>
84
+ <Button
85
+ variant="ghost"
86
+ size="sm"
76
87
  onClick={() => setAnnual(true)}
77
88
  className={cn(
78
89
  'flex items-center gap-2 rounded-full px-4 py-1.5 text-sm font-medium transition-colors',
@@ -83,7 +94,7 @@ export function PricingSection({
83
94
  {saveBadge && (
84
95
  <Badge variant="secondary" className="text-xs">{saveBadge}</Badge>
85
96
  )}
86
- </button>
97
+ </Button>
87
98
  </div>
88
99
  </div>
89
100
 
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Separator } from '@olwiba/cn';
3
+ import { Separator, cn, useUIVariant } from '@olwiba/cn';
4
4
  import { SectionTitle } from './SectionTitle';
5
5
  import { StaggerChildren } from '../motion/StaggerChildren';
6
6
  import { CountUp } from '../motion/CountUp';
@@ -33,8 +33,15 @@ export function StatsSection({
33
33
  badge = 'By the numbers',
34
34
  stats,
35
35
  }: StatsSectionProps) {
36
+ const mode = useUIVariant()
37
+ const sectionClasses = cn(
38
+ 'overflow-hidden bg-card',
39
+ mode === 'smooth' && 'rounded-3xl border',
40
+ mode === 'playful' && 'rounded-2xl border-primary/25 border',
41
+ !mode && 'rounded-2xl border',
42
+ )
36
43
  return (
37
- <section className="overflow-hidden rounded-2xl border bg-card">
44
+ <section className={sectionClasses}>
38
45
  <div className="relative px-6 py-14 sm:px-10 sm:py-20">
39
46
  <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom,hsl(var(--primary)/0.08),transparent_60%)]" />
40
47
  <div className="relative mx-auto max-w-4xl">
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { cn, useUIVariant } from '@olwiba/cn';
3
4
  import { TestimonialCard } from '../components/TestimonialCard';
4
5
  import { SectionTitle } from './SectionTitle';
5
6
  import { StaggerChildren } from '../motion/StaggerChildren';
@@ -25,8 +26,15 @@ export function TestimonialsSection({
25
26
  badge = 'Testimonials',
26
27
  testimonials,
27
28
  }: TestimonialsSectionProps) {
29
+ const mode = useUIVariant()
30
+ const sectionClasses = cn(
31
+ 'overflow-hidden bg-card',
32
+ mode === 'smooth' && 'rounded-3xl border',
33
+ mode === 'playful' && 'rounded-2xl border-primary/25 border',
34
+ !mode && 'rounded-2xl border',
35
+ )
28
36
  return (
29
- <section className="overflow-hidden rounded-2xl border bg-card">
37
+ <section className={sectionClasses}>
30
38
  <div className="px-6 py-14 sm:px-10 sm:py-20">
31
39
  <div className="mx-auto max-w-5xl">
32
40
  <SectionTitle title={title} description={description} badge={badge} />
@@ -59,6 +59,7 @@ export function StaggerChildren({
59
59
  <div ref={ref} className={cn(className)} {...props}>
60
60
  {React.Children.map(children, (child, i) => (
61
61
  <div
62
+ className="h-full"
62
63
  style={{
63
64
  transition: `opacity ${duration}ms ease, transform ${duration}ms ease`,
64
65
  transitionDelay: `${delay + i * stagger}ms`,