@olwiba/ui 0.2.7 → 0.2.9
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 +44 -1
- package/dist/index.js +65 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/marketing/PricingSection.tsx +192 -31
package/dist/index.d.ts
CHANGED
|
@@ -703,10 +703,36 @@ interface PricingCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
703
703
|
}
|
|
704
704
|
declare function PricingCard({ name, price, period, description, features, cta, highlighted, disabled, ctaDisabled, badge, footer, onSelect, className, ...props }: PricingCardProps): react_jsx_runtime.JSX.Element;
|
|
705
705
|
|
|
706
|
+
/**
|
|
707
|
+
* One billing period a plan can be bought at.
|
|
708
|
+
*
|
|
709
|
+
* Products are not all sold monthly-or-annually: weekly, quarterly, and
|
|
710
|
+
* one-off all exist. Pass `cadences` to describe whatever this product
|
|
711
|
+
* actually sells and the section renders a tab per entry, rather than the
|
|
712
|
+
* fixed Monthly/Annual pair it assumes otherwise.
|
|
713
|
+
*/
|
|
714
|
+
interface PricingCadence {
|
|
715
|
+
/** Matches the keys of `PricingPlan.prices`. */
|
|
716
|
+
key: string;
|
|
717
|
+
label: string;
|
|
718
|
+
/**
|
|
719
|
+
* Billing periods in a year — 52 weekly, 12 monthly, 1 annual. Used only to
|
|
720
|
+
* compare cadences for the savings badge; omit it and no badge is computed
|
|
721
|
+
* for this cadence.
|
|
722
|
+
*/
|
|
723
|
+
periodsPerYear?: number;
|
|
724
|
+
/** Price suffix, e.g. "/ week". Falls back to `PricingPlan.periodDisplay`. */
|
|
725
|
+
suffix?: string;
|
|
726
|
+
}
|
|
706
727
|
interface PricingPlan {
|
|
707
728
|
name: string;
|
|
708
729
|
monthly: number;
|
|
709
730
|
annual: number;
|
|
731
|
+
/**
|
|
732
|
+
* Price per cadence key, for products using `cadences`. The `monthly` and
|
|
733
|
+
* `annual` fields above stay as the fallback for callers that don't.
|
|
734
|
+
*/
|
|
735
|
+
prices?: Record<string, number>;
|
|
710
736
|
description: string;
|
|
711
737
|
cta: string;
|
|
712
738
|
highlighted?: boolean;
|
|
@@ -736,8 +762,25 @@ interface PricingSectionProps {
|
|
|
736
762
|
highlightedBadgeLabel?: string;
|
|
737
763
|
/** Rendered below each plan's CTA button (e.g. a "Get notified" link). */
|
|
738
764
|
renderPlanFooter?: (plan: PricingPlan) => React.ReactNode;
|
|
765
|
+
/**
|
|
766
|
+
* Billing periods this product sells at. One entry renders no toggle at all;
|
|
767
|
+
* two or more render a tab each, with savings badges computed from
|
|
768
|
+
* `periodsPerYear`. Omit to keep the built-in Monthly/Annual pair.
|
|
769
|
+
*/
|
|
770
|
+
cadences?: PricingCadence[];
|
|
771
|
+
/** Which cadence opens selected. Defaults to the first in `cadences`. */
|
|
772
|
+
defaultCadence?: string;
|
|
773
|
+
/**
|
|
774
|
+
* Called when a plan's CTA is pressed, with the cadence currently selected —
|
|
775
|
+
* which is the half the caller can't otherwise know, since the toggle's state
|
|
776
|
+
* lives in here. Without it the CTA is inert: `PricingCard` takes an
|
|
777
|
+
* `onSelect` and this section had never passed one.
|
|
778
|
+
*/
|
|
779
|
+
onSelectPlan?: (plan: PricingPlan, cadence?: string) => void;
|
|
780
|
+
/** Marks one plan as busy (e.g. its checkout is being created). */
|
|
781
|
+
pendingPlanName?: string;
|
|
739
782
|
}
|
|
740
|
-
declare function PricingSection({ title, description, badge, plans, saveBadge, isAuthenticated, renderLink, footnote, mode, foundingDeadline, currency, highlightedBadgeLabel, renderPlanFooter, }: PricingSectionProps): react_jsx_runtime.JSX.Element;
|
|
783
|
+
declare function PricingSection({ title, description, badge, plans, saveBadge, isAuthenticated, renderLink, footnote, mode, foundingDeadline, currency, highlightedBadgeLabel, renderPlanFooter, cadences, defaultCadence, onSelectPlan, pendingPlanName, }: PricingSectionProps): react_jsx_runtime.JSX.Element;
|
|
741
784
|
|
|
742
785
|
interface TestimonialsSectionProps {
|
|
743
786
|
title?: string;
|
package/dist/index.js
CHANGED
|
@@ -2282,6 +2282,32 @@ function CountdownTimer({
|
|
|
2282
2282
|
] }, unit)) })
|
|
2283
2283
|
] });
|
|
2284
2284
|
}
|
|
2285
|
+
function computeSaveBadges(plans, cadences, defaultKey) {
|
|
2286
|
+
const annualised = (key) => {
|
|
2287
|
+
const cadence = cadences.find((c) => c.key === key);
|
|
2288
|
+
if (!cadence?.periodsPerYear) return null;
|
|
2289
|
+
const totals = plans.filter((plan) => plan.priceDisplay === void 0 && plan.prices?.[key] !== void 0).map((plan) => plan.prices[key] * cadence.periodsPerYear);
|
|
2290
|
+
return totals.length > 0 ? totals.reduce((sum, n) => sum + n, 0) : null;
|
|
2291
|
+
};
|
|
2292
|
+
const baseline = annualised(defaultKey);
|
|
2293
|
+
if (!baseline) return {};
|
|
2294
|
+
const badges = {};
|
|
2295
|
+
for (const cadence of cadences) {
|
|
2296
|
+
if (cadence.key === defaultKey) continue;
|
|
2297
|
+
const total = annualised(cadence.key);
|
|
2298
|
+
if (!total || total >= baseline) continue;
|
|
2299
|
+
const percent = Math.round((baseline - total) / baseline * 100);
|
|
2300
|
+
if (percent > 0) badges[cadence.key] = `Save ${percent}%`;
|
|
2301
|
+
}
|
|
2302
|
+
return badges;
|
|
2303
|
+
}
|
|
2304
|
+
function gridClassesFor(count) {
|
|
2305
|
+
if (count <= 1) return "mx-auto max-w-sm";
|
|
2306
|
+
if (count === 2) return "mx-auto max-w-3xl sm:grid-cols-2";
|
|
2307
|
+
if (count === 3) return "mx-auto max-w-5xl lg:grid-cols-3";
|
|
2308
|
+
if (count === 4) return "mx-auto max-w-6xl sm:grid-cols-2 lg:grid-cols-4";
|
|
2309
|
+
return "mx-auto max-w-5xl sm:grid-cols-2 lg:grid-cols-3";
|
|
2310
|
+
}
|
|
2285
2311
|
var defaultRenderLink6 = ({ href, children, className }) => /* @__PURE__ */ jsx("a", { href, className, children });
|
|
2286
2312
|
function PricingSection({
|
|
2287
2313
|
title = "Simple, transparent pricing",
|
|
@@ -2296,10 +2322,23 @@ function PricingSection({
|
|
|
2296
2322
|
foundingDeadline,
|
|
2297
2323
|
currency = "$",
|
|
2298
2324
|
highlightedBadgeLabel = "Founding member",
|
|
2299
|
-
renderPlanFooter
|
|
2325
|
+
renderPlanFooter,
|
|
2326
|
+
cadences,
|
|
2327
|
+
defaultCadence,
|
|
2328
|
+
onSelectPlan,
|
|
2329
|
+
pendingPlanName
|
|
2300
2330
|
}) {
|
|
2301
2331
|
const [annual, setAnnual] = React35.useState(false);
|
|
2302
2332
|
const isOneTime = mode === "one-time";
|
|
2333
|
+
const useCadences = !!cadences?.length;
|
|
2334
|
+
const initialCadence = (defaultCadence && cadences?.some((c) => c.key === defaultCadence) ? defaultCadence : null) ?? cadences?.[0]?.key ?? "";
|
|
2335
|
+
const [activeCadence, setActiveCadence] = React35.useState(initialCadence);
|
|
2336
|
+
const cadence = cadences?.find((c) => c.key === activeCadence);
|
|
2337
|
+
const saveBadges = React35.useMemo(
|
|
2338
|
+
() => useCadences ? computeSaveBadges(plans, cadences, initialCadence) : {},
|
|
2339
|
+
[useCadences, plans, cadences, initialCadence]
|
|
2340
|
+
);
|
|
2341
|
+
const showToggle = useCadences ? cadences.length > 1 : !isOneTime;
|
|
2303
2342
|
const uiMode = useUIVariant();
|
|
2304
2343
|
const sectionClasses = cn$1(
|
|
2305
2344
|
"overflow-hidden bg-card",
|
|
@@ -2312,7 +2351,23 @@ function PricingSection({
|
|
|
2312
2351
|
badge && (typeof badge === "string" ? /* @__PURE__ */ jsx(Badge$1, { variant: "secondary", className: "mb-4", children: badge }) : /* @__PURE__ */ jsx("div", { className: "mb-4", children: badge })),
|
|
2313
2352
|
/* @__PURE__ */ jsx("h2", { className: "text-balance text-3xl font-semibold tracking-tight sm:text-4xl", children: title }),
|
|
2314
2353
|
description && /* @__PURE__ */ jsx("p", { className: "mx-auto mt-4 max-w-xl text-pretty text-muted-foreground", children: description }),
|
|
2315
|
-
|
|
2354
|
+
showToggle && /* @__PURE__ */ jsx("div", { className: "mt-6 inline-flex items-center gap-3 rounded-full border bg-muted p-1", children: useCadences ? cadences.map((entry) => /* @__PURE__ */ jsxs(
|
|
2355
|
+
Button$1,
|
|
2356
|
+
{
|
|
2357
|
+
variant: "ghost",
|
|
2358
|
+
size: "sm",
|
|
2359
|
+
onClick: () => setActiveCadence(entry.key),
|
|
2360
|
+
className: cn$1(
|
|
2361
|
+
"flex items-center gap-2 rounded-full px-4 py-1.5 text-sm font-medium transition-colors",
|
|
2362
|
+
entry.key === activeCadence ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
|
|
2363
|
+
),
|
|
2364
|
+
children: [
|
|
2365
|
+
entry.label,
|
|
2366
|
+
saveBadges[entry.key] && /* @__PURE__ */ jsx(Badge$1, { variant: "secondary", className: "text-xs", children: saveBadges[entry.key] })
|
|
2367
|
+
]
|
|
2368
|
+
},
|
|
2369
|
+
entry.key
|
|
2370
|
+
)) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2316
2371
|
/* @__PURE__ */ jsx(
|
|
2317
2372
|
Button$1,
|
|
2318
2373
|
{
|
|
@@ -2342,12 +2397,12 @@ function PricingSection({
|
|
|
2342
2397
|
]
|
|
2343
2398
|
}
|
|
2344
2399
|
)
|
|
2345
|
-
] })
|
|
2400
|
+
] }) })
|
|
2346
2401
|
] }),
|
|
2347
|
-
/* @__PURE__ */ jsx(StaggerChildren, { className: "mt-10 grid gap-4
|
|
2348
|
-
const rawPrice = isOneTime ? plan.monthly : annual ? plan.annual : plan.monthly;
|
|
2402
|
+
/* @__PURE__ */ jsx(StaggerChildren, { className: cn$1("mt-10 grid gap-4", gridClassesFor(plans.length)), children: plans.map((plan) => {
|
|
2403
|
+
const rawPrice = useCadences ? plan.prices?.[activeCadence] ?? plan.monthly : isOneTime ? plan.monthly : annual ? plan.annual : plan.monthly;
|
|
2349
2404
|
const price = plan.priceDisplay ?? `${currency}${rawPrice}`;
|
|
2350
|
-
const period = plan.periodDisplay ?? (isOneTime ? "one-time" : rawPrice > 0 ? "/mo" : "");
|
|
2405
|
+
const period = useCadences ? cadence?.suffix ?? plan.periodDisplay ?? "" : plan.periodDisplay ?? (isOneTime ? "one-time" : rawPrice > 0 ? "/mo" : "");
|
|
2351
2406
|
const badge2 = plan.highlighted && foundingDeadline ? /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full border bg-secondary px-2.5 py-0.5 text-xs font-semibold text-secondary-foreground", children: /* @__PURE__ */ jsx(CountdownTimer, { deadline: foundingDeadline, compact: true }) }) : plan.highlighted && highlightedBadgeLabel ? highlightedBadgeLabel : void 0;
|
|
2352
2407
|
return /* @__PURE__ */ jsx(
|
|
2353
2408
|
PricingCard,
|
|
@@ -2357,12 +2412,13 @@ function PricingSection({
|
|
|
2357
2412
|
period,
|
|
2358
2413
|
description: plan.description,
|
|
2359
2414
|
features: plan.features,
|
|
2360
|
-
cta: plan.cta,
|
|
2415
|
+
cta: pendingPlanName === plan.name ? "Redirecting\u2026" : plan.cta,
|
|
2361
2416
|
highlighted: plan.highlighted,
|
|
2362
2417
|
disabled: plan.disabled,
|
|
2363
|
-
ctaDisabled: plan.ctaDisabled,
|
|
2418
|
+
ctaDisabled: plan.ctaDisabled || pendingPlanName === plan.name,
|
|
2364
2419
|
badge: badge2,
|
|
2365
|
-
footer: renderPlanFooter?.(plan)
|
|
2420
|
+
footer: renderPlanFooter?.(plan),
|
|
2421
|
+
onSelect: onSelectPlan ? () => onSelectPlan(plan, useCadences ? activeCadence : void 0) : void 0
|
|
2366
2422
|
},
|
|
2367
2423
|
plan.name
|
|
2368
2424
|
);
|