@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 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
- !isOneTime && /* @__PURE__ */ jsxs("div", { className: "mt-6 inline-flex items-center gap-3 rounded-full border bg-muted p-1", children: [
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 lg:grid-cols-3", children: plans.map((plan) => {
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
  );