@kerne/react 0.1.3 → 1.1.0

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/ui/index.js CHANGED
@@ -6,17 +6,18 @@ import {
6
6
  useAccess,
7
7
  useAuth,
8
8
  useAuthConfig,
9
- useCheckout,
9
+ useCancelSubscription,
10
10
  useClient,
11
11
  useEntitlements,
12
12
  useErrorResolver,
13
13
  useLocalization,
14
+ usePlanSwitch,
14
15
  usePlans,
15
16
  usePortal,
16
17
  useSubscription,
17
18
  useUser,
18
19
  useWaitlist
19
- } from "../chunk-F6NV76OR.js";
20
+ } from "../chunk-IZMRK3WK.js";
20
21
 
21
22
  // src/ui/AuthFlow.tsx
22
23
  import { useState as useState10 } from "react";
@@ -246,6 +247,7 @@ var CSS = `
246
247
 
247
248
  /* ---------- Stack ---------- */
248
249
  .kerne-stack { display: flex; flex-direction: column; gap: 16px; }
250
+ .kerne-inline-actions { display: flex; gap: 8px; flex-wrap: wrap; }
249
251
  .kerne-form { display: flex; flex-direction: column; gap: 12px; }
250
252
  .kerne-field { display: flex; flex-direction: column; gap: 6px; }
251
253
  .kerne-label { font-size: 14px; font-weight: 500; color: var(--kerne-fg); }
@@ -512,6 +514,126 @@ var CSS = `
512
514
  .kerne-meter-note { font-size: 12px; color: var(--kerne-fg-subtle); margin: 0; }
513
515
  @media (prefers-reduced-motion: reduce) { .kerne-meter-fill { transition: none; } }
514
516
 
517
+ /* ---------- Pricing table ---------- */
518
+ .kerne-pricing-skeleton { width: 100%; }
519
+
520
+ .kerne-pricing-switch-wrap { display: flex; justify-content: center; }
521
+ .kerne-pricing-switch {
522
+ position: relative;
523
+ display: inline-flex;
524
+ gap: 2px;
525
+ padding: 4px;
526
+ background: var(--kerne-panel);
527
+ border-radius: 999px;
528
+ }
529
+ .kerne-pricing-switch-thumb {
530
+ position: absolute;
531
+ top: 4px;
532
+ bottom: 4px;
533
+ left: 0;
534
+ border-radius: 999px;
535
+ background: var(--kerne-surface);
536
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
537
+ transition: transform 0.32s cubic-bezier(0.16, 1, 0.3, 1), width 0.32s cubic-bezier(0.16, 1, 0.3, 1);
538
+ }
539
+ .kerne-pricing-switch-option {
540
+ position: relative;
541
+ z-index: 1;
542
+ display: inline-flex;
543
+ align-items: center;
544
+ gap: 7px;
545
+ font: inherit;
546
+ font-size: 13.5px;
547
+ font-weight: 500;
548
+ white-space: nowrap;
549
+ color: var(--kerne-fg-muted);
550
+ background: none;
551
+ border: none;
552
+ border-radius: 999px;
553
+ padding: 7px 16px;
554
+ cursor: pointer;
555
+ transition: color 0.18s ease;
556
+ }
557
+ .kerne-pricing-switch-option[aria-pressed="true"] { color: var(--kerne-fg); }
558
+ .kerne-pricing-switch-option:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: 2px; }
559
+ .kerne-pricing-switch-save {
560
+ font-size: 10.5px;
561
+ font-weight: 600;
562
+ letter-spacing: 0.01em;
563
+ color: var(--kerne-success);
564
+ background: color-mix(in srgb, var(--kerne-success) 14%, transparent);
565
+ padding: 2px 6px;
566
+ border-radius: 999px;
567
+ }
568
+
569
+ .kerne-pricing-grid {
570
+ display: grid;
571
+ grid-template-columns: repeat(auto-fit, minmax(248px, 1fr));
572
+ gap: 20px;
573
+ align-items: stretch;
574
+ }
575
+
576
+ .kerne-pricing-card {
577
+ position: relative;
578
+ display: flex;
579
+ flex-direction: column;
580
+ gap: 24px;
581
+ height: 100%;
582
+ padding: 28px 26px;
583
+ background: var(--kerne-surface);
584
+ border: 1px solid var(--kerne-border);
585
+ border-radius: calc(var(--kerne-radius) + 8px);
586
+ }
587
+ .kerne-pricing-card[data-featured="true"] {
588
+ background: var(--kerne-surface-raised);
589
+ border-color: var(--kerne-fg);
590
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 24px 48px -28px rgba(0, 0, 0, 0.22);
591
+ }
592
+ .kerne-pricing-card[data-current="true"]:not([data-featured="true"]) { border-color: var(--kerne-border-strong); }
593
+
594
+ /* A quiet inline pill, not a loud badge escaping the card border - that
595
+ shouted-uppercase-chip-on-the-edge look is the single most template-y
596
+ tell a pricing page can have. Reads as confirmation of what the card's
597
+ own elevation already signals, never as the primary signal itself. */
598
+ .kerne-pricing-badge {
599
+ align-self: flex-start;
600
+ font-size: 11.5px;
601
+ font-weight: 600;
602
+ letter-spacing: 0.01em;
603
+ color: var(--kerne-fg);
604
+ background: var(--kerne-panel);
605
+ padding: 3px 9px;
606
+ border-radius: 999px;
607
+ }
608
+
609
+ .kerne-pricing-card-head { display: flex; flex-direction: column; gap: 8px; }
610
+ .kerne-pricing-card-name { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
611
+ .kerne-pricing-card-desc { margin: 0; font-size: 13px; line-height: 1.5; color: var(--kerne-fg-muted); }
612
+
613
+ .kerne-pricing-price { display: flex; flex-direction: column; gap: 3px; min-height: 52px; justify-content: center; }
614
+ .kerne-pricing-price-row { display: flex; align-items: baseline; gap: 5px; }
615
+ .kerne-pricing-amount {
616
+ font-size: 38px;
617
+ font-weight: 600;
618
+ letter-spacing: -0.025em;
619
+ line-height: 1;
620
+ font-variant-numeric: tabular-nums;
621
+ }
622
+ .kerne-pricing-interval { font-size: 14px; color: var(--kerne-fg-muted); }
623
+ .kerne-pricing-billed { margin: 0; font-size: 12px; color: var(--kerne-fg-subtle); }
624
+
625
+ .kerne-pricing-features { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 11px; flex: 1; }
626
+ .kerne-pricing-feature { display: flex; align-items: flex-start; gap: 9px; font-size: 13.5px; line-height: 1.4; color: var(--kerne-fg); }
627
+ .kerne-pricing-feature svg { flex-shrink: 0; margin-top: 2px; color: var(--kerne-fg-subtle); }
628
+ .kerne-pricing-feature-more { font-size: 12.5px; color: var(--kerne-fg-subtle); padding-left: 24px; }
629
+
630
+ .kerne-pricing-cta { margin-top: auto; }
631
+ .kerne-pricing-cta .kerne-button { width: 100%; }
632
+
633
+ @media (prefers-reduced-motion: reduce) {
634
+ .kerne-pricing-switch-thumb { transition: none; }
635
+ }
636
+
515
637
  /* ---------- Sections (profile, settings) ---------- */
516
638
  .kerne-section-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
517
639
  .kerne-section-title { font-size: 15px; font-weight: 600; margin: 0; }
@@ -2677,15 +2799,83 @@ function formatInterval(interval, labels) {
2677
2799
  return interval;
2678
2800
  }
2679
2801
  }
