@olwiba/ui 0.2.7 → 0.2.8

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,16 @@ 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;
739
773
  }
740
- declare function PricingSection({ title, description, badge, plans, saveBadge, isAuthenticated, renderLink, footnote, mode, foundingDeadline, currency, highlightedBadgeLabel, renderPlanFooter, }: PricingSectionProps): react_jsx_runtime.JSX.Element;
774
+ declare function PricingSection({ title, description, badge, plans, saveBadge, isAuthenticated, renderLink, footnote, mode, foundingDeadline, currency, highlightedBadgeLabel, renderPlanFooter, cadences, defaultCadence, }: PricingSectionProps): react_jsx_runtime.JSX.Element;
741
775
 
742
776
  interface TestimonialsSectionProps {
743
777
  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,21 @@ function PricingSection({
2296
2322
  foundingDeadline,
2297
2323
  currency = "$",
2298
2324
  highlightedBadgeLabel = "Founding member",
2299
- renderPlanFooter
2325
+ renderPlanFooter,
2326
+ cadences,
2327
+ defaultCadence
2300
2328
  }) {
2301
2329
  const [annual, setAnnual] = React35.useState(false);
2302
2330
  const isOneTime = mode === "one-time";
2331
+ const useCadences = !!cadences?.length;
2332
+ const initialCadence = (defaultCadence && cadences?.some((c) => c.key === defaultCadence) ? defaultCadence : null) ?? cadences?.[0]?.key ?? "";
2333
+ const [activeCadence, setActiveCadence] = React35.useState(initialCadence);
2334
+ const cadence = cadences?.find((c) => c.key === activeCadence);
2335
+ const saveBadges = React35.useMemo(
2336
+ () => useCadences ? computeSaveBadges(plans, cadences, initialCadence) : {},
2337
+ [useCadences, plans, cadences, initialCadence]
2338
+ );
2339
+ const showToggle = useCadences ? cadences.length > 1 : !isOneTime;
2303
2340
  const uiMode = useUIVariant();
2304
2341
  const sectionClasses = cn$1(
2305
2342
  "overflow-hidden bg-card",
@@ -2312,7 +2349,23 @@ function PricingSection({
2312
2349
  badge && (typeof badge === "string" ? /* @__PURE__ */ jsx(Badge$1, { variant: "secondary", className: "mb-4", children: badge }) : /* @__PURE__ */ jsx("div", { className: "mb-4", children: badge })),
2313
2350
  /* @__PURE__ */ jsx("h2", { className: "text-balance text-3xl font-semibold tracking-tight sm:text-4xl", children: title }),
2314
2351
  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: [
2352
+ 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(
2353
+ Button$1,
2354
+ {
2355
+ variant: "ghost",
2356
+ size: "sm",
2357
+ onClick: () => setActiveCadence(entry.key),
2358
+ className: cn$1(
2359
+ "flex items-center gap-2 rounded-full px-4 py-1.5 text-sm font-medium transition-colors",
2360
+ entry.key === activeCadence ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
2361
+ ),
2362
+ children: [
2363
+ entry.label,
2364
+ saveBadges[entry.key] && /* @__PURE__ */ jsx(Badge$1, { variant: "secondary", className: "text-xs", children: saveBadges[entry.key] })
2365
+ ]
2366
+ },
2367
+ entry.key
2368
+ )) : /* @__PURE__ */ jsxs(Fragment, { children: [
2316
2369
  /* @__PURE__ */ jsx(
2317
2370
  Button$1,
2318
2371
  {
@@ -2342,12 +2395,12 @@ function PricingSection({
2342
2395
  ]
2343
2396
  }
2344
2397
  )
2345
- ] })
2398
+ ] }) })
2346
2399
  ] }),
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;
2400
+ /* @__PURE__ */ jsx(StaggerChildren, { className: cn$1("mt-10 grid gap-4", gridClassesFor(plans.length)), children: plans.map((plan) => {
2401
+ const rawPrice = useCadences ? plan.prices?.[activeCadence] ?? plan.monthly : isOneTime ? plan.monthly : annual ? plan.annual : plan.monthly;
2349
2402
  const price = plan.priceDisplay ?? `${currency}${rawPrice}`;
2350
- const period = plan.periodDisplay ?? (isOneTime ? "one-time" : rawPrice > 0 ? "/mo" : "");
2403
+ const period = useCadences ? cadence?.suffix ?? plan.periodDisplay ?? "" : plan.periodDisplay ?? (isOneTime ? "one-time" : rawPrice > 0 ? "/mo" : "");
2351
2404
  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
2405
  return /* @__PURE__ */ jsx(
2353
2406
  PricingCard,