@olwiba/ui 0.2.19 → 0.2.21

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,7 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import * as React from 'react';
4
- import { cn, useUIVariant } from '@olwiba/cn';
4
+ import { cn } from '@olwiba/cn';
5
+ import { useSectionSurface, type MarketingSurface } from './section-surface';
5
6
  import { SectionTitle } from './SectionTitle';
6
7
  import { FadeIn } from '../motion/FadeIn';
7
8
 
@@ -26,6 +27,8 @@ export interface StepsSectionProps {
26
27
  variant?: 'default' | 'timeline';
27
28
  /** Timeline only — step groups with optional interstitial content; overrides `steps`. Numbering continues across groups. */
28
29
  groups?: StepGroup[];
30
+ /** How the section sits on the page. @default 'card' */
31
+ surface?: MarketingSurface;
29
32
  }
30
33
 
31
34
  function TimelineGroup({
@@ -88,14 +91,9 @@ export function StepsSection({
88
91
  steps,
89
92
  variant = 'default',
90
93
  groups,
94
+ surface,
91
95
  }: StepsSectionProps) {
92
- const mode = useUIVariant();
93
- const sectionClasses = cn(
94
- 'overflow-hidden bg-card',
95
- mode === 'smooth' && 'rounded-3xl border',
96
- mode === 'playful' && 'rounded-2xl border-primary/25 border',
97
- !mode && 'rounded-2xl border',
98
- );
96
+ const sectionClasses = useSectionSurface(surface);
99
97
 
100
98
  if (variant === 'timeline') {
101
99
  const resolvedGroups: StepGroup[] = groups ?? (steps ? [{ steps }] : []);
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import { Github, Linkedin, Twitter } from 'lucide-react';
4
- import { Avatar, AvatarFallback, AvatarImage, cn, useUIVariant } from '@olwiba/cn';
4
+ import { Avatar, AvatarFallback, AvatarImage } from '@olwiba/cn';
5
+ import { useSectionSurface, type MarketingSurface } from './section-surface';
5
6
  import { Badge } from '../primitives/Badge';
6
7
 
7
8
  export interface TeamMember {
@@ -49,6 +50,8 @@ export interface TeamSectionProps {
49
50
  badge?: string;
50
51
  title?: string;
51
52
  description?: string;
53
+ /** How the section sits on the page. @default 'card' */
54
+ surface?: MarketingSurface;
52
55
  }
53
56
 
54
57
  export function TeamSection({
@@ -56,14 +59,9 @@ export function TeamSection({
56
59
  badge = 'Team',
57
60
  title = 'The people behind Olwiba',
58
61
  description = 'A small team with deep experience in design systems, open source, and developer tooling.',
62
+ surface,
59
63
  }: TeamSectionProps) {
60
- const mode = useUIVariant();
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
- );
64
+ const sectionClasses = useSectionSurface(surface);
67
65
  return (
68
66
  <section className={sectionClasses}>
69
67
  <div className="px-6 py-14 sm:px-10 sm:py-20">
@@ -4,6 +4,7 @@ import type { LucideIcon } from 'lucide-react';
4
4
  import { cn, useUIVariant } from '@olwiba/cn';
5
5
  import { SectionTitle } from './SectionTitle';
6
6
  import { StaggerChildren } from '../motion/StaggerChildren';
7
+ import { useSectionSurface, type MarketingSurface } from './section-surface';
7
8
 
8
9
  export interface TechStackItem {
9
10
  icon: LucideIcon;
@@ -17,6 +18,8 @@ export interface TechStackSectionProps {
17
18
  title?: string;
18
19
  description?: string;
19
20
  items: TechStackItem[];
21
+ /** How the section sits on the page. @default 'card' */
22
+ surface?: MarketingSurface;
20
23
  }
21
24
 
22
25
  export function TechStackSection({
@@ -24,14 +27,11 @@ export function TechStackSection({
24
27
  title = 'The stack',
25
28
  description,
26
29
  items,
30
+ surface,
27
31
  }: TechStackSectionProps) {
32
+ // Still read directly: `mode` also drives the per-item tile rounding below.
28
33
  const mode = useUIVariant();
29
- const sectionClasses = cn(
30
- 'overflow-hidden bg-card',
31
- mode === 'smooth' && 'rounded-3xl border',
32
- mode === 'playful' && 'rounded-2xl border-primary/25 border',
33
- !mode && 'rounded-2xl border',
34
- );
34
+ const sectionClasses = useSectionSurface(surface);
35
35
 
36
36
  return (
37
37
  <section className={sectionClasses}>
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
 
3
- import { cn, useUIVariant } from '@olwiba/cn';
4
3
  import { TestimonialCard } from '../components/TestimonialCard';
5
4
  import { SectionTitle } from './SectionTitle';
6
5
  import { StaggerChildren } from '../motion/StaggerChildren';
6
+ import { useSectionSurface, type MarketingSurface } from './section-surface';
7
7
 
8
8
  export interface TestimonialsSectionProps {
9
9
  title?: string;
@@ -18,6 +18,8 @@ export interface TestimonialsSectionProps {
18
18
  initials?: string;
19
19
  rating?: number;
20
20
  }>;
21
+ /** How the section sits on the page. @default 'card' */
22
+ surface?: MarketingSurface;
21
23
  }
22
24
 
23
25
  export function TestimonialsSection({
@@ -25,14 +27,9 @@ export function TestimonialsSection({
25
27
  description = "Here's what engineers and product teams say after using Olwiba in production.",
26
28
  badge = 'Testimonials',
27
29
  testimonials,
30
+ surface,
28
31
  }: 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
- )
32
+ const sectionClasses = useSectionSurface(surface);
36
33
  return (
37
34
  <section className={sectionClasses}>
38
35
  <div className="px-6 py-14 sm:px-10 sm:py-20">
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { cn, useUIVariant } from '@olwiba/cn';
5
+
6
+ /**
7
+ * How a marketing section sits on the page.
8
+ *
9
+ * Every section used to hard-code the same island — an opaque `bg-card` panel
10
+ * with a border and a radius — which is right for a page that is only its
11
+ * sections, and wrong for one with an ambient background behind it: a run of
12
+ * opaque slabs blocks the thing that makes the page feel alive.
13
+ *
14
+ * - `'card'` — the opaque island. Contained, high contrast, reads as a panel.
15
+ * - `'soft'` — translucent and blurred. Still a shape, but the page behind it
16
+ * shows through, which is the treatment that works over an aura or gradient.
17
+ * - `'plain'` — no panel at all. The content sits directly on the page.
18
+ *
19
+ * `'card'` stays the default so nothing changes for a page that never sets it.
20
+ */
21
+ export type MarketingSurface = 'card' | 'soft' | 'plain';
22
+
23
+ const MarketingSurfaceContext = React.createContext<MarketingSurface | undefined>(undefined);
24
+
25
+ /**
26
+ * Sets the default surface for every marketing section beneath it.
27
+ *
28
+ * Page-level rather than per-section because "this page has an ambient
29
+ * background, so its sections should not be opaque" is one decision, and making
30
+ * it once beats repeating it on every section and having a new section silently
31
+ * default back to an island.
32
+ */
33
+ export function MarketingSurfaceProvider({
34
+ surface,
35
+ children,
36
+ }: {
37
+ surface: MarketingSurface | undefined;
38
+ children: React.ReactNode;
39
+ }) {
40
+ return (
41
+ <MarketingSurfaceContext.Provider value={surface}>{children}</MarketingSurfaceContext.Provider>
42
+ );
43
+ }
44
+
45
+ export function useMarketingSurface(): MarketingSurface | undefined {
46
+ return React.useContext(MarketingSurfaceContext);
47
+ }
48
+
49
+ /**
50
+ * The section wrapper's classes for the resolved surface and UI mode.
51
+ *
52
+ * `overflow-hidden` is unconditional on every surface, including `'plain'` —
53
+ * the marquee and carousel sections clip their own content against it, so
54
+ * dropping it with the panel would let them bleed across the page.
55
+ *
56
+ * Resolution order is prop, then `MarketingSurfaceProvider`, then `'card'`.
57
+ * Both context reads happen every render rather than short-circuiting on the
58
+ * prop: `modeProp ?? useHook()` skips a hook call and changes hook order
59
+ * between renders, which is the exact bug @olwiba/cn 0.1.28 and 0.1.30 fixed in
60
+ * `Toaster` and `Input`.
61
+ *
62
+ * Only `smooth`, `playful` and unset are branched on. olwibaCN's source has a
63
+ * fourth `glass` variant, but the published `UIVariant` type does not carry it
64
+ * yet, so a branch for it does not typecheck here.
65
+ */
66
+ export function useSectionSurface(override?: MarketingSurface): string {
67
+ const mode = useUIVariant();
68
+ const contextSurface = useMarketingSurface();
69
+ const surface = override ?? contextSurface ?? 'card';
70
+
71
+ if (surface === 'plain') return 'overflow-hidden';
72
+
73
+ if (surface === 'soft') {
74
+ return cn(
75
+ 'overflow-hidden border backdrop-blur-md',
76
+ mode === 'smooth' && 'rounded-[2rem] border-border/40 bg-card/40',
77
+ mode === 'playful' && 'rounded-3xl border-primary/20 bg-card/50',
78
+ !mode && 'rounded-3xl border-border/50 bg-card/45',
79
+ );
80
+ }
81
+
82
+ return cn(
83
+ 'overflow-hidden bg-card',
84
+ mode === 'smooth' && 'rounded-3xl border',
85
+ mode === 'playful' && 'rounded-2xl border-primary/25 border',
86
+ !mode && 'rounded-2xl border',
87
+ );
88
+ }