@olwiba/ui 0.1.1 → 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.1",
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;
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { LayoutGrid, Sparkles, Layers } from 'lucide-react';
5
+ import { Button } from '@olwiba/cn';
6
+ import { useOlwibaUI, type UIMode } from '../context/OlwibaUIContext';
7
+
8
+ const MODES: UIMode[] = ['default', 'playful', 'smooth'];
9
+
10
+ const MODE_ICONS: Record<UIMode, React.ReactNode> = {
11
+ default: <LayoutGrid className="size-4" />,
12
+ playful: <Sparkles className="size-4" />,
13
+ smooth: <Layers className="size-4" />,
14
+ };
15
+
16
+ const MODE_LABELS: Record<UIMode, string> = {
17
+ default: 'Default mode',
18
+ playful: 'Playful mode',
19
+ smooth: 'Smooth mode',
20
+ };
21
+
22
+ export function ModeSwitchMinimal() {
23
+ const { mode, setMode } = useOlwibaUI();
24
+
25
+ const cycle = () => {
26
+ const idx = MODES.indexOf(mode);
27
+ setMode(MODES[(idx + 1) % MODES.length]);
28
+ };
29
+
30
+ return (
31
+ <Button variant="ghost" size="icon" onClick={cycle} className="size-8" aria-label={MODE_LABELS[mode]}>
32
+ <span
33
+ key={mode}
34
+ className="inline-flex size-4 items-center justify-center animate-in fade-in zoom-in-75 duration-200 motion-reduce:animate-none"
35
+ >
36
+ {MODE_ICONS[mode]}
37
+ </span>
38
+ </Button>
39
+ );
40
+ }
@@ -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
  }
@@ -8,11 +8,13 @@ export type UIMode = 'default' | 'playful' | 'smooth';
8
8
  interface OlwibaUIContextValue {
9
9
  isMobile: boolean;
10
10
  mode: UIMode;
11
+ setMode: (mode: UIMode) => void;
11
12
  }
12
13
 
13
14
  const OlwibaUIContext = React.createContext<OlwibaUIContextValue>({
14
15
  isMobile: false,
15
16
  mode: 'default',
17
+ setMode: () => {},
16
18
  });
17
19
 
