@olwiba/ui 0.1.9 → 0.1.11
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/dist/index.d.ts +14 -5
- package/dist/index.js +49 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/FlowBracket.tsx +56 -23
- package/src/components/PricingCard.tsx +9 -1
- package/src/marketing/PricingSection.tsx +7 -0
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@ export interface FlowBracketProps {
|
|
|
18
18
|
* Defaults to 22.
|
|
19
19
|
*/
|
|
20
20
|
extent?: number;
|
|
21
|
-
/** Show open-chevron arrowhead at the
|
|
21
|
+
/** Show open-chevron arrowhead at the destination end. Default: true. */
|
|
22
22
|
arrow?: boolean;
|
|
23
23
|
/** CSS color for both bracket lines and animated dots. Defaults to muted slate. */
|
|
24
24
|
color?: string;
|
|
@@ -46,16 +46,14 @@ export interface FlowBracketProps {
|
|
|
46
46
|
children: React.ReactNode;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const MARKER_L = 'fb-ml';
|
|
50
|
-
const MARKER_R = 'fb-mr';
|
|
51
|
-
|
|
52
49
|
/**
|
|
53
50
|
* Wraps children in a relative container and draws square bracket lines on the
|
|
54
51
|
* left and/or right outside edges — connecting the top of the first child down
|
|
55
52
|
* to the bottom of the last child (e.g. docs → SYNC direction).
|
|
56
53
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
54
|
+
* Arrowheads are rendered as absolutely-positioned SVGs (not SVG markers) so
|
|
55
|
+
* they render at a consistent pixel size regardless of the bracket SVG's aspect
|
|
56
|
+
* ratio (which spans the full section height with preserveAspectRatio="none").
|
|
59
57
|
*/
|
|
60
58
|
export function FlowBracket({
|
|
61
59
|
anchorTop = 8,
|
|
@@ -80,17 +78,15 @@ export function FlowBracket({
|
|
|
80
78
|
const cl = colorLeft ?? color;
|
|
81
79
|
const cr = colorRight ?? color;
|
|
82
80
|
|
|
83
|
-
// Left bracket: bottom → top (arrowhead at top, pointing into genesis)
|
|
81
|
+
// Left bracket: bottom → top (arrowhead at top, pointing right into genesis)
|
|
84
82
|
const leftD = `M 0,${anchorBottom} L ${L},${anchorBottom} L ${L},${anchorTop} L 0,${anchorTop}`;
|
|
85
|
-
// Right bracket: reversed = top → bottom (arrowhead at bottom, pointing into sync)
|
|
83
|
+
// Right bracket: reversed = top → bottom (arrowhead at bottom, pointing left into sync)
|
|
86
84
|
const rightD = reverseRight
|
|
87
85
|
? `M 1000,${anchorTop} L ${R},${anchorTop} L ${R},${anchorBottom} L 1000,${anchorBottom}`
|
|
88
86
|
: `M 1000,${anchorBottom} L ${R},${anchorBottom} L ${R},${anchorTop} L 1000,${anchorTop}`;
|
|
89
87
|
|
|
90
88
|
// CSS dot: extent as % of container width
|
|
91
89
|
const ep = `${(extent / 10).toFixed(1)}%`;
|
|
92
|
-
// Dots travel bottom → top (sync → genesis/docs direction), fade in/out at card edges
|
|
93
|
-
// fb-drr: reversed — top → bottom (docs → sync direction) used when reverseRight=true
|
|
94
90
|
const dotKeyframes = animate ? `
|
|
95
91
|
@keyframes fb-dl{
|
|
96
92
|
0%{top:${anchorBottom}%;left:0;opacity:0}
|
|
@@ -127,6 +123,25 @@ export function FlowBracket({
|
|
|
127
123
|
opacity: 0,
|
|
128
124
|
};
|
|
129
125
|
|
|
126
|
+
// Shared arrowhead polyline props — same chevron shape as ecosystem connector markers
|
|
127
|
+
const chevronProps = {
|
|
128
|
+
fill: 'none',
|
|
129
|
+
strokeWidth: '1.5',
|
|
130
|
+
strokeLinecap: 'round' as const,
|
|
131
|
+
strokeLinejoin: 'round' as const,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Arrowhead SVG style — positioned in CSS space so it renders at a fixed pixel size
|
|
135
|
+
// regardless of the bracket SVG's non-uniform scale (preserveAspectRatio="none")
|
|
136
|
+
const arrowBase: React.CSSProperties = {
|
|
137
|
+
position: 'absolute',
|
|
138
|
+
width: 10,
|
|
139
|
+
height: 8,
|
|
140
|
+
overflow: 'visible',
|
|
141
|
+
pointerEvents: 'none',
|
|
142
|
+
transform: 'translate(-50%, -50%)',
|
|
143
|
+
};
|
|
144
|
+
|
|
130
145
|
return (
|
|
131
146
|
<div className={className} style={{ position: 'relative', ...style }}>
|
|
132
147
|
{animate && <style>{dotKeyframes}</style>}
|
|
@@ -137,24 +152,42 @@ export function FlowBracket({
|
|
|
137
152
|
className="pointer-events-none absolute inset-0"
|
|
138
153
|
style={{ width: '100%', height: '100%', overflow: 'visible' }}
|
|
139
154
|
>
|
|
140
|
-
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
</marker>
|
|
144
|
-
<marker id={MARKER_R} markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
|
|
145
|
-
<polyline points="1,1 8,4 1,7" fill="none" stroke={cr} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
146
|
-
</marker>
|
|
147
|
-
</defs>
|
|
148
|
-
{left && (
|
|
149
|
-
<path d={leftD} fill="none" stroke={cl} strokeWidth={1.5} strokeDasharray="5,4"
|
|
150
|
-
vectorEffect="non-scaling-stroke" {...(arrow ? { markerEnd: `url(#${MARKER_L})` } : {})} />
|
|
155
|
+
{left && (
|
|
156
|
+
<path d={leftD} fill="none" style={{ stroke: cl }} strokeWidth={1.5} strokeDasharray="5,4"
|
|
157
|
+
vectorEffect="non-scaling-stroke" />
|
|
151
158
|
)}
|
|
152
159
|
{right && (
|
|
153
|
-
<path d={rightD} fill="none"
|
|
154
|
-
vectorEffect="non-scaling-stroke"
|
|
160
|
+
<path d={rightD} fill="none" style={{ stroke: cr }} strokeWidth={1.5} strokeDasharray="5,4"
|
|
161
|
+
vectorEffect="non-scaling-stroke" />
|
|
155
162
|
)}
|
|
156
163
|
</svg>
|
|
157
164
|
|
|
165
|
+
{/* Left bracket arrowhead — tip points right (→) into the genesis/docs tier */}
|
|
166
|
+
{arrow && left && (
|
|
167
|
+
<svg
|
|
168
|
+
aria-hidden
|
|
169
|
+
viewBox="0 0 10 8"
|
|
170
|
+
style={{ ...arrowBase, top: `${anchorTop}%`, left: 0 }}
|
|
171
|
+
>
|
|
172
|
+
<polyline points="1,1 8,4 1,7" style={{ stroke: cl }} {...chevronProps} />
|
|
173
|
+
</svg>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{/* Right bracket arrowhead — tip points left (←) into the destination tier */}
|
|
177
|
+
{arrow && right && (
|
|
178
|
+
<svg
|
|
179
|
+
aria-hidden
|
|
180
|
+
viewBox="0 0 10 8"
|
|
181
|
+
style={{
|
|
182
|
+
...arrowBase,
|
|
183
|
+
top: reverseRight ? `${anchorBottom}%` : `${anchorTop}%`,
|
|
184
|
+
left: '100%',
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
<polyline points="9,1 2,4 9,7" style={{ stroke: cr }} {...chevronProps} />
|
|
188
|
+
</svg>
|
|
189
|
+
)}
|
|
190
|
+
|
|
158
191
|
{children}
|
|
159
192
|
|
|
160
193
|
{/* Dots placed after children so card stacking contexts (z-index ≥ 1) render above */}
|
|
@@ -18,7 +18,11 @@ export interface PricingCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
18
18
|
cta: string;
|
|
19
19
|
highlighted?: boolean;
|
|
20
20
|
disabled?: boolean;
|
|
21
|
+
/** Disables only the CTA button, leaving the rest of the card interactive. */
|
|
22
|
+
ctaDisabled?: boolean;
|
|
21
23
|
badge?: React.ReactNode;
|
|
24
|
+
/** Rendered directly below the CTA button (e.g. a "Get notified" link). */
|
|
25
|
+
footer?: React.ReactNode;
|
|
22
26
|
onSelect?: () => void;
|
|
23
27
|
}
|
|
24
28
|
|
|
@@ -31,7 +35,9 @@ export function PricingCard({
|
|
|
31
35
|
cta,
|
|
32
36
|
highlighted = false,
|
|
33
37
|
disabled = false,
|
|
38
|
+
ctaDisabled = false,
|
|
34
39
|
badge,
|
|
40
|
+
footer,
|
|
35
41
|
onSelect,
|
|
36
42
|
className,
|
|
37
43
|
...props
|
|
@@ -72,11 +78,13 @@ export function PricingCard({
|
|
|
72
78
|
variant={highlighted ? 'default' : 'outline'}
|
|
73
79
|
className="mt-6 w-full"
|
|
74
80
|
onClick={onSelect}
|
|
75
|
-
disabled={disabled}
|
|
81
|
+
disabled={disabled || ctaDisabled}
|
|
76
82
|
>
|
|
77
83
|
{cta}
|
|
78
84
|
</Button>
|
|
79
85
|
|
|
86
|
+
{footer && <div className="mt-3 text-center text-sm">{footer}</div>}
|
|
87
|
+
|
|
80
88
|
<Separator className="my-6" />
|
|
81
89
|
|
|
82
90
|
<ul className="space-y-3 text-sm">
|
|
@@ -15,6 +15,8 @@ export interface PricingPlan {
|
|
|
15
15
|
cta: string;
|
|
16
16
|
highlighted?: boolean;
|
|
17
17
|
disabled?: boolean;
|
|
18
|
+
/** Disables only the CTA button, leaving the rest of the card interactive. */
|
|
19
|
+
ctaDisabled?: boolean;
|
|
18
20
|
features: PricingFeature[];
|
|
19
21
|
/** Overrides computed price display (e.g. "$?" for community/reach-out tiers). */
|
|
20
22
|
priceDisplay?: string;
|
|
@@ -33,6 +35,8 @@ export interface PricingSectionProps {
|
|
|
33
35
|
footnote?: string;
|
|
34
36
|
mode?: 'subscription' | 'one-time';
|
|
35
37
|
foundingDeadline?: string;
|
|
38
|
+
/** Rendered below each plan's CTA button (e.g. a "Get notified" link). */
|
|
39
|
+
renderPlanFooter?: (plan: PricingPlan) => React.ReactNode;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
const defaultRenderLink: AppShellRenderLink = ({ href, children, className }) => (
|
|
@@ -50,6 +54,7 @@ export function PricingSection({
|
|
|
50
54
|
footnote,
|
|
51
55
|
mode = 'subscription',
|
|
52
56
|
foundingDeadline,
|
|
57
|
+
renderPlanFooter,
|
|
53
58
|
}: PricingSectionProps) {
|
|
54
59
|
const [annual, setAnnual] = React.useState(false);
|
|
55
60
|
const isOneTime = mode === 'one-time';
|
|
@@ -139,7 +144,9 @@ export function PricingSection({
|
|
|
139
144
|
cta={plan.cta}
|
|
140
145
|
highlighted={plan.highlighted}
|
|
141
146
|
disabled={plan.disabled}
|
|
147
|
+
ctaDisabled={plan.ctaDisabled}
|
|
142
148
|
badge={badge}
|
|
149
|
+
footer={renderPlanFooter?.(plan)}
|
|
143
150
|
/>
|
|
144
151
|
);
|
|
145
152
|
})}
|