2802
+ var ENCODED_INTERVAL_UNIT = { D: 12 / 365, M: 1, Y: 12 };
2803
+ function approxMonths(value) {
2804
+ const match = ENCODED_INTERVAL.exec(value);
2805
+ if (!match) return 0;
2806
+ return parseInt(match[1], 10) * ENCODED_INTERVAL_UNIT[match[2]];
2807
+ }
2808
+ function resolvePriceDisplay(price, displayInterval) {
2809
+ if (!displayInterval || displayInterval === price.interval) {
2810
+ return {
2811
+ billedAmount: price.amount,
2812
+ billedInterval: price.interval,
2813
+ displayAmount: price.amount,
2814
+ displayInterval: price.interval
2815
+ };
2816
+ }
2817
+ const ratio = approxMonths(displayInterval) / approxMonths(price.interval);
2818
+ return {
2819
+ billedAmount: price.amount,
2820
+ billedInterval: price.interval,
2821
+ displayAmount: Math.round(price.amount * ratio),
2822
+ displayInterval
2823
+ };
2824
+ }
2825
+ function collectPriceLabels(plans) {
2826
+ const bySlug = /* @__PURE__ */ new Map();
2827
+ for (const plan of plans) {
2828
+ for (const price of plan.prices) {
2829
+ if (!bySlug.has(price.label.slug)) bySlug.set(price.label.slug, price.label);
2830
+ }
2831
+ }
2832
+ return Array.from(bySlug.values()).sort(
2833
+ (a, b) => a.sort_order - b.sort_order || a.name.localeCompare(b.name)
2834
+ );
2835
+ }
2836
+ function resolveLabelSavings(plans, labels) {
2837
+ if (labels.length < 2) return null;
2838
+ const priceFor = (labelSlug) => {
2839
+ for (const plan of plans) {
2840
+ const price = plan.prices.find((p) => p.label.slug === labelSlug);
2841
+ if (price) return price;
2842
+ }
2843
+ return void 0;
2844
+ };
2845
+ const sorted = [...labels].sort(
2846
+ (a, b) => approxMonths(priceFor(a.slug)?.interval ?? "1M") - approxMonths(priceFor(b.slug)?.interval ?? "1M")
2847
+ );
2848
+ const shortest = sorted[0];
2849
+ const longest = sorted[sorted.length - 1];
2850
+ if (shortest.slug === longest.slug) return null;
2851
+ for (const plan of plans) {
2852
+ const shortPrice = plan.prices.find((p) => p.label.slug === shortest.slug);
2853
+ const longPrice = plan.prices.find((p) => p.label.slug === longest.slug);
2854
+ if (!shortPrice?.amount || !longPrice) continue;
2855
+ const shortMonths = approxMonths(shortPrice.interval);
2856
+ const longMonths = approxMonths(longPrice.interval);
2857
+ if (!shortMonths || !longMonths) continue;
2858
+ const savings = 1 - longPrice.amount / longMonths / (shortPrice.amount / shortMonths);
2859
+ if (savings > 0.01) return { labelSlug: longest.slug, percent: Math.round(savings * 100) };
2860
+ }
2861
+ return null;
2862
+ }
2680
2863
 
2681
2864
  // src/ui/UsageMeters.tsx
2682
- import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
2683
- function levelFor(ratio, overageAllowed) {
2684
- if (overageAllowed) return ratio >= 1 ? "warning" : "normal";
2865
+ import { Fragment as Fragment8, jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
2866
+ function isSoftOverage(overage) {
2867
+ return overage === "ALLOW" || overage === "BILL";
2868
+ }
2869
+ function levelFor(ratio, softOverage) {
2870
+ if (softOverage) return ratio >= 1 ? "warning" : "normal";
2685
2871
  if (ratio >= 0.9) return "critical";
2686
2872
  if (ratio >= 0.75) return "warning";
2687
2873
  return "normal";
2688
2874
  }
2875
+ function quotaRatio(used, limit) {
2876
+ if (limit <= 0) return used > 0 ? 1 : 0;
2877
+ return Math.min(used / limit, 1);
2878
+ }
2689
2879
  function UsageMeters({
2690
2880
  title,
2691
2881
  featureKeys,
@@ -2739,10 +2929,13 @@ function UsageMeters({
2739
2929
  return shell(
2740
2930
  /* @__PURE__ */ jsx15("div", { className: "kerne-meters", children: rows.map((e) => {
2741
2931
  const unlimited = e.limit === null || e.limit === void 0;
2742
- const overageAllowed = e.overage_behavior === "ALLOW";
2743
- const ratio = unlimited ? 0 : Math.min(e.used / e.limit, 1);
2744
- const level = unlimited ? "normal" : levelFor(e.used / e.limit, overageAllowed);
2932
+ const softOverage = isSoftOverage(e.overage_behavior);
2933
+ const billed = e.overage_behavior === "BILL";
2934
+ const ratio = unlimited ? 0 : quotaRatio(e.used, e.limit);
2935
+ const level = unlimited ? "normal" : levelFor(ratio, softOverage);
2745
2936
  const over = !unlimited && e.used > e.limit;
2937
+ const standing = billed ? subscription?.entitlements.find((se) => se.feature_key === e.feature_key) : void 0;
2938
+ const capReached = billed && (standing?.cap_reached ?? false);
2746
2939
  return /* @__PURE__ */ jsxs14("div", { className: "kerne-meter", "data-level": level, children: [
2747
2940
  /* @__PURE__ */ jsxs14("div", { className: "kerne-meter-head", children: [
2748
2941
  /* @__PURE__ */ jsx15("span", { className: "kerne-meter-name", children: nameFor(e.feature_key) }),
@@ -2760,9 +2953,18 @@ function UsageMeters({
2760
2953
  children: /* @__PURE__ */ jsx15("div", { className: "kerne-meter-fill", style: { width: `${ratio * 100}%` } })
2761
2954
  }
2762
2955
  ) : null,
2763
- over && overageAllowed ? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: fill(l10n.usage.overage, {
2764
- count: formatNumber(e.used - e.limit)
2765
- }) }) : over ? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: l10n.usage.limitReached }) : null
2956
+ over && softOverage ? /* @__PURE__ */ jsxs14(Fragment8, { children: [
2957
+ /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: fill(
2958
+ // capReached inverts overageBilled's meaning (billing stopped,
2959
+ // access didn't) - a variant string would say the opposite of
2960
+ // the truth for the one plan that just stopped costing money.
2961
+ capReached ? l10n.usage.capReached : billed ? l10n.usage.overageBilled : l10n.usage.overage,
2962
+ { count: formatNumber(e.used - e.limit) }
2963
+ ) }),
2964
+ billed && standing?.billed_amount != null && standing.currency ? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note kerne-meter-cost", children: fill(l10n.usage.billedAmount, {
2965
+ amount: formatMoney(standing.billed_amount, standing.currency)
2966
+ }) }) : null
2967
+ ] }) : over ? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: l10n.usage.limitReached }) : null
2766
2968
  ] }, e.feature_key);
2767
2969
  }) })