18
20
  export interface OlwibaUIProviderProps {
@@ -33,14 +35,15 @@ export interface OlwibaUIProviderProps {
33
35
  mode?: UIMode;
34
36
  }
35
37
 
36
- export function OlwibaUIProvider({ children, isMobile: isMobileProp, mode = 'default' }: OlwibaUIProviderProps) {
38
+ export function OlwibaUIProvider({ children, isMobile: isMobileProp, mode: initialMode = 'default' }: OlwibaUIProviderProps) {
37
39
  const detectedMobile = useIsMobile();
38
40
  const isMobile = isMobileProp ?? detectedMobile;
41
+ const [mode, setMode] = React.useState<UIMode>(initialMode);
39
42
 
40
43
  const variant = mode !== 'default' ? (mode as UIVariant) : undefined;
41
44
 
42
45
  return (
43
- <OlwibaUIContext.Provider value={{ isMobile, mode }}>
46
+ <OlwibaUIContext.Provider value={{ isMobile, mode, setMode }}>
44
47
  <UIVariantProvider mode={variant}>
45
48
  {children}
46
49
  </UIVariantProvider>
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';
@@ -101,6 +102,7 @@ export { FullPageSpinner } from './components/Spinner';
101
102
  export { PageHeader, type PageHeaderProps, type PageHeaderBreadcrumb, type PageHeaderBackButton } from './components/PageHeader';
102
103
  export { Suspensed } from './components/Suspensed';
103
104
  export { ThemeSwitchMinimal } from './components/ThemeSwitchMinimal';
105
+ export { ModeSwitchMinimal } from './components/ModeSwitchMinimal';
104
106
  export { ThemeColorUpdater } from './components/ThemeColorUpdater';
105
107
  export { VersionBanner } from './components/VersionBanner';
106
108
  export { DevBanner, type DevBannerProps } from './components/DevBanner';
@@ -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} />
@@ -4,7 +4,6 @@ import * as React from 'react';
4
4
  import { Menu, X } from 'lucide-react';
5
5
  import { Button, Separator, Sheet, SheetContent, SheetTrigger } from '@olwiba/cn';
6
6
  import type { AppShellRenderLink } from '../app/AppShell';
7
- import { ThemeSwitchMinimal } from '../components/ThemeSwitchMinimal';
8
7
 
9
8
  export interface NavbarProps {
10
9
  brand: { name: string; logo?: React.ReactNode; href?: string };
@@ -13,7 +12,8 @@ export interface NavbarProps {
13
12
  primary?: { label: string; href: string };
14
13
  secondary?: { label: string; href: string };
15
14
  };
16
- showThemeToggle?: boolean;
15
+ /** Icon-button controls rendered right-of-nav, left-of-CTA. Pass e.g. ModeSwitchMinimal, ThemeSwitchMinimal. */
16
+ controls?: React.ReactNode[];
17
17
  renderLink?: AppShellRenderLink;
18
18
  }
19
19
 
@@ -25,11 +25,12 @@ export function Navbar({
25
25
  brand,
26
26
  navLinks,
27
27
  cta,
28
- showThemeToggle = false,
28
+ controls,
29
29
  renderLink = defaultRenderLink,
30
30
  }: NavbarProps) {
31
31
  const [open, setOpen] = React.useState(false);
32
32
  const brandHref = brand.href ?? '/';
33
+ const hasRightContent = controls?.length || cta?.primary || cta?.secondary;
33
34
 
34
35
  return (
35
36
  <section className="overflow-hidden rounded-2xl border bg-card">
@@ -49,8 +50,8 @@ export function Navbar({
49
50
  ),
50
51
  })}
51
52
 
52
- {/* Desktop nav links — center column, truly centered */}
53
- <div className="hidden items-center gap-1 md:flex">
53
+ {/* Desktop nav links — center column when right content exists, else shift right */}
54
+ <div className={`hidden items-center gap-1 md:flex ${!hasRightContent ? 'col-span-2 justify-end' : ''}`}>
54
55
  {navLinks.map((link) => (
55
56
  <span key={link.label}>
56
57
  {renderLink({
@@ -62,20 +63,27 @@ export function Navbar({
62
63
  ))}
63
64
  </div>
64
65
 
65
- {/* Desktop CTAsright column, right-aligned */}
66
- <div className="hidden items-center justify-end gap-3 md:flex">
67
- {cta?.secondary &&
68
- renderLink({
69
- href: cta.secondary.href,
70
- children: <Button variant="ghost" size="sm">{cta.secondary.label}</Button>,
71
- })}
72
- {cta?.primary &&
73
- renderLink({
74
- href: cta.primary.href,
75
- children: <Button size="sm">{cta.primary.label}</Button>,
76
- })}
77
- {showThemeToggle && <ThemeSwitchMinimal />}
78
- </div>
66
+ {/* Desktop rightcontrols + CTA */}
67
+ {hasRightContent && (
68
+ <div className="hidden items-center justify-end gap-2 md:flex">
69
+ {controls?.map((control, i) => (
70
+ <React.Fragment key={i}>{control}</React.Fragment>
71
+ ))}
72
+ {(cta?.secondary || cta?.primary) && controls?.length ? (
73
+ <div className="mx-1 h-4 w-px bg-border" />
74
+ ) : null}
75
+ {cta?.secondary &&
76
+ renderLink({
77
+ href: cta.secondary.href,
78
+ children: <Button variant="ghost" size="sm">{cta.secondary.label}</Button>,
79
+ })}
80
+ {cta?.primary &&
81
+ renderLink({
82
+ href: cta.primary.href,
83
+ children: <Button size="sm">{cta.primary.label}</Button>,
84
+ })}
85
+ </div>
86
+ )}
79
87
 
80
88
  {/* Mobile drawer — right column on small screens */}
81
89
  <Sheet open={open} onOpenChange={setOpen}>
@@ -103,7 +111,9 @@ export function Navbar({
103
111
  })}
104
112
  </span>
105
113
  <div className="flex items-center gap-1">
106
- {showThemeToggle && <ThemeSwitchMinimal />}
114
+ {controls?.map((control, i) => (
115
+ <React.Fragment key={i}>{control}</React.Fragment>
116
+ ))}
107
117
  <Button variant="ghost" size="icon" onClick={() => setOpen(false)}>
108
118
  <X className="size-4" />
109
119
  <span className="sr-only">Close menu</span>
@@ -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