@olwiba/ui 0.2.3 → 0.2.4

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.2.3",
3
+ "version": "0.2.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
package/src/index.ts CHANGED
@@ -82,7 +82,7 @@ export { marketingSectionSpacing, type MarketingSectionSpacing } from './marketi
82
82
  export { HeroSection, type HeroSectionProps } from './marketing/HeroSection';
83
83
  export { FeaturesSection, type FeaturesSectionProps } from './marketing/FeaturesSection';
84
84
  export { GroupedFeaturesSection, type GroupedFeaturesSectionProps, type GroupedFeatureGroup } from './marketing/GroupedFeaturesSection';
85
- export { StepsSection, type StepsSectionProps, type StepItem } from './marketing/StepsSection';
85
+ export { StepsSection, type StepsSectionProps, type StepItem, type StepGroup } from './marketing/StepsSection';
86
86
  export { TechStackSection, type TechStackSectionProps, type TechStackItem } from './marketing/TechStackSection';
87
87
  export { FeatureMarqueeSection, type FeatureMarqueeSectionProps, type FeatureMarqueeRow, type FeatureMarqueeItem } from './marketing/FeatureMarqueeSection';
88
88
  export { CtaSection, type CtaSectionProps } from './marketing/CtaSection';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import * as React from 'react';
3
4
  import { cn, useUIVariant } from '@olwiba/cn';
4
5
  import { SectionTitle } from './SectionTitle';
5
6
  import { FadeIn } from '../motion/FadeIn';
@@ -7,14 +8,77 @@ import { FadeIn } from '../motion/FadeIn';
7
8
  export interface StepItem {
8
9
  emoji?: string;
9
10
  title: string;
10
- description: string;
11
+ description: React.ReactNode;
12
+ }
13
+
14
+ export interface StepGroup {
15
+ steps: StepItem[];
16
+ /** Content rendered after this group, outside the timeline (e.g. a CTA card). */
17
+ after?: React.ReactNode;
11
18
  }
12
19
 
13
20
  export interface StepsSectionProps {
14
21
  badge?: string;
15
22
  title?: string;
16
23
  description?: string;
24
+ steps?: StepItem[];
25
+ /** 'default': horizontal row (vertical on mobile). 'timeline': vertical numbered timeline with a fading accent line. */
26
+ variant?: 'default' | 'timeline';
27
+ /** Timeline only — step groups with optional interstitial content; overrides `steps`. Numbering continues across groups. */
28
+ groups?: StepGroup[];
29
+ }
30
+
31
+ function TimelineGroup({
32
+ steps,
33
+ startNumber,
34
+ fadeIn,
35
+ }: {
17
36
  steps: StepItem[];
37
+ startNumber: number;
38
+ fadeIn: boolean;
39
+ }) {
40
+ const fadePx = 48;
41
+ return (
42
+ <div className="relative">
43
+ {/* Accent line: optional fade-in head, solid middle, fade-out tail */}
44
+ {fadeIn && (
45
+ <div
46
+ aria-hidden="true"
47
+ className="absolute left-[19px] w-1 bg-gradient-to-b from-transparent to-primary"
48
+ style={{ top: 0, height: fadePx }}
49
+ />
50
+ )}
51
+ <div
52
+ aria-hidden="true"
53
+ className="absolute left-[19px] w-1 bg-primary"
54
+ style={{
55
+ top: fadeIn ? fadePx : 20,
56
+ height: fadeIn ? `calc(100% - ${fadePx}px)` : 'calc(100% - 20px)',
57
+ }}
58
+ />
59
+ <div
60
+ aria-hidden="true"
61
+ className="absolute left-[19px] w-1 bg-gradient-to-b from-primary to-transparent"
62
+ style={{ top: '100%', height: fadePx }}
63
+ />
64
+
65
+ <div className="space-y-10" style={fadeIn ? { paddingTop: fadePx } : undefined}>
66
+ {steps.map((step, i) => (
67
+ <FadeIn key={step.title} direction="up" delay={i * 80}>
68
+ <div className="relative flex gap-5">
69
+ <div className="relative z-10 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border-2 border-primary bg-primary text-lg font-bold text-primary-foreground">
70
+ {step.emoji ?? startNumber + i}
71
+ </div>
72
+ <div className="flex-1 pt-1 pb-2">
73
+ <h3 className="mb-2 text-xl font-semibold text-foreground">{step.title}</h3>
74
+ <div className="text-sm leading-relaxed text-muted-foreground">{step.description}</div>
75
+ </div>
76
+ </div>
77
+ </FadeIn>
78
+ ))}
79
+ </div>
80
+ </div>
81
+ );
18
82
  }
