@olwiba/ui 0.1.15 → 0.2.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.
@@ -1,126 +0,0 @@
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,95 +0,0 @@
1
- 'use client';
2
-
3
- import * as React from 'react';
4
- import { ArrowRight, Rocket } from 'lucide-react';
5
- import { cn } from '@olwiba/cn';
6
- import type { AppShellRenderLink } from '../app/AppShell';
7
-
8
- export interface CtaCardSectionProps {
9
- heading: string;
10
- description?: string;
11
- primaryCta: { label: string; href: string };
12
- footnote?: string;
13
- icon?: React.ReactNode;
14
- renderLink?: AppShellRenderLink;
15
- }
16
-
17
- const defaultRenderLink: AppShellRenderLink = ({ href, children, className }) => (
18
- <a href={href} className={className}>{children}</a>
19
- );
20
-
21
- export function CtaCardSection({
22
- heading,
23
- description,
24
- primaryCta,
25
- footnote,
26
- icon,
27
- renderLink = defaultRenderLink,
28
- }: CtaCardSectionProps) {
29
- const ref = React.useRef<HTMLElement>(null);
30
- const [visible, setVisible] = React.useState(false);
31
-
32
- React.useEffect(() => {
33
- const el = ref.current;
34
- if (!el) return;
35
- const observer = new IntersectionObserver(
36
- ([entry]) => {
37
- if (entry.isIntersecting) {
38
- setVisible(true);
39
- observer.disconnect();
40
- }
41
- },
42
- { threshold: 0.15 },
43
- );
44
- observer.observe(el);
45
- return () => observer.disconnect();
46
- }, []);
47
-
48
- return (
49
- <section
50
- ref={ref}
51
- className={cn(
52
- 'relative overflow-hidden rounded-3xl border border-gray-200 shadow-sm dark:border-border',
53
- 'bg-[#E0DFDB] dark:bg-card',
54
- 'transition-[opacity,transform] duration-700 ease-out',
55
- visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-6',
56
- )}
57
- >
58
- {/* Decorative icon */}
59
- <div
60
- className="pointer-events-none absolute inset-0 flex items-center justify-center"
61
- style={{
62
- opacity: visible ? 0.12 : 0,
63
- transform: visible ? 'scale(1) rotate(0deg)' : 'scale(0.9) rotate(-5deg)',
64
- transition: 'opacity 700ms ease 200ms, transform 700ms ease 200ms',
65
- }}
66
- >
67
- {icon ?? <Rocket className="size-72 text-foreground dark:text-foreground" strokeWidth={0.75} />}
68
- </div>
69
-
70
- {/* Content */}
71
- <div className="relative z-10 px-6 py-20 text-center sm:px-10 sm:py-28">
72
- <h2 className="text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
73
- {heading}
74
- </h2>
75
- {description && (
76
- <p className="mx-auto mt-4 max-w-lg text-pretty text-muted-foreground">{description}</p>
77
- )}
78
- <div className="mt-8">
79
- {renderLink({
80
- href: primaryCta.href,
81
- children: (
82
- <span className="inline-flex items-center gap-2 rounded-full bg-foreground px-6 py-3 text-sm font-medium text-background transition-opacity hover:opacity-80">
83
- {primaryCta.label}
84
- <ArrowRight className="size-4" />
85
- </span>
86
- ),
87
- })}
88
- </div>
89
- {footnote && (
90
- <p className="mt-5 text-sm text-muted-foreground">{footnote}</p>
91
- )}
92
- </div>
93
- </section>
94
- );
95
- }