2768
2970
  );
@@ -2770,7 +2972,7 @@ function UsageMeters({
2770
2972
 
2771
2973
  // src/ui/SubscriptionCard.tsx
2772
2974
  import { useState as useState16 } from "react";
2773
- import { Fragment as Fragment8, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
2975
+ import { Fragment as Fragment9, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
2774
2976
  var STATUS_TONES = {
2775
2977
  ACTIVE: "success",
2776
2978
  TRIALING: "success",
@@ -2786,11 +2988,13 @@ function SubscriptionCard({
2786
2988
  upgradeUrl,
2787
2989
  onUpgradeClick,
2788
2990
  hidePortalButton = false,
2991
+ hideCancelButton = false,
2789
2992
  appearance,
2790
2993
  className
2791
2994
  }) {
2792
- const { subscription, isLoading, error } = useSubscription(productSlug);
2995
+ const { subscription, isLoading, error, refetch } = useSubscription(productSlug);
2793
2996
  const { openPortal } = usePortal();
2997
+ const { cancelSubscription, isCanceling } = useCancelSubscription();
2794
2998
  const l10n = useLocalization();
2795
2999
  const showError = useErrorResolver();
2796
3000
  const heading = title === null ? null : title ?? l10n.billing.plan;
@@ -2804,6 +3008,8 @@ function SubscriptionCard({
2804
3008
  };
2805
3009
  const [opening, setOpening] = useState16(false);
2806
3010
  const [portalError, setPortalError] = useState16(null);
3011
+ const [confirmingCancel, setConfirmingCancel] = useState16(false);
3012
+ const [cancelError, setCancelError] = useState16(null);
2807
3013
  const handlePortal = async () => {
2808
3014
  setPortalError(null);
2809
3015
  setOpening(true);
@@ -2814,6 +3020,17 @@ function SubscriptionCard({
2814
3020
  setOpening(false);
2815
3021
  }
2816
3022
  };
3023
+ const handleCancel = async () => {
3024
+ if (!subscription) return;
3025
+ setCancelError(null);
3026
+ try {
3027
+ await cancelSubscription(subscription.id, false);
3028
+ setConfirmingCancel(false);
3029
+ refetch();
3030
+ } catch (e) {
3031
+ setCancelError(showError(e));
3032
+ }
3033
+ };
2817
3034
  const shell = (children) => /* @__PURE__ */ jsx16(Shell, { appearance, className, children: /* @__PURE__ */ jsxs15("div", { className: "kerne-stack", children: [
2818
3035
  heading ? /* @__PURE__ */ jsx16("h2", { className: "kerne-section-title", children: heading }) : null,
2819
3036
  children
@@ -2831,7 +3048,7 @@ function SubscriptionCard({
2831
3048
  }
2832
3049
  if (!subscription) {
2833
3050
  return shell(
2834
- /* @__PURE__ */ jsxs15(Fragment8, { children: [
3051
+ /* @__PURE__ */ jsxs15(Fragment9, { children: [
2835
3052
  /* @__PURE__ */ jsx16("p", { className: "kerne-subsection-desc", children: l10n.billing.freePlan }),
2836
3053
  upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx16(
2837
3054
  Button,
@@ -2850,7 +3067,7 @@ function SubscriptionCard({
2850
3067
  const renews = formatDate(current_period_end);
2851
3068
  const trialEnds = formatDate(trial_end);
2852
3069
  return shell(
2853
- /* @__PURE__ */ jsxs15(Fragment8, { children: [
3070
+ /* @__PURE__ */ jsxs15(Fragment9, { children: [
2854
3071
  portalError ? /* @__PURE__ */ jsx16(Panel, { variant: "danger", children: portalError }) : null,
2855
3072
  /* @__PURE__ */ jsxs15("dl", { className: "kerne-rows", children: [
2856
3073
  /* @__PURE__ */ jsxs15("div", { className: "kerne-row", children: [
@@ -2879,13 +3096,33 @@ function SubscriptionCard({
2879
3096
  ] }) : null
2880
3097
  ] }),
2881
3098
  canceled_at ? /* @__PURE__ */ jsx16(Panel, { children: l10n.billing.cancelNotice }) : null,
2882
- !hidePortalButton ? /* @__PURE__ */ jsx16(Button, { variant: "outline", type: "button", onClick: handlePortal, loading: opening, children: opening ? l10n.billing.opening : l10n.billing.managePortal }) : null
3099
+ cancelError ? /* @__PURE__ */ jsx16(Panel, { variant: "danger", children: cancelError }) : null,
3100
+ confirmingCancel ? /* @__PURE__ */ jsxs15(Panel, { children: [
3101
+ /* @__PURE__ */ jsx16("p", { children: l10n.billing.cancelConfirmTitle }),
3102
+ /* @__PURE__ */ jsx16("p", { className: "kerne-subsection-desc", children: l10n.billing.cancelConfirmBody }),
3103
+ /* @__PURE__ */ jsxs15("div", { className: "kerne-inline-actions", children: [
3104
+ /* @__PURE__ */ jsx16(
3105
+ Button,
3106
+ {
3107
+ variant: "outline",
3108
+ type: "button",
3109
+ onClick: () => setConfirmingCancel(false),
3110
+ disabled: isCanceling,
3111
+ children: l10n.billing.cancelConfirmDismiss
3112
+ }
3113
+ ),
3114
+ /* @__PURE__ */ jsx16(Button, { type: "button", onClick: handleCancel, loading: isCanceling, children: isCanceling ? l10n.billing.canceling : l10n.billing.cancelConfirmButton })
3115
+ ] })
3116
+ ] }) : /* @__PURE__ */ jsxs15("div", { className: "kerne-inline-actions", children: [
3117
+ !hidePortalButton ? /* @__PURE__ */ jsx16(Button, { variant: "outline", type: "button", onClick: handlePortal, loading: opening, children: opening ? l10n.billing.opening : l10n.billing.managePortal }) : null,
3118
+ !hideCancelButton && !canceled_at && status !== "CANCELED" ? /* @__PURE__ */ jsx16(Button, { variant: "outline", type: "button", onClick: () => setConfirmingCancel(true), children: l10n.billing.cancelSubscription }) : null
3119
+ ] })
2883
3120
  ] })
2884
3121
  );
2885
3122
  }
2886
3123
 
2887
3124
  // src/ui/profile/BillingSection.tsx
2888
- import { Fragment as Fragment9, jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
3125
+ import { Fragment as Fragment10, jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
2889
3126
  function intervalsOf(plans) {
2890
3127
  const seen = [];
2891
3128
  for (const plan of plans) {
@@ -2906,31 +3143,37 @@ function BillingSection({
2906
3143
  usageFeatureKeys,
2907
3144
  usageLabels
2908
3145
  }) {
2909
- const { subscription } = useSubscription(productSlug);
3146
+ const { subscription, refetch: refetchSubscription } = useSubscription(productSlug);
2910
3147
  const { plans, isLoading: plansLoading } = usePlans(pricingProductIdOrSlug);
2911
- const { openCheckout } = useCheckout();
2912
3148
  const l10n = useLocalization();
2913
3149
  const showError = useErrorResolver();
2914
- const [busy, setBusy] = useState17(null);
2915
- const [error, setError] = useState17(null);
3150
+ const {
3151
+ pending: pendingPlan,
3152
+ switching,
3153
+ switchError,
3154
+ checkoutPriceId: busy,
3155
+ checkoutError,
3156
+ selectPlan,
3157
+ confirmSwitch,
3158
+ dismissSwitch
3159
+ } = usePlanSwitch(subscription, refetchSubscription, {
3160
+ successUrl: checkoutSuccessUrl,
3161
+ cancelUrl: checkoutCancelUrl
3162
+ });
3163
+ const error = checkoutError ?? switchError;
2916
3164
  const intervals = useMemo(() => intervalsOf(plans), [plans]);
2917
3165
  const [interval, setInterval] = useState17(null);
2918
3166
  const activeInterval = interval ?? subscription?.plan_price.interval ?? intervals[0] ?? null;
2919
- const handleSwitch = async (planPriceId) => {
2920
- setError(null);
2921
- setBusy(planPriceId);
2922
- try {
2923
- await openCheckout(planPriceId, {
2924
- successUrl: checkoutSuccessUrl,
2925
- cancelUrl: checkoutCancelUrl
2926
- });
2927
- } catch (e) {
2928
- setError(showError(e));
2929
- setBusy(null);
2930
- }
2931
- };
2932
- return /* @__PURE__ */ jsxs16(Fragment9, { children: [
2933
- error ? /* @__PURE__ */ jsx17(Panel, { variant: "danger", children: error }) : null,
3167
+ return /* @__PURE__ */ jsxs16(Fragment10, { children: [
3168
+ error ? /* @__PURE__ */ jsx17(Panel, { variant: "danger", children: showError(error) }) : null,
3169
+ pendingPlan ? /* @__PURE__ */ jsxs16(Panel, { children: [
3170
+ /* @__PURE__ */ jsx17("p", { children: fill(l10n.billing.switchConfirmTitle, { plan: pendingPlan.plan.name }) }),
3171
+ /* @__PURE__ */ jsx17("p", { className: "kerne-subsection-desc", children: l10n.billing.switchConfirmBody }),
3172
+ /* @__PURE__ */ jsxs16("div", { className: "kerne-inline-actions", children: [
3173
+ /* @__PURE__ */ jsx17(Button, { variant: "outline", type: "button", onClick: dismissSwitch, disabled: switching, children: l10n.billing.switchConfirmDismiss }),
3174
+ /* @__PURE__ */ jsx17(Button, { type: "button", onClick: confirmSwitch, loading: switching, children: switching ? l10n.billing.switching : l10n.billing.switchConfirmButton })
3175
+ ] })
3176
+ ] }) : null,
2934
3177
  /* @__PURE__ */ jsx17(Subsection, { title: l10n.billing.currentPlan, children: /* @__PURE__ */ jsx17(
2935
3178
  SubscriptionCard,
2936
3179
  {
@@ -2947,7 +3190,7 @@ function BillingSection({
2947
3190
  style: { height: 64 },
2948
3191
  "aria-label": l10n.billing.loadingPlans
2949
3192
  }
2950
- ) : plans.length === 0 ? /* @__PURE__ */ jsx17("p", { className: "kerne-subsection-desc", children: l10n.billing.noPlans }) : /* @__PURE__ */ jsxs16(Fragment9, { children: [
3193
+ ) : plans.length === 0 ? /* @__PURE__ */ jsx17("p", { className: "kerne-subsection-desc", children: l10n.billing.noPlans }) : /* @__PURE__ */ jsxs16(Fragment10, { children: [
2951
3194
  intervals.length > 1 ? /* @__PURE__ */ jsx17(
2952
3195
  "div",
2953
3196
  {
@@ -2988,7 +3231,7 @@ function BillingSection({
2988
3231
  {
2989
3232
  variant: "outline",
2990
3233
  type: "button",
2991
- onClick: () => handleSwitch(price.id),
3234
+ onClick: () => selectPlan(plan, price),
2992
3235
  loading: busy === price.id,
2993
3236
  disabled: busy !== null,
2994
3237
  children: busy === price.id ? l10n.billing.opening : l10n.billing.switchTo
@@ -3004,7 +3247,7 @@ function BillingSection({
3004
3247
  }
3005
3248
 
3006
3249
  // src/ui/UserProfile.tsx
3007
- import { Fragment as Fragment10, jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
3250
+ import { Fragment as Fragment11, jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
3008
3251
  function UserProfile({
3009
3252
  tabs = ["profile", "security", "billing"],
3010
3253
  extraTabs = [],
@@ -3099,7 +3342,7 @@ function UserProfile({
3099
3342
  tab.id
3100
3343
  )) }),
3101
3344
  /* @__PURE__ */ jsxs17("div", { className: "kerne-pane", children: [
3102
- active ? /* @__PURE__ */ jsxs17(Fragment10, { children: [
3345
+ active ? /* @__PURE__ */ jsxs17(Fragment11, { children: [
3103
3346
  /* @__PURE__ */ jsx18("h2", { className: "kerne-pane-title", children: active.title }),
3104
3347
  active.description ? /* @__PURE__ */ jsx18("p", { className: "kerne-pane-desc", children: active.description }) : null
3105
3348
  ] }) : null,
@@ -3108,9 +3351,291 @@ function UserProfile({
3108
3351
  ] }) });
3109
3352
  }
3110
3353
 
3354
+ // src/ui/PricingTable.tsx
3355
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect7, useMemo as useMemo2, useRef as useRef4, useState as useState19 } from "react";
3356
+ import { Fragment as Fragment12, jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
3357
+ function formatEntitlement(e, unlimitedLabel) {
3358
+ if (e.feature_type === "BOOLEAN") return e.feature_name;
3359
+ if (e.feature_type === "STATIC")
3360
+ return e.value ? `${e.feature_name}: ${e.value}` : e.feature_name;
3361
+ if (e.limit === null) return e.feature_name;
3362
+ if (e.limit === -1) return `${unlimitedLabel} ${e.feature_name.toLowerCase()}`;
3363
+ return `${formatNumber(e.limit)} ${e.feature_name.toLowerCase()}`;
3364
+ }
3365
+ var PricingTableContext = createContext2(null);
3366
+ function usePricingTable() {
3367
+ const ctx = useContext2(PricingTableContext);
3368
+ if (!ctx) {
3369
+ throw new Error("usePricingTable() must be called inside <PricingTable>.");
3370
+ }
3371
+ return ctx;
3372
+ }
3373
+ function PricingTable({
3374
+ product,
3375
+ defaultLabel,
3376
+ checkoutSuccessUrl,
3377
+ checkoutCancelUrl,
3378
+ onSelectPlan,
3379
+ appearance,
3380
+ className,
3381
+ children
3382
+ }) {
3383
+ const { plans, isLoading, error } = usePlans(product);
3384
+ const { subscription, refetch: refetchSubscription } = useSubscription(product);
3385
+ const l10n = useLocalization();
3386
+ const showError = useErrorResolver();
3387
+ const [selectedLabelSlug, setSelectedLabelSlug] = useState19(defaultLabel ?? null);
3388
+ const {
3389
+ pending: pendingSwitch,
3390
+ switching,
3391
+ switchError,
3392
+ checkoutPriceId: busyPriceId,
3393
+ checkoutError,
3394
+ selectPlan,
3395
+ confirmSwitch,
3396
+ dismissSwitch
3397
+ } = usePlanSwitch(subscription, refetchSubscription, {
3398
+ successUrl: checkoutSuccessUrl,
3399
+ cancelUrl: checkoutCancelUrl
3400
+ });
3401
+ const labels = useMemo2(() => collectPriceLabels(plans), [plans]);
3402
+ const savings = useMemo2(() => resolveLabelSavings(plans, labels), [plans, labels]);
3403
+ useEffect7(() => {
3404
+ if (labels.length === 0) return;
3405
+ if (selectedLabelSlug && labels.some((l) => l.slug === selectedLabelSlug)) return;
3406
+ setSelectedLabelSlug(labels[0].slug);
3407
+ }, [labels]);
3408
+ const handleSelectPlan = (plan) => {
3409
+ const price = plan.prices.find((p) => p.label.slug === selectedLabelSlug) ?? plan.prices[0];
3410
+ if (!price) return;
3411
+ if (onSelectPlan) {
3412
+ onSelectPlan(plan, price);
3413
+ return;
3414
+ }
3415
+ void selectPlan(plan, price);
3416
+ };
3417
+ const ctx = {
3418
+ plans,
3419
+ labels,
3420
+ selectedLabelSlug,
3421
+ setSelectedLabelSlug,
3422
+ savings,
3423
+ currentPlanId: subscription?.plan?.id ?? null,
3424
+ busyPriceId,
3425
+ onSelectPlan: handleSelectPlan
3426
+ };
3427
+ return /* @__PURE__ */ jsx19(Shell, { appearance, className, size: "wide", children: isLoading ? /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-skeleton", "aria-busy": "true", "aria-label": l10n.pricing.loading, children: /* @__PURE__ */ jsx19("div", { className: "kerne-skeleton", style: { height: 320 } }) }) : error ? /* @__PURE__ */ jsx19(Panel, { variant: "danger", children: l10n.pricing.loadFailed }) : /* @__PURE__ */ jsxs18(PricingTableContext.Provider, { value: ctx, children: [
3428
+ checkoutError ? /* @__PURE__ */ jsx19(Panel, { variant: "danger", children: showError(checkoutError) }) : null,
3429
+ pendingSwitch ? /* @__PURE__ */ jsxs18(Panel, { children: [
3430
+ /* @__PURE__ */ jsx19("p", { children: fill(l10n.billing.switchConfirmTitle, { plan: pendingSwitch.plan.name }) }),
3431
+ /* @__PURE__ */ jsx19("p", { className: "kerne-subsection-desc", children: l10n.billing.switchConfirmBody }),
3432
+ switchError ? /* @__PURE__ */ jsx19(Panel, { variant: "danger", children: showError(switchError) }) : null,
3433
+ /* @__PURE__ */ jsxs18("div", { className: "kerne-inline-actions", children: [
3434
+ /* @__PURE__ */ jsx19(
3435
+ Button,
3436
+ {
3437
+ variant: "outline",
3438
+ type: "button",
3439
+ onClick: dismissSwitch,
3440
+ disabled: switching,
3441
+ children: l10n.billing.switchConfirmDismiss
3442
+ }
3443
+ ),
3444
+ /* @__PURE__ */ jsx19(Button, { type: "button", onClick: confirmSwitch, loading: switching, children: switching ? l10n.billing.switching : l10n.billing.switchConfirmButton })
3445
+ ] })
3446
+ ] }) : null,
3447
+ children
3448
+ ] }) });
3449
+ }
3450
+ function LabelSwitch() {
3451
+ const { labels, selectedLabelSlug, setSelectedLabelSlug, savings } = usePricingTable();
3452
+ const l10n = useLocalization();
3453
+ const containerRef = useRef4(null);
3454
+ const buttonRefs = useRef4(/* @__PURE__ */ new Map());
3455
+ const [thumb, setThumb] = useState19(null);
3456
+ useEffect7(() => {
3457
+ const measure = () => {
3458
+ const container = containerRef.current;
3459
+ const button = selectedLabelSlug ? buttonRefs.current.get(selectedLabelSlug) : null;
3460
+ if (!container || !button) return;
3461
+ const containerBox = container.getBoundingClientRect();
3462
+ const buttonBox = button.getBoundingClientRect();
3463
+ setThumb({ left: buttonBox.left - containerBox.left, width: buttonBox.width });
3464
+ };
3465
+ measure();
3466
+ window.addEventListener("resize", measure);
3467
+ return () => window.removeEventListener("resize", measure);
3468
+ }, [selectedLabelSlug, labels]);
3469
+ if (labels.length < 2) return null;
3470
+ return /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-switch-wrap", children: /* @__PURE__ */ jsxs18(
3471
+ "div",
3472
+ {
3473
+ ref: containerRef,
3474
+ className: "kerne-pricing-switch",
3475
+ role: "group",
3476
+ "aria-label": l10n.billing.intervalGroupLabel,
3477
+ children: [
3478
+ thumb ? /* @__PURE__ */ jsx19(
3479
+ "span",
3480
+ {
3481
+ className: "kerne-pricing-switch-thumb",
3482
+ style: { transform: `translateX(${thumb.left}px)`, width: `${thumb.width}px` },
3483
+ "aria-hidden": "true"
3484
+ }
3485
+ ) : null,
3486
+ labels.map((label) => /* @__PURE__ */ jsxs18(
3487
+ "button",
3488
+ {
3489
+ ref: (el) => {
3490
+ if (el) buttonRefs.current.set(label.slug, el);
3491
+ else buttonRefs.current.delete(label.slug);
3492
+ },
3493
+ type: "button",
3494
+ className: "kerne-pricing-switch-option",
3495
+ "aria-pressed": label.slug === selectedLabelSlug,
3496
+ onClick: () => setSelectedLabelSlug(label.slug),
3497
+ children: [
3498
+ label.name,
3499
+ savings && savings.labelSlug === label.slug ? /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-switch-save", children: fill(l10n.pricing.save, { percent: savings.percent }) }) : null
3500
+ ]
3501
+ },
3502
+ label.slug
3503
+ ))
3504
+ ]
3505
+ }
3506
+ ) });
3507
+ }
3508
+ function useAnimatedAmount(target, durationMs = 420) {
3509
+ const [displayed, setDisplayed] = useState19(target);
3510
+ const displayedRef = useRef4(target);
3511
+ const frameRef = useRef4();
3512
+ useEffect7(() => {
3513
+ if (displayedRef.current === target) return;
3514
+ const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
3515
+ if (reduceMotion) {
3516
+ displayedRef.current = target;
3517
+ setDisplayed(target);
3518
+ return;
3519
+ }
3520
+ const from = displayedRef.current;
3521
+ const start = performance.now();
3522
+ const tick = (now) => {
3523
+ const t = Math.min((now - start) / durationMs, 1);
3524
+ const eased = 1 - Math.pow(1 - t, 3);
3525
+ const value = Math.round(from + (target - from) * eased);
3526
+ displayedRef.current = value;
3527
+ setDisplayed(value);
3528
+ if (t < 1) frameRef.current = requestAnimationFrame(tick);
3529
+ };
3530
+ frameRef.current = requestAnimationFrame(tick);
3531
+ return () => {
3532
+ if (frameRef.current) cancelAnimationFrame(frameRef.current);
3533
+ };
3534
+ }, [target, durationMs]);
3535
+ return displayed;
3536
+ }
3537
+ function Plan({ slug, featured, badge, maxFeatures }) {
3538
+ const { plans, selectedLabelSlug, currentPlanId, busyPriceId, onSelectPlan } = usePricingTable();
3539
+ const l10n = useLocalization();
3540
+ const plan = plans.find((p) => p.slug === slug);
3541
+ useEffect7(() => {
3542
+ if (!plan && process.env.NODE_ENV === "development") {
3543
+ console.warn(
3544
+ `[Kerne] <PricingTable.Plan slug="${slug}"> - no plan with this slug on the loaded product.`
3545
+ );
3546
+ }
3547
+ }, [plan, slug]);
3548
+ const price = plan?.prices.find((p) => p.label.slug === selectedLabelSlug) ?? plan?.prices[0];
3549
+ const resolved = price ? resolvePriceDisplay(price, price.label.display_interval) : null;
3550
+ const animatedAmount = useAnimatedAmount(resolved?.displayAmount ?? 0);
3551
+ if (!plan) return null;
3552
+ const isCurrent = plan.id === currentPlanId;
3553
+ const isBusy = !!price && busyPriceId === price.id;
3554
+ const shownEntitlements = maxFeatures ? plan.entitlements.slice(0, maxFeatures) : plan.entitlements;
3555
+ const remaining = plan.entitlements.length - shownEntitlements.length;
3556
+ const billedCaption = resolved && resolved.displayInterval !== resolved.billedInterval ? fill(l10n.pricing.billedAt, {
3557
+ amount: formatMoney(resolved.billedAmount, price.currency),
3558
+ interval: formatInterval(resolved.billedInterval, l10n.billing)
3559
+ }) : null;
3560
+ return /* @__PURE__ */ jsxs18(
3561
+ "div",
3562
+ {
3563
+ className: "kerne-pricing-card",
3564
+ "data-featured": featured ? "true" : void 0,
3565
+ "data-current": isCurrent ? "true" : void 0,
3566
+ children: [
3567
+ /* @__PURE__ */ jsxs18("div", { className: "kerne-pricing-card-head", children: [
3568
+ badge ? /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-badge", children: badge }) : null,
3569
+ /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-card-name", children: plan.name }),
3570
+ plan.description ? /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-card-desc", children: plan.description }) : null
3571
+ ] }),
3572
+ /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-price", children: resolved ? /* @__PURE__ */ jsxs18(Fragment12, { children: [
3573
+ /* @__PURE__ */ jsxs18("div", { className: "kerne-pricing-price-row", children: [
3574
+ /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-amount", children: formatMoney(animatedAmount, price.currency) }),
3575
+ /* @__PURE__ */ jsxs18("span", { className: "kerne-pricing-interval", children: [
3576
+ "/ ",
3577
+ formatInterval(resolved.displayInterval, l10n.billing)
3578
+ ] })
3579
+ ] }),
3580
+ billedCaption ? /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-billed", children: billedCaption }) : null
3581
+ ] }) : /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-billed", children: l10n.billing.notOnInterval }) }),
3582
+ shownEntitlements.length > 0 ? /* @__PURE__ */ jsxs18("ul", { className: "kerne-pricing-features", children: [
3583
+ shownEntitlements.map((e) => /* @__PURE__ */ jsxs18("li", { className: "kerne-pricing-feature", children: [
3584
+ /* @__PURE__ */ jsx19(CheckIcon2, {}),
3585
+ /* @__PURE__ */ jsx19("span", { children: formatEntitlement(e, l10n.pricing.unlimited) })
3586
+ ] }, e.feature_key)),
3587
+ remaining > 0 ? /* @__PURE__ */ jsx19("li", { className: "kerne-pricing-feature-more", children: fill(l10n.pricing.moreFeatures, { count: remaining }) }) : null
3588
+ ] }) : null,
3589
+ /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-cta", children: isCurrent ? /* @__PURE__ */ jsx19(Button, { variant: "outline", type: "button", disabled: true, children: l10n.billing.current }) : /* @__PURE__ */ jsx19(
3590
+ Button,
3591
+ {
3592
+ variant: featured ? "primary" : "outline",
3593
+ type: "button",
3594
+ disabled: !price || busyPriceId !== null && !isBusy,
3595
+ loading: isBusy,
3596
+ onClick: () => onSelectPlan(plan),
3597
+ children: currentPlanId ? l10n.billing.switchTo : l10n.pricing.getStarted
3598
+ }
3599
+ ) })
3600
+ ]
3601
+ }
3602
+ );
3603
+ }
3604
+ function CheckIcon2() {
3605
+ return /* @__PURE__ */ jsx19(
3606
+ "svg",
3607
+ {
3608
+ width: "15",
3609
+ height: "15",
3610
+ viewBox: "0 0 24 24",
3611
+ fill: "none",
3612
+ stroke: "currentColor",
3613
+ strokeWidth: "2",
3614
+ "aria-hidden": "true",
3615
+ children: /* @__PURE__ */ jsx19("path", { d: "m5 13 4 4L19 7", strokeLinecap: "round", strokeLinejoin: "round" })
3616
+ }
3617
+ );
3618
+ }
3619
+ function Plans({ featuredSlug, featuredBadge, maxFeatures }) {
3620
+ const { plans } = usePricingTable();
3621
+ return /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-grid", children: plans.map((plan) => /* @__PURE__ */ jsx19(
3622
+ Plan,
3623
+ {
3624
+ slug: plan.slug,
3625
+ featured: plan.slug === featuredSlug,
3626
+ badge: plan.slug === featuredSlug ? featuredBadge : void 0,
3627
+ maxFeatures
3628
+ },
3629
+ plan.id
3630
+ )) });
3631
+ }
3632
+ PricingTable.LabelSwitch = LabelSwitch;
3633
+ PricingTable.Plan = Plan;
3634
+ PricingTable.Plans = Plans;
3635
+
3111
3636
  // src/ui/CheckoutResult.tsx
3112
- import { useEffect as useEffect7, useRef as useRef4, useState as useState19 } from "react";
3113
- import { jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
3637
+ import { useEffect as useEffect8, useRef as useRef5, useState as useState20 } from "react";
3638
+ import { jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
3114
3639
  var POLL_INTERVAL_MS = 1500;
3115
3640
  function CheckoutResult({
3116
3641
  outcome = "success",
@@ -3128,9 +3653,9 @@ function CheckoutResult({
3128
3653
  const client = useClient();
3129
3654
  const { config } = useAuthConfig();
3130
3655
  const l10n = useLocalization();
3131
- const [state, setState] = useState19("checking");
3132
- const confirmed = useRef4(false);
3133
- useEffect7(() => {
3656
+ const [state, setState] = useState20("checking");
3657
+ const confirmed = useRef5(false);
3658
+ useEffect8(() => {
3134
3659
  if (outcome !== "success") return;
3135
3660
  let stopped = false;
3136
3661
  const startedAt = Date.now();
@@ -3161,20 +3686,20 @@ function CheckoutResult({
3161
3686
  const go = (url, handler) => handler ?? (() => {
3162
3687
  if (url) window.location.href = url;
3163
3688
  });
3164
- const continueAction = continueUrl || onContinue ? /* @__PURE__ */ jsx19(Button, { type: "button", onClick: go(continueUrl, onContinue), children: l10n.common.continue }) : void 0;
3165
- const shell = (children) => /* @__PURE__ */ jsxs18(Shell, { appearance, className, children: [
3166
- logo ? /* @__PURE__ */ jsx19("div", { className: "kerne-masthead", children: logo }) : null,
3689
+ const continueAction = continueUrl || onContinue ? /* @__PURE__ */ jsx20(Button, { type: "button", onClick: go(continueUrl, onContinue), children: l10n.common.continue }) : void 0;
3690
+ const shell = (children) => /* @__PURE__ */ jsxs19(Shell, { appearance, className, children: [
3691
+ logo ? /* @__PURE__ */ jsx20("div", { className: "kerne-masthead", children: logo }) : null,
3167
3692
  children,
3168
- /* @__PURE__ */ jsx19(KerneBranding, { show: config?.show_kerne_branding ?? false })
3693
+ /* @__PURE__ */ jsx20(KerneBranding, { show: config?.show_kerne_branding ?? false })
3169
3694
  ] });
3170
3695
  if (outcome === "cancel") {
3171
3696
  return shell(
3172
- /* @__PURE__ */ jsx19(
3697
+ /* @__PURE__ */ jsx20(
3173
3698
  Status,
3174
3699
  {
3175
3700
  tone: "danger",
3176
3701
  title: l10n.checkout.cancelTitle,
3177
- action: pricingUrl || onRetry ? /* @__PURE__ */ jsx19(Button, { variant: "outline", type: "button", onClick: go(pricingUrl, onRetry), children: l10n.checkout.backToPlans }) : void 0,
3702
+ action: pricingUrl || onRetry ? /* @__PURE__ */ jsx20(Button, { variant: "outline", type: "button", onClick: go(pricingUrl, onRetry), children: l10n.checkout.backToPlans }) : void 0,
3178
3703
  children: l10n.checkout.cancelBody
3179
3704
  }
3180
3705
  )
@@ -3182,21 +3707,21 @@ function CheckoutResult({
3182
3707
  }
3183
3708
  if (state === "checking") {
3184
3709
  return shell(
3185
- /* @__PURE__ */ jsx19(Status, { tone: "loading", title: l10n.checkout.confirmingTitle, children: l10n.checkout.confirmingBody })
3710
+ /* @__PURE__ */ jsx20(Status, { tone: "loading", title: l10n.checkout.confirmingTitle, children: l10n.checkout.confirmingBody })
3186
3711
  );
3187
3712
  }
3188
3713
  if (state === "pending") {
3189
3714
  return shell(
3190
- /* @__PURE__ */ jsx19(Status, { tone: "success", title: l10n.checkout.pendingTitle, action: continueAction, children: l10n.checkout.pendingBody })
3715
+ /* @__PURE__ */ jsx20(Status, { tone: "success", title: l10n.checkout.pendingTitle, action: continueAction, children: l10n.checkout.pendingBody })
3191
3716
  );
3192
3717
  }
3193
3718
  return shell(
3194
- /* @__PURE__ */ jsx19(Status, { tone: "success", title: l10n.checkout.doneTitle, action: continueAction, children: l10n.checkout.doneBody })
3719
+ /* @__PURE__ */ jsx20(Status, { tone: "success", title: l10n.checkout.doneTitle, action: continueAction, children: l10n.checkout.doneBody })
3195
3720
  );
3196
3721
  }
3197
3722
 
3198
3723
  // src/ui/UpgradePrompt.tsx
3199
- import { Fragment as Fragment11, jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
3724
+ import { Fragment as Fragment13, jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
3200
3725
  function UpgradePrompt({
3201
3726
  featureKey,
3202
3727
  requested,
@@ -3215,22 +3740,25 @@ function UpgradePrompt({
3215
3740
  const l10n = useLocalization();
3216
3741
  if (isLoading) {
3217
3742
  if (hideWhileLoading) return null;
3218
- return /* @__PURE__ */ jsx20(Shell, { appearance, className, children: /* @__PURE__ */ jsx20("div", { className: "kerne-skeleton", style: { height: 80 }, "aria-label": l10n.upgrade.checking }) });
3743
+ return /* @__PURE__ */ jsx21(Shell, { appearance, className, children: /* @__PURE__ */ jsx21("div", { className: "kerne-skeleton", style: { height: 80 }, "aria-label": l10n.upgrade.checking }) });
3219
3744
  }
3220
- if (allowed) return /* @__PURE__ */ jsx20(Fragment11, { children });
3745
+ if (allowed) return /* @__PURE__ */ jsx21(Fragment13, { children });
3221
3746
  const name = featureName ?? humanizeFeatureKey(featureKey).toLowerCase();
3222
3747
  const quotaExhausted = details?.feature_type === "QUOTA" && typeof details.limit === "number" && typeof details.used === "number";
3223
3748
  const heading = title ?? fill(quotaExhausted ? l10n.upgrade.quotaTitle : l10n.upgrade.featureTitle, { feature: name });
3224
3749
  const body = description ?? (quotaExhausted ? fill(l10n.upgrade.quotaBody, {
3225
3750
  used: formatNumber(details.used),
3226
- limit: formatNumber(details.limit)
3751
+ // `access_limit` is where access actually stops; `limit` is only the
3752
+ // included allocation and can understate it past an overage ceiling.
3753
+ // Falls back to `limit` defensively - should never be needed here.
3754
+ limit: formatNumber(details.access_limit ?? details.limit)
3227
3755
  }) : fill(l10n.upgrade.featureBody, { feature: name }));
3228
- return /* @__PURE__ */ jsx20(Shell, { appearance, className, children: /* @__PURE__ */ jsxs19("div", { className: "kerne-status", children: [
3229
- /* @__PURE__ */ jsxs19("div", { className: "kerne-heading", children: [
3230
- /* @__PURE__ */ jsx20("h2", { className: "kerne-section-title", style: { fontSize: 17 }, children: heading }),
3231
- /* @__PURE__ */ jsx20("p", { className: "kerne-status-text", children: body })
3756
+ return /* @__PURE__ */ jsx21(Shell, { appearance, className, children: /* @__PURE__ */ jsxs20("div", { className: "kerne-status", children: [
3757
+ /* @__PURE__ */ jsxs20("div", { className: "kerne-heading", children: [
3758
+ /* @__PURE__ */ jsx21("h2", { className: "kerne-section-title", style: { fontSize: 17 }, children: heading }),
3759
+ /* @__PURE__ */ jsx21("p", { className: "kerne-status-text", children: body })
3232
3760
  ] }),
3233
- upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx20(
3761
+ upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx21(
3234
3762
  Button,
3235
3763
  {
3236
3764
  type: "button",
@@ -3251,6 +3779,7 @@ export {
3251
3779
  ForgotPasswordForm,
3252
3780
  LoginForm,
3253
3781
  MagicLinkCallback,
3782
+ PricingTable,
3254
3783
  ProfileSection,
3255
3784
  RegisterForm,
3256
3785
  ResetPasswordForm,
@@ -3263,6 +3792,15 @@ export {
3263
3792
  UserProfile,
3264
3793
  VerifyEmailForm,
3265
3794
  WaitlistForm,
3795
+ collectPriceLabels,
3266
3796
  defaultLocalization,
3267
- useLocalization
3797
+ formatDate,
3798
+ formatInterval,
3799
+ formatMoney,
3800
+ formatNumber,
3801
+ humanizeFeatureKey,
3802
+ resolveLabelSavings,
3803
+ resolvePriceDisplay,
3804
+ useLocalization,
3805
+ usePricingTable
3268
3806
  };