@nozle-js/react 0.5.1 → 0.5.3

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/README.md CHANGED
@@ -169,6 +169,92 @@ function AfterPayment({ planCode }: { planCode: string }) {
169
169
 
170
170
  Pre-built UI components with built-in styling. No CSS imports needed.
171
171
 
172
+ ### `PricingTable`
173
+
174
+ Renders plan cards in a responsive grid with monthly/annual toggle, current plan detection, and CSS variable theming. Auto-fetches plans from the API if none are provided.
175
+
176
+ ```tsx
177
+ import { PricingTable } from "@nozle-js/react";
178
+
179
+ // Auto-fetch plans from API
180
+ <PricingTable
181
+ customerId="cust_123"
182
+ highlightPlan="pro"
183
+ onSelect={(plan) => handleUpgrade(plan)}
184
+ />
185
+
186
+ // Or pass plans explicitly
187
+ <PricingTable
188
+ plans={[
189
+ { code: "starter", name: "Starter", amount_cents: 2900, amount_currency: "USD", interval: "monthly" },
190
+ { code: "pro", name: "Pro", amount_cents: 9900, amount_currency: "USD", interval: "monthly" },
191
+ ]}
192
+ features={[
193
+ ["10K API calls", "Email support"],
194
+ ["100K API calls", "Priority support", "Analytics"],
195
+ ]}
196
+ currentPlanCode="starter"
197
+ highlightPlan="pro"
198
+ onSelect={(plan) => console.log("Selected:", plan.code)}
199
+ showToggle={true}
200
+ enterpriseEmail="sales@example.com"
201
+ className="my-pricing"
202
+ />
203
+ ```
204
+
205
+ | Prop | Type | Default | Description |
206
+ |------|------|---------|-------------|
207
+ | `customerId` | `string` | -- | Customer ID for auto-detecting current plan |
208
+ | `currentPlanCode` | `string` | -- | Explicitly set current plan (overrides auto-detect) |
209
+ | `plans` | `PricingPlan[]` | -- | Plans to display (auto-fetched from API if omitted) |
210
+ | `features` | `string[][]` | -- | Feature lists per plan (index-matched to plans array) |
211
+ | `onSelect` | `(plan: PricingPlan) => void` | -- | Called when a plan's CTA is clicked |
212
+ | `highlightPlan` | `string` | -- | Plan code to highlight as "Most Popular" |
213
+ | `enterpriseEmail` | `string` | -- | Email for enterprise plan "Contact Sales" button |
214
+ | `showToggle` | `boolean` | `true` | Show monthly/annual toggle (only when annual plans exist) |
215
+ | `className` | `string` | -- | CSS class for the outer wrapper |
216
+
217
+ #### PricingPlan type
218
+
219
+ ```ts
220
+ interface PricingPlan {
221
+ code: string;
222
+ name: string;
223
+ amount_cents: number;
224
+ amount_currency: string;
225
+ interval: string;
226
+ description?: string;
227
+ }
228
+ ```
229
+
230
+ #### CSS Variable Theming
231
+
232
+ PricingTable uses CSS custom properties for full theming control. Set these on a parent element or `:root`:
233
+
234
+ ```css
235
+ :root {
236
+ /* Component-specific overrides */
237
+ --nozle-pricing-bg: #ffffff;
238
+ --nozle-pricing-card-bg: #ffffff;
239
+ --nozle-pricing-highlight: #6366f1;
240
+ --nozle-pricing-border: #e5e7eb;
241
+ --nozle-pricing-radius: 12px;
242
+
243
+ /* Or use global Nozle variables (PricingTable falls back to these) */
244
+ --nozle-background: #ffffff;
245
+ --nozle-card: #ffffff;
246
+ --nozle-primary: #6366f1;
247
+ --nozle-border: #e5e7eb;
248
+ --nozle-radius: 12px;
249
+ --nozle-foreground: #111827;
250
+ --nozle-muted-foreground: #6b7280;
251
+ --nozle-muted: #f3f4f6;
252
+ --nozle-primary-foreground: #ffffff;
253
+ }
254
+ ```
255
+
256
+ The component-specific variables (`--nozle-pricing-*`) take precedence over the global ones (`--nozle-*`), which fall back to sensible defaults.
257
+
172
258
  ### `UsageMeter`
173
259
 
174
260
  Displays usage with bar, ring, or minimal variants.
@@ -230,6 +316,8 @@ import type {
230
316
  UsePlanResult,
231
317
  UseCheckoutResult,
232
318
  Plan,
319
+ PricingPlan,
320
+ PricingTableProps,
233
321
  UsageMeterProps,
234
322
  PlanBadgeProps,
235
323
  UpgradePromptProps,
package/dist/index.cjs CHANGED
@@ -303,6 +303,335 @@ function BillingPortalProvider({
303
303
  }) {
304
304
  return /* @__PURE__ */ jsxRuntime.jsx(BillingPortalContext.Provider, { value: { customerId, apiKey }, children });
305
305
  }
306
+ var SYMBOLS = { USD: "$", EUR: "\u20AC", GBP: "\xA3", JPY: "\xA5" };
307
+ function fmtPrice(cents, cur) {
308
+ const s = SYMBOLS[cur] ?? cur + " ";
309
+ const val = cents % 100 === 0 ? String(cents / 100) : (cents / 100).toFixed(2);
310
+ return `${s}${val}`;
311
+ }
312
+ function isEnterprise(p) {
313
+ return /enterprise|custom/i.test(p.name + p.code);
314
+ }
315
+ function v(name, fb) {
316
+ return `var(${name}, ${fb})`;
317
+ }
318
+ var C = {
319
+ bg: v("--nozle-pricing-bg", v("--nozle-background", "#ffffff")),
320
+ cardBg: v("--nozle-pricing-card-bg", v("--nozle-card", "#ffffff")),
321
+ hl: v("--nozle-pricing-highlight", v("--nozle-primary", "#6366f1")),
322
+ border: v("--nozle-pricing-border", v("--nozle-border", "#e5e7eb")),
323
+ radius: v("--nozle-pricing-radius", v("--nozle-radius", "12px")),
324
+ text: v("--nozle-foreground", "#111827"),
325
+ muted: v("--nozle-muted-foreground", "#6b7280"),
326
+ mutedBg: v("--nozle-muted", "#f3f4f6"),
327
+ hlFg: v("--nozle-primary-foreground", "#ffffff")
328
+ };
329
+ function Check() {
330
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) });
331
+ }
332
+ function Toggle({ isAnnual, onChange }) {
333
+ const pill = {
334
+ display: "inline-flex",
335
+ borderRadius: "9999px",
336
+ padding: "4px",
337
+ background: C.mutedBg,
338
+ gap: "2px"
339
+ };
340
+ const tab = (active) => ({
341
+ padding: "8px 20px",
342
+ borderRadius: "9999px",
343
+ border: "none",
344
+ background: active ? C.cardBg : "transparent",
345
+ color: active ? C.text : C.muted,
346
+ fontWeight: active ? 600 : 400,
347
+ fontSize: "0.875rem",
348
+ cursor: "pointer",
349
+ boxShadow: active ? "0 1px 3px rgba(0,0,0,0.08)" : "none",
350
+ transition: "all 0.15s ease"
351
+ });
352
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "2.5rem" }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: pill, children: [
353
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => onChange(false), style: tab(!isAnnual), "aria-pressed": !isAnnual, children: "Monthly" }),
354
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { onClick: () => onChange(true), style: tab(isAnnual), "aria-pressed": isAnnual, children: [
355
+ "Annual",
356
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
357
+ marginLeft: "6px",
358
+ fontSize: "0.7rem",
359
+ fontWeight: 600,
360
+ color: isAnnual ? C.hl : C.muted
361
+ }, children: "Save 20%" })
362
+ ] })
363
+ ] }) });
364
+ }
365
+ function Card({
366
+ plan,
367
+ features,
368
+ isCurrent,
369
+ isHighlighted,
370
+ isEnt,
371
+ enterpriseEmail,
372
+ onSelect
373
+ }) {
374
+ const [hover, setHover] = React.useState(false);
375
+ const shadow = isHighlighted ? "0 8px 28px rgba(99,102,241,0.12), 0 2px 8px rgba(99,102,241,0.06)" : hover ? "0 8px 24px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.03)" : "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.03)";
376
+ const interval = plan.interval === "yearly" ? "/year" : "/month";
377
+ const handleClick = () => {
378
+ if (isCurrent) return;
379
+ if (isEnt && enterpriseEmail) {
380
+ window.location.href = `mailto:${enterpriseEmail}?subject=Enterprise Plan Inquiry`;
381
+ return;
382
+ }
383
+ onSelect?.();
384
+ };
385
+ return /* @__PURE__ */ jsxRuntime.jsxs(
386
+ "div",
387
+ {
388
+ onMouseEnter: () => setHover(true),
389
+ onMouseLeave: () => setHover(false),
390
+ style: {
391
+ position: "relative",
392
+ display: "flex",
393
+ flexDirection: "column",
394
+ background: C.cardBg,
395
+ border: `1px solid ${isHighlighted ? C.hl : C.border}`,
396
+ borderTopWidth: isHighlighted ? "3px" : "1px",
397
+ borderRadius: C.radius,
398
+ padding: "2rem",
399
+ transition: "box-shadow 0.2s ease, transform 0.2s ease",
400
+ boxShadow: shadow,
401
+ transform: hover && !isCurrent ? "translateY(-2px)" : "none"
402
+ },
403
+ children: [
404
+ isHighlighted && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
405
+ position: "absolute",
406
+ top: "-1px",
407
+ left: "50%",
408
+ transform: "translate(-50%, -50%)",
409
+ background: C.hl,
410
+ color: C.hlFg,
411
+ fontSize: "0.7rem",
412
+ fontWeight: 700,
413
+ padding: "4px 12px",
414
+ borderRadius: "9999px",
415
+ letterSpacing: "0.025em",
416
+ textTransform: "uppercase",
417
+ whiteSpace: "nowrap"
418
+ }, children: isCurrent ? "Current Plan" : "Most Popular" }),
419
+ isCurrent && !isHighlighted && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
420
+ position: "absolute",
421
+ top: "0",
422
+ left: "50%",
423
+ transform: "translate(-50%, -50%)",
424
+ background: C.mutedBg,
425
+ color: C.muted,
426
+ fontSize: "0.7rem",
427
+ fontWeight: 700,
428
+ padding: "4px 12px",
429
+ borderRadius: "9999px",
430
+ letterSpacing: "0.025em",
431
+ textTransform: "uppercase",
432
+ whiteSpace: "nowrap"
433
+ }, children: "Current Plan" }),
434
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "1.25rem", marginTop: isHighlighted || isCurrent ? "0.5rem" : 0 }, children: [
435
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { style: {
436
+ fontSize: "1.25rem",
437
+ fontWeight: 700,
438
+ color: C.text,
439
+ margin: 0,
440
+ lineHeight: 1.3
441
+ }, children: plan.name }),
442
+ plan.description && /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
443
+ fontSize: "0.875rem",
444
+ color: C.muted,
445
+ margin: "6px 0 0",
446
+ lineHeight: 1.5
447
+ }, children: plan.description })
448
+ ] }),
449
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginBottom: "1.5rem" }, children: isEnt ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "2.25rem", fontWeight: 700, color: C.text, lineHeight: 1 }, children: "Custom" }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: "2px" }, children: [
450
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "2.75rem", fontWeight: 700, color: C.text, lineHeight: 1, letterSpacing: "-0.025em" }, children: fmtPrice(plan.amount_cents, plan.amount_currency) }),
451
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.9375rem", color: C.muted, marginLeft: "4px" }, children: interval })
452
+ ] }) }),
453
+ features && features.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { style: {
454
+ listStyle: "none",
455
+ padding: 0,
456
+ margin: "0 0 2rem",
457
+ display: "flex",
458
+ flexDirection: "column",
459
+ gap: "0.75rem",
460
+ flex: 1
461
+ }, children: features.map((feat) => /* @__PURE__ */ jsxRuntime.jsxs("li", { style: {
462
+ display: "flex",
463
+ alignItems: "flex-start",
464
+ gap: "10px",
465
+ fontSize: "0.875rem",
466
+ color: C.text,
467
+ lineHeight: 1.5
468
+ }, children: [
469
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: C.hl, marginTop: "2px" }, children: /* @__PURE__ */ jsxRuntime.jsx(Check, {}) }),
470
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: feat })
471
+ ] }, feat)) }),
472
+ (!features || features.length === 0) && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 } }),
473
+ onSelect && /* @__PURE__ */ jsxRuntime.jsx(
474
+ CTAButton,
475
+ {
476
+ onClick: handleClick,
477
+ disabled: isCurrent,
478
+ variant: isCurrent ? "current" : isEnt ? "outline" : isHighlighted ? "primary" : "secondary",
479
+ children: isCurrent ? "Current Plan" : isEnt ? "Contact Sales" : `Get ${plan.name}`
480
+ }
481
+ )
482
+ ]
483
+ }
484
+ );
485
+ }
486
+ function CTAButton({
487
+ children,
488
+ onClick,
489
+ disabled,
490
+ variant
491
+ }) {
492
+ const [hover, setHover] = React.useState(false);
493
+ const base = {
494
+ width: "100%",
495
+ padding: "12px 24px",
496
+ borderRadius: "10px",
497
+ fontSize: "0.9375rem",
498
+ fontWeight: 600,
499
+ cursor: disabled ? "default" : "pointer",
500
+ border: "none",
501
+ transition: "all 0.15s ease",
502
+ lineHeight: 1.4
503
+ };
504
+ const variants = {
505
+ primary: {
506
+ background: hover && !disabled ? "#4f46e5" : C.hl,
507
+ color: C.hlFg
508
+ },
509
+ secondary: {
510
+ background: hover && !disabled ? "#f0f0f3" : C.mutedBg,
511
+ color: C.text,
512
+ border: `1px solid ${C.border}`
513
+ },
514
+ outline: {
515
+ background: hover && !disabled ? "rgba(99,102,241,0.04)" : "transparent",
516
+ color: C.hl,
517
+ border: `1.5px solid ${C.hl}`
518
+ },
519
+ current: {
520
+ background: C.mutedBg,
521
+ color: C.muted
522
+ }
523
+ };
524
+ return /* @__PURE__ */ jsxRuntime.jsx(
525
+ "button",
526
+ {
527
+ onClick: disabled ? void 0 : onClick,
528
+ disabled,
529
+ onMouseEnter: () => setHover(true),
530
+ onMouseLeave: () => setHover(false),
531
+ style: { ...base, ...variants[variant] },
532
+ children
533
+ }
534
+ );
535
+ }
536
+ function Skeleton() {
537
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
538
+ display: "grid",
539
+ gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
540
+ gap: "1.5rem"
541
+ }, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
542
+ background: C.cardBg,
543
+ border: `1px solid ${C.border}`,
544
+ borderRadius: C.radius,
545
+ padding: "2rem",
546
+ height: "380px"
547
+ }, children: [
548
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 20, width: "40%", borderRadius: 6, background: "#f0f0f0", marginBottom: 12 } }),
549
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 40, width: "50%", borderRadius: 6, background: "#f0f0f0", marginBottom: 24 } }),
550
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "80%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
551
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "70%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
552
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "60%", borderRadius: 6, background: "#f0f0f0" } })
553
+ ] }, i)) });
554
+ }
555
+ function PricingTable({
556
+ customerId,
557
+ currentPlanCode,
558
+ plans: propPlans,
559
+ features,
560
+ onSelect,
561
+ showToggle = true,
562
+ highlightPlan,
563
+ className,
564
+ enterpriseEmail
565
+ }) {
566
+ const client = useNozleClient();
567
+ const [plans, setPlans] = React.useState(propPlans ?? []);
568
+ const [currentPlan, setCurrentPlan] = React.useState(currentPlanCode ?? null);
569
+ const [isAnnual, setIsAnnual] = React.useState(false);
570
+ const [loading, setLoading] = React.useState(!propPlans);
571
+ React.useEffect(() => {
572
+ if (propPlans) {
573
+ setPlans(propPlans);
574
+ return;
575
+ }
576
+ let cancelled = false;
577
+ client.fetch("/api/v1/plans").then(async (res) => {
578
+ if (!cancelled && res.ok) {
579
+ const data = await res.json();
580
+ setPlans(data.plans || []);
581
+ }
582
+ }).finally(() => {
583
+ if (!cancelled) setLoading(false);
584
+ });
585
+ return () => {
586
+ cancelled = true;
587
+ };
588
+ }, [client, propPlans]);
589
+ React.useEffect(() => {
590
+ if (currentPlanCode !== void 0) {
591
+ setCurrentPlan(currentPlanCode);
592
+ return;
593
+ }
594
+ if (!customerId) return;
595
+ let cancelled = false;
596
+ client.fetch(`/api/v1/billing/status?customer_id=${encodeURIComponent(customerId)}`).then(async (res) => {
597
+ if (!cancelled && res.ok) {
598
+ const data = await res.json();
599
+ setCurrentPlan(data.plan ?? data.planName ?? null);
600
+ }
601
+ }).catch(() => {
602
+ });
603
+ return () => {
604
+ cancelled = true;
605
+ };
606
+ }, [client, customerId, currentPlanCode]);
607
+ const hasAnnual = plans.some((p) => p.interval === "yearly");
608
+ const filtered = showToggle && hasAnnual ? plans.filter((p) => p.interval === (isAnnual ? "yearly" : "monthly") || isEnterprise(p)) : plans;
609
+ if (loading) return /* @__PURE__ */ jsxRuntime.jsx(Skeleton, {});
610
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: {
611
+ fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
612
+ color: C.text,
613
+ background: C.bg
614
+ }, children: [
615
+ showToggle && hasAnnual && /* @__PURE__ */ jsxRuntime.jsx(Toggle, { isAnnual, onChange: setIsAnnual }),
616
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
617
+ display: "grid",
618
+ gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
619
+ gap: "1.5rem"
620
+ }, children: filtered.map((plan, i) => /* @__PURE__ */ jsxRuntime.jsx(
621
+ Card,
622
+ {
623
+ plan,
624
+ features: features?.[i],
625
+ isCurrent: currentPlan === plan.code,
626
+ isHighlighted: highlightPlan === plan.code,
627
+ isEnt: isEnterprise(plan),
628
+ enterpriseEmail,
629
+ onSelect: onSelect ? () => onSelect(plan) : void 0
630
+ },
631
+ plan.code
632
+ )) })
633
+ ] });
634
+ }
306
635
  function PlanCard({
307
636
  id,
308
637
  name,
@@ -447,91 +776,6 @@ function PlanCard({
447
776
  }
448
777
  );
449
778
  }
450
- function PricingTable({
451
- plans,
452
- currentPlanId,
453
- onSelect
454
- }) {
455
- const [isAnnual, setIsAnnual] = React.useState(false);
456
- return /* @__PURE__ */ jsxRuntime.jsxs(
457
- "div",
458
- {
459
- style: {
460
- background: "var(--nozle-background, var(--background))",
461
- color: "var(--nozle-foreground, var(--foreground))"
462
- },
463
- children: [
464
- /* @__PURE__ */ jsxRuntime.jsxs(
465
- "div",
466
- {
467
- style: {
468
- display: "flex",
469
- gap: "0.5rem",
470
- justifyContent: "center",
471
- marginBottom: "1.5rem"
472
- },
473
- children: [
474
- /* @__PURE__ */ jsxRuntime.jsx(
475
- "button",
476
- {
477
- onClick: () => setIsAnnual(false),
478
- style: {
479
- padding: "0.5rem 1rem",
480
- borderRadius: "var(--nozle-radius, 0.5rem)",
481
- border: "1px solid var(--nozle-border, var(--border))",
482
- background: !isAnnual ? "var(--nozle-primary, var(--primary))" : "transparent",
483
- color: !isAnnual ? "var(--nozle-primary-foreground, var(--primary-foreground))" : "var(--nozle-foreground, var(--foreground))",
484
- cursor: "pointer",
485
- fontWeight: !isAnnual ? 600 : 400
486
- },
487
- "aria-pressed": !isAnnual,
488
- children: "Monthly"
489
- }
490
- ),
491
- /* @__PURE__ */ jsxRuntime.jsx(
492
- "button",
493
- {
494
- onClick: () => setIsAnnual(true),
495
- style: {
496
- padding: "0.5rem 1rem",
497
- borderRadius: "var(--nozle-radius, 0.5rem)",
498
- border: "1px solid var(--nozle-border, var(--border))",
499
- background: isAnnual ? "var(--nozle-primary, var(--primary))" : "transparent",
500
- color: isAnnual ? "var(--nozle-primary-foreground, var(--primary-foreground))" : "var(--nozle-foreground, var(--foreground))",
501
- cursor: "pointer",
502
- fontWeight: isAnnual ? 600 : 400
503
- },
504
- "aria-pressed": isAnnual,
505
- children: "Annual"
506
- }
507
- )
508
- ]
509
- }
510
- ),
511
- /* @__PURE__ */ jsxRuntime.jsx(
512
- "div",
513
- {
514
- style: {
515
- display: "grid",
516
- gridTemplateColumns: `repeat(${Math.min(plans.length, 3)}, 1fr)`,
517
- gap: "1rem"
518
- },
519
- children: plans.map((plan) => /* @__PURE__ */ jsxRuntime.jsx(
520
- PlanCard,
521
- {
522
- ...plan,
523
- isAnnual,
524
- isCurrent: plan.id === currentPlanId,
525
- onSelect: onSelect ? () => onSelect(plan.id) : void 0
526
- },
527
- plan.id
528
- ))
529
- }
530
- )
531
- ]
532
- }
533
- );
534
- }
535
779
  function PlanComparison({
536
780
  plans,
537
781
  features