19
83
 
20
84
  export function StepsSection({
@@ -22,6 +86,8 @@ export function StepsSection({
22
86
  title = 'From zero to shipped',
23
87
  description,
24
88
  steps,
89
+ variant = 'default',
90
+ groups,
25
91
  }: StepsSectionProps) {
26
92
  const mode = useUIVariant();
27
93
  const sectionClasses = cn(
@@ -31,6 +97,37 @@ export function StepsSection({
31
97
  !mode && 'rounded-2xl border',
32
98
  );
33
99
 
100
+ if (variant === 'timeline') {
101
+ const resolvedGroups: StepGroup[] = groups ?? (steps ? [{ steps }] : []);
102
+ let nextNumber = 1;
103
+
104
+ return (
105
+ <section className={sectionClasses}>
106
+ <div className="px-6 py-14 sm:px-10 sm:py-20">
107
+ <div className="mx-auto max-w-3xl">
108
+ <SectionTitle badge={badge} title={title} description={description} className="text-left [&>p]:mx-0" />
109
+ <div className="mt-12">
110
+ {resolvedGroups.map((group, groupIndex) => {
111
+ const startNumber = nextNumber;
112
+ nextNumber += group.steps.length;
113
+ return (
114
+ <React.Fragment key={groupIndex}>
115
+ <div className={cn(groupIndex > 0 && 'mt-10')}>
116
+ <TimelineGroup steps={group.steps} startNumber={startNumber} fadeIn={groupIndex > 0} />
117
+ </div>
118
+ {group.after && <div className="mt-16">{group.after}</div>}
119
+ </React.Fragment>
120
+ );
121
+ })}
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </section>
126
+ );
127
+ }
128
+
129
+ const resolvedSteps = steps ?? [];
130
+
34
131
  return (
35
132
  <section className={sectionClasses}>
36
133
  <div className="px-6 py-14 sm:px-10 sm:py-20">
@@ -39,12 +136,12 @@ export function StepsSection({
39
136
 
40
137
  <div className="mt-12">
41
138
  {/* Desktop: horizontal row */}
42
- <div className="hidden sm:grid" style={{ gridTemplateColumns: `repeat(${steps.length}, 1fr)` }}>
43
- {steps.map((step, i) => (
139
+ <div className="hidden sm:grid" style={{ gridTemplateColumns: `repeat(${resolvedSteps.length}, 1fr)` }}>
140
+ {resolvedSteps.map((step, i) => (
44
141
  <FadeIn key={step.title} direction="up" delay={i * 80}>
45
142
  <div className="relative flex flex-col items-center text-center px-4">
46
143
  {/* Dashed connector to next step */}
47
- {i < steps.length - 1 && (
144
+ {i < resolvedSteps.length - 1 && (
48
145
  <div
49
146
  aria-hidden="true"
50
147
  className="absolute top-6 left-1/2 w-full border-t border-dashed border-border"
@@ -57,7 +154,7 @@ export function StepsSection({
57
154
  )}
58
155
  </div>
59
156
  <h3 className="font-semibold text-foreground">{step.title}</h3>
60
- <p className="mt-2 text-sm leading-relaxed text-muted-foreground">{step.description}</p>
157
+ <div className="mt-2 text-sm leading-relaxed text-muted-foreground">{step.description}</div>
61
158
  </div>
62
159
  </FadeIn>
63
160
  ))}
@@ -65,11 +162,11 @@ export function StepsSection({
65
162
 
66
163
  {/* Mobile: vertical timeline */}
67
164
  <div className="flex flex-col gap-0 sm:hidden">
68
- {steps.map((step, i) => (
165
+ {resolvedSteps.map((step, i) => (
69
166
  <FadeIn key={step.title} direction="up" delay={i * 80}>
70
167
  <div className="relative flex gap-4 pb-8 last:pb-0">
71
168
  {/* Vertical connector */}
72
- {i < steps.length - 1 && (
169
+ {i < resolvedSteps.length - 1 && (
73
170
  <div
74
171
  aria-hidden="true"
75
172
  className="absolute left-[23px] top-14 bottom-0 w-px border-l border-dashed border-border"
@@ -83,7 +180,7 @@ export function StepsSection({
83
180
  </div>
84
181
  <div className="pt-3">
85
182
  <h3 className="font-semibold text-foreground">{step.title}</h3>
86
- <p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">{step.description}</p>
183
+ <div className="mt-1.5 text-sm leading-relaxed text-muted-foreground">{step.description}</div>
87
184
  </div>
88
185
  </div>
89
186
  </FadeIn>