myoperator-ui 0.0.165 → 0.0.167-beta.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/index.js +1471 -129
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8566,158 +8566,1380 @@ import {
|
|
|
8566
8566
|
DialogTitle,
|
|
8567
8567
|
} from "../dialog";
|
|
8568
8568
|
import { PaymentOptionCard } from "./payment-option-card";
|
|
8569
|
-
import type {
|
|
8569
|
+
import type { PaymentOptionCardProps } from "./types";
|
|
8570
|
+
|
|
8571
|
+
interface PaymentOptionCardModalProps
|
|
8572
|
+
extends Omit<PaymentOptionCardProps, "onClose"> {
|
|
8573
|
+
open: boolean;
|
|
8574
|
+
onOpenChange: (open: boolean) => void;
|
|
8575
|
+
}
|
|
8576
|
+
|
|
8577
|
+
/**
|
|
8578
|
+
* PaymentOptionCardModal wraps the PaymentOptionCard in a centered Dialog overlay.
|
|
8579
|
+
* Use this when you want to show payment method selection as a modal popup rather
|
|
8580
|
+
* than an inline card.
|
|
8581
|
+
*
|
|
8582
|
+
* @example
|
|
8583
|
+
* \`\`\`tsx
|
|
8584
|
+
* const [open, setOpen] = useState(false);
|
|
8585
|
+
*
|
|
8586
|
+
* <PaymentOptionCardModal
|
|
8587
|
+
* open={open}
|
|
8588
|
+
* onOpenChange={setOpen}
|
|
8589
|
+
* options={paymentOptions}
|
|
8590
|
+
* onOptionSelect={(id) => console.log(id)}
|
|
8591
|
+
* onCtaClick={() => { handlePayment(); setOpen(false); }}
|
|
8592
|
+
* />
|
|
8593
|
+
* \`\`\`
|
|
8594
|
+
*/
|
|
8595
|
+
export const PaymentOptionCardModal = React.forwardRef<
|
|
8596
|
+
HTMLDivElement,
|
|
8597
|
+
PaymentOptionCardModalProps
|
|
8598
|
+
>(
|
|
8599
|
+
(
|
|
8600
|
+
{
|
|
8601
|
+
open,
|
|
8602
|
+
onOpenChange,
|
|
8603
|
+
title,
|
|
8604
|
+
subtitle,
|
|
8605
|
+
options,
|
|
8606
|
+
selectedOptionId,
|
|
8607
|
+
defaultSelectedOptionId,
|
|
8608
|
+
onOptionSelect,
|
|
8609
|
+
ctaText,
|
|
8610
|
+
onCtaClick,
|
|
8611
|
+
loading,
|
|
8612
|
+
disabled,
|
|
8613
|
+
className,
|
|
8614
|
+
},
|
|
8615
|
+
ref
|
|
8616
|
+
) => {
|
|
8617
|
+
const handleClose = () => {
|
|
8618
|
+
onOpenChange(false);
|
|
8619
|
+
};
|
|
8620
|
+
|
|
8621
|
+
return (
|
|
8622
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
8623
|
+
<DialogContent
|
|
8624
|
+
ref={ref}
|
|
8625
|
+
size="sm"
|
|
8626
|
+
hideCloseButton
|
|
8627
|
+
className="p-0 border-0 bg-transparent shadow-none"
|
|
8628
|
+
>
|
|
8629
|
+
{/* Visually hidden title for accessibility */}
|
|
8630
|
+
<DialogTitle className="sr-only">
|
|
8631
|
+
{title || "Select payment method"}
|
|
8632
|
+
</DialogTitle>
|
|
8633
|
+
<PaymentOptionCard
|
|
8634
|
+
title={title}
|
|
8635
|
+
subtitle={subtitle}
|
|
8636
|
+
options={options}
|
|
8637
|
+
selectedOptionId={selectedOptionId}
|
|
8638
|
+
defaultSelectedOptionId={defaultSelectedOptionId}
|
|
8639
|
+
onOptionSelect={onOptionSelect}
|
|
8640
|
+
ctaText={ctaText}
|
|
8641
|
+
onCtaClick={onCtaClick}
|
|
8642
|
+
onClose={handleClose}
|
|
8643
|
+
loading={loading}
|
|
8644
|
+
disabled={disabled}
|
|
8645
|
+
className={className}
|
|
8646
|
+
/>
|
|
8647
|
+
</DialogContent>
|
|
8648
|
+
</Dialog>
|
|
8649
|
+
);
|
|
8650
|
+
}
|
|
8651
|
+
);
|
|
8652
|
+
|
|
8653
|
+
PaymentOptionCardModal.displayName = "PaymentOptionCardModal";
|
|
8654
|
+
`, prefix)
|
|
8655
|
+
},
|
|
8656
|
+
{
|
|
8657
|
+
name: "types.ts",
|
|
8658
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
8659
|
+
|
|
8660
|
+
/**
|
|
8661
|
+
* A single payment option entry.
|
|
8662
|
+
*/
|
|
8663
|
+
export interface PaymentOption {
|
|
8664
|
+
/** Unique identifier for this option */
|
|
8665
|
+
id: string;
|
|
8666
|
+
/** Icon rendered inside a rounded container (e.g. an SVG or Lucide icon) */
|
|
8667
|
+
icon: React.ReactNode;
|
|
8668
|
+
/** Primary label (e.g. "Net banking") */
|
|
8669
|
+
title: string;
|
|
8670
|
+
/** Secondary description (e.g. "Pay securely through your bank") */
|
|
8671
|
+
description: string;
|
|
8672
|
+
}
|
|
8673
|
+
|
|
8674
|
+
/**
|
|
8675
|
+
* Props for the PaymentOptionCard component.
|
|
8676
|
+
*/
|
|
8677
|
+
export interface PaymentOptionCardProps {
|
|
8678
|
+
/** Header title */
|
|
8679
|
+
title?: string;
|
|
8680
|
+
/** Header subtitle */
|
|
8681
|
+
subtitle?: string;
|
|
8682
|
+
/** List of selectable payment options */
|
|
8683
|
+
options: PaymentOption[];
|
|
8684
|
+
/** Currently selected option id */
|
|
8685
|
+
selectedOptionId?: string;
|
|
8686
|
+
/** Default selected option id (uncontrolled mode) */
|
|
8687
|
+
defaultSelectedOptionId?: string;
|
|
8688
|
+
/** Callback fired when an option is selected */
|
|
8689
|
+
onOptionSelect?: (optionId: string) => void;
|
|
8690
|
+
/** CTA button text */
|
|
8691
|
+
ctaText?: string;
|
|
8692
|
+
/** Callback fired when CTA button is clicked */
|
|
8693
|
+
onCtaClick?: () => void;
|
|
8694
|
+
/** Callback fired when close button is clicked */
|
|
8695
|
+
onClose?: () => void;
|
|
8696
|
+
/** Whether the CTA button shows loading state */
|
|
8697
|
+
loading?: boolean;
|
|
8698
|
+
/** Whether the CTA button is disabled */
|
|
8699
|
+
disabled?: boolean;
|
|
8700
|
+
/** Additional className for the root element */
|
|
8701
|
+
className?: string;
|
|
8702
|
+
}
|
|
8703
|
+
`, prefix)
|
|
8704
|
+
},
|
|
8705
|
+
{
|
|
8706
|
+
name: "index.ts",
|
|
8707
|
+
content: prefixTailwindClasses(`export { PaymentOptionCard } from "./payment-option-card";
|
|
8708
|
+
export { PaymentOptionCardModal } from "./payment-option-card-modal";
|
|
8709
|
+
export type { PaymentOptionCardProps, PaymentOption } from "./types";
|
|
8710
|
+
`, prefix)
|
|
8711
|
+
}
|
|
8712
|
+
]
|
|
8713
|
+
},
|
|
8714
|
+
"let-us-drive-card": {
|
|
8715
|
+
name: "let-us-drive-card",
|
|
8716
|
+
description: "A managed service card with pricing, billing badge, 'Show details' link, and CTA for the full-service management section",
|
|
8717
|
+
category: "custom",
|
|
8718
|
+
dependencies: [
|
|
8719
|
+
"clsx",
|
|
8720
|
+
"tailwind-merge"
|
|
8721
|
+
],
|
|
8722
|
+
internalDependencies: [
|
|
8723
|
+
"button",
|
|
8724
|
+
"badge"
|
|
8725
|
+
],
|
|
8726
|
+
isMultiFile: true,
|
|
8727
|
+
directory: "let-us-drive-card",
|
|
8728
|
+
mainFile: "let-us-drive-card.tsx",
|
|
8729
|
+
files: [
|
|
8730
|
+
{
|
|
8731
|
+
name: "let-us-drive-card.tsx",
|
|
8732
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
8733
|
+
import { cn } from "../../../lib/utils";
|
|
8734
|
+
import { Button } from "../button";
|
|
8735
|
+
import { Badge } from "../badge";
|
|
8736
|
+
import type { LetUsDriveCardProps } from "./types";
|
|
8737
|
+
|
|
8738
|
+
/**
|
|
8739
|
+
* LetUsDriveCard displays a managed service offering with pricing, billing
|
|
8740
|
+
* frequency badge, and a CTA. Used in the "Let us drive \u2014 Full-service
|
|
8741
|
+
* management" section of the pricing page.
|
|
8742
|
+
*
|
|
8743
|
+
* Supports a "free/discount" state where the original price is shown with
|
|
8744
|
+
* strikethrough and a green label (e.g., "FREE") replaces it.
|
|
8745
|
+
*
|
|
8746
|
+
* @example
|
|
8747
|
+
* \`\`\`tsx
|
|
8748
|
+
* <LetUsDriveCard
|
|
8749
|
+
* title="Account Manager"
|
|
8750
|
+
* price="15,000"
|
|
8751
|
+
* period="/month"
|
|
8752
|
+
* billingBadge="Annually"
|
|
8753
|
+
* description="One expert who knows your business. And moves it forward."
|
|
8754
|
+
* onShowDetails={() => console.log("details")}
|
|
8755
|
+
* onCtaClick={() => console.log("talk")}
|
|
8756
|
+
* />
|
|
8757
|
+
* \`\`\`
|
|
8758
|
+
*/
|
|
8759
|
+
const LetUsDriveCard = React.forwardRef<HTMLDivElement, LetUsDriveCardProps>(
|
|
8760
|
+
(
|
|
8761
|
+
{
|
|
8762
|
+
title,
|
|
8763
|
+
price,
|
|
8764
|
+
period,
|
|
8765
|
+
startsAt = false,
|
|
8766
|
+
billingBadge,
|
|
8767
|
+
description,
|
|
8768
|
+
freeLabel,
|
|
8769
|
+
showDetailsLabel = "Show details",
|
|
8770
|
+
ctaLabel = "Talk to us",
|
|
8771
|
+
onShowDetails,
|
|
8772
|
+
onCtaClick,
|
|
8773
|
+
className,
|
|
8774
|
+
...props
|
|
8775
|
+
},
|
|
8776
|
+
ref
|
|
8777
|
+
) => {
|
|
8778
|
+
return (
|
|
8779
|
+
<div
|
|
8780
|
+
ref={ref}
|
|
8781
|
+
className={cn(
|
|
8782
|
+
"flex flex-col gap-6 rounded-[14px] border border-semantic-border-layout bg-card p-5",
|
|
8783
|
+
className
|
|
8784
|
+
)}
|
|
8785
|
+
{...props}
|
|
8786
|
+
>
|
|
8787
|
+
{/* Header: title + optional billing badge */}
|
|
8788
|
+
<div className="flex items-center justify-between">
|
|
8789
|
+
<h3 className="text-base font-semibold text-semantic-text-primary m-0">
|
|
8790
|
+
{title}
|
|
8791
|
+
</h3>
|
|
8792
|
+
{billingBadge && (
|
|
8793
|
+
<Badge
|
|
8794
|
+
size="sm"
|
|
8795
|
+
className="bg-semantic-info-surface text-semantic-info-primary font-normal"
|
|
8796
|
+
>
|
|
8797
|
+
{billingBadge}
|
|
8798
|
+
</Badge>
|
|
8799
|
+
)}
|
|
8800
|
+
</div>
|
|
8801
|
+
|
|
8802
|
+
{/* Price section */}
|
|
8803
|
+
<div className="flex flex-col gap-2.5">
|
|
8804
|
+
{startsAt && (
|
|
8805
|
+
<span className="text-xs text-semantic-text-muted tracking-[0.048px]">
|
|
8806
|
+
Starts at
|
|
8807
|
+
</span>
|
|
8808
|
+
)}
|
|
8809
|
+
<div className="flex gap-1 items-end">
|
|
8810
|
+
{freeLabel ? (
|
|
8811
|
+
<span className="text-[28px] font-semibold leading-[36px]">
|
|
8812
|
+
<span className="line-through text-semantic-text-muted">
|
|
8813
|
+
\u20B9{price}
|
|
8814
|
+
</span>{" "}
|
|
8815
|
+
<span className="text-semantic-success-primary">
|
|
8816
|
+
{freeLabel}
|
|
8817
|
+
</span>
|
|
8818
|
+
</span>
|
|
8819
|
+
) : (
|
|
8820
|
+
<span className="text-[28px] font-semibold leading-[36px] text-semantic-text-primary">
|
|
8821
|
+
\u20B9{price}
|
|
8822
|
+
</span>
|
|
8823
|
+
)}
|
|
8824
|
+
{period && (
|
|
8825
|
+
<span className="text-sm text-semantic-text-muted tracking-[0.035px]">
|
|
8826
|
+
{period}
|
|
8827
|
+
</span>
|
|
8828
|
+
)}
|
|
8829
|
+
</div>
|
|
8830
|
+
|
|
8831
|
+
{/* Description */}
|
|
8832
|
+
<p className="text-sm text-semantic-text-secondary tracking-[0.035px] m-0">
|
|
8833
|
+
{description}
|
|
8834
|
+
</p>
|
|
8835
|
+
</div>
|
|
8836
|
+
|
|
8837
|
+
{/* Actions: Show details link + CTA button */}
|
|
8838
|
+
<div className="flex flex-col gap-3 w-full">
|
|
8839
|
+
{onShowDetails && (
|
|
8840
|
+
<Button
|
|
8841
|
+
variant="link"
|
|
8842
|
+
className="text-semantic-text-link p-0 h-auto min-w-0 justify-start"
|
|
8843
|
+
onClick={onShowDetails}
|
|
8844
|
+
>
|
|
8845
|
+
{showDetailsLabel}
|
|
8846
|
+
</Button>
|
|
8847
|
+
)}
|
|
8848
|
+
<Button variant="outline" className="w-full" onClick={onCtaClick}>
|
|
8849
|
+
{ctaLabel}
|
|
8850
|
+
</Button>
|
|
8851
|
+
</div>
|
|
8852
|
+
</div>
|
|
8853
|
+
);
|
|
8854
|
+
}
|
|
8855
|
+
);
|
|
8856
|
+
|
|
8857
|
+
LetUsDriveCard.displayName = "LetUsDriveCard";
|
|
8858
|
+
|
|
8859
|
+
export { LetUsDriveCard };
|
|
8860
|
+
`, prefix)
|
|
8861
|
+
},
|
|
8862
|
+
{
|
|
8863
|
+
name: "types.ts",
|
|
8864
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
8865
|
+
|
|
8866
|
+
/**
|
|
8867
|
+
* Props for the LetUsDriveCard component.
|
|
8868
|
+
*/
|
|
8869
|
+
export interface LetUsDriveCardProps
|
|
8870
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
8871
|
+
/** Service title (e.g., "Dedicated Onboarding", "Account Manager") */
|
|
8872
|
+
title: string;
|
|
8873
|
+
/** Price amount as formatted string (e.g., "20,000", "15,000") */
|
|
8874
|
+
price: string;
|
|
8875
|
+
/** Billing period label (e.g., "/one-time fee", "/month") */
|
|
8876
|
+
period?: string;
|
|
8877
|
+
/** Show "Starts at" prefix above the price */
|
|
8878
|
+
startsAt?: boolean;
|
|
8879
|
+
/** Billing frequency badge text (e.g., "Annually", "Quarterly") */
|
|
8880
|
+
billingBadge?: string;
|
|
8881
|
+
/** Service description text */
|
|
8882
|
+
description: string;
|
|
8883
|
+
/** When provided, price is shown with strikethrough and this label (e.g., "FREE") is displayed in green */
|
|
8884
|
+
freeLabel?: string;
|
|
8885
|
+
/** Text for the details link (default: "Show details") */
|
|
8886
|
+
showDetailsLabel?: string;
|
|
8887
|
+
/** CTA button text (default: "Talk to us") */
|
|
8888
|
+
ctaLabel?: string;
|
|
8889
|
+
/** Callback when "Show details" link is clicked */
|
|
8890
|
+
onShowDetails?: () => void;
|
|
8891
|
+
/** Callback when CTA button is clicked */
|
|
8892
|
+
onCtaClick?: () => void;
|
|
8893
|
+
}
|
|
8894
|
+
`, prefix)
|
|
8895
|
+
},
|
|
8896
|
+
{
|
|
8897
|
+
name: "index.ts",
|
|
8898
|
+
content: prefixTailwindClasses(`export { LetUsDriveCard } from "./let-us-drive-card";
|
|
8899
|
+
export type { LetUsDriveCardProps } from "./types";
|
|
8900
|
+
`, prefix)
|
|
8901
|
+
}
|
|
8902
|
+
]
|
|
8903
|
+
},
|
|
8904
|
+
"power-up-card": {
|
|
8905
|
+
name: "power-up-card",
|
|
8906
|
+
description: "An add-on service card with icon, title, pricing, description, and CTA button for the power-ups section",
|
|
8907
|
+
category: "custom",
|
|
8908
|
+
dependencies: [
|
|
8909
|
+
"clsx",
|
|
8910
|
+
"tailwind-merge"
|
|
8911
|
+
],
|
|
8912
|
+
internalDependencies: [
|
|
8913
|
+
"button"
|
|
8914
|
+
],
|
|
8915
|
+
isMultiFile: true,
|
|
8916
|
+
directory: "power-up-card",
|
|
8917
|
+
mainFile: "power-up-card.tsx",
|
|
8918
|
+
files: [
|
|
8919
|
+
{
|
|
8920
|
+
name: "power-up-card.tsx",
|
|
8921
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
8922
|
+
import { cn } from "../../../lib/utils";
|
|
8923
|
+
import { Button } from "../button";
|
|
8924
|
+
import type { PowerUpCardProps } from "./types";
|
|
8925
|
+
|
|
8926
|
+
/**
|
|
8927
|
+
* PowerUpCard displays an add-on service with icon, pricing, description,
|
|
8928
|
+
* and a CTA button. Used in the "Power-ups and charges" section of
|
|
8929
|
+
* the pricing page.
|
|
8930
|
+
*
|
|
8931
|
+
* @example
|
|
8932
|
+
* \`\`\`tsx
|
|
8933
|
+
* <PowerUpCard
|
|
8934
|
+
* icon={<PhoneCall className="size-6" />}
|
|
8935
|
+
* title="Auto-Dialer"
|
|
8936
|
+
* price="Starts @ \u20B9700/user/month"
|
|
8937
|
+
* description="Available for SUV & Enterprise plans as an add-on per user."
|
|
8938
|
+
* onCtaClick={() => console.log("clicked")}
|
|
8939
|
+
* />
|
|
8940
|
+
* \`\`\`
|
|
8941
|
+
*/
|
|
8942
|
+
const PowerUpCard = React.forwardRef<HTMLDivElement, PowerUpCardProps>(
|
|
8943
|
+
(
|
|
8944
|
+
{
|
|
8945
|
+
icon,
|
|
8946
|
+
title,
|
|
8947
|
+
price,
|
|
8948
|
+
description,
|
|
8949
|
+
ctaLabel = "Talk to us",
|
|
8950
|
+
onCtaClick,
|
|
8951
|
+
className,
|
|
8952
|
+
...props
|
|
8953
|
+
},
|
|
8954
|
+
ref
|
|
8955
|
+
) => {
|
|
8956
|
+
return (
|
|
8957
|
+
<div
|
|
8958
|
+
ref={ref}
|
|
8959
|
+
className={cn(
|
|
8960
|
+
"flex flex-col justify-between gap-8 rounded-md border border-semantic-border-layout bg-card p-5",
|
|
8961
|
+
className
|
|
8962
|
+
)}
|
|
8963
|
+
{...props}
|
|
8964
|
+
>
|
|
8965
|
+
{/* Content */}
|
|
8966
|
+
<div className="flex flex-col gap-4">
|
|
8967
|
+
{/* Icon + title/price row */}
|
|
8968
|
+
<div className="flex gap-4 items-start">
|
|
8969
|
+
{icon && (
|
|
8970
|
+
<div className="flex items-center justify-center size-[47px] rounded bg-[var(--color-info-25)] shrink-0">
|
|
8971
|
+
{icon}
|
|
8972
|
+
</div>
|
|
8973
|
+
)}
|
|
8974
|
+
<div className="flex flex-col gap-2 min-w-0">
|
|
8975
|
+
<h3 className="text-base font-semibold text-semantic-text-primary m-0 leading-normal">
|
|
8976
|
+
{title}
|
|
8977
|
+
</h3>
|
|
8978
|
+
<p className="text-sm text-semantic-text-primary tracking-[0.035px] m-0 leading-normal">
|
|
8979
|
+
{price}
|
|
8980
|
+
</p>
|
|
8981
|
+
</div>
|
|
8982
|
+
</div>
|
|
8983
|
+
|
|
8984
|
+
{/* Description */}
|
|
8985
|
+
<p className="text-sm text-semantic-text-secondary tracking-[0.035px] m-0 leading-normal">
|
|
8986
|
+
{description}
|
|
8987
|
+
</p>
|
|
8988
|
+
</div>
|
|
8989
|
+
|
|
8990
|
+
{/* CTA */}
|
|
8991
|
+
<Button variant="outline" className="w-full" onClick={onCtaClick}>
|
|
8992
|
+
{ctaLabel}
|
|
8993
|
+
</Button>
|
|
8994
|
+
</div>
|
|
8995
|
+
);
|
|
8996
|
+
}
|
|
8997
|
+
);
|
|
8998
|
+
|
|
8999
|
+
PowerUpCard.displayName = "PowerUpCard";
|
|
9000
|
+
|
|
9001
|
+
export { PowerUpCard };
|
|
9002
|
+
`, prefix)
|
|
9003
|
+
},
|
|
9004
|
+
{
|
|
9005
|
+
name: "types.ts",
|
|
9006
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
9007
|
+
|
|
9008
|
+
/**
|
|
9009
|
+
* Props for the PowerUpCard component.
|
|
9010
|
+
*/
|
|
9011
|
+
export interface PowerUpCardProps
|
|
9012
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
9013
|
+
/** Icon or illustration displayed in the tinted container */
|
|
9014
|
+
icon?: React.ReactNode;
|
|
9015
|
+
/** Service title (e.g., "Truecaller business") */
|
|
9016
|
+
title: string;
|
|
9017
|
+
/** Pricing text (e.g., "Starts @ \u20B930,000/month") */
|
|
9018
|
+
price: string;
|
|
9019
|
+
/** Description explaining the service value */
|
|
9020
|
+
description: string;
|
|
9021
|
+
/** CTA button label (default: "Talk to us") */
|
|
9022
|
+
ctaLabel?: string;
|
|
9023
|
+
/** Callback when CTA button is clicked */
|
|
9024
|
+
onCtaClick?: () => void;
|
|
9025
|
+
}
|
|
9026
|
+
`, prefix)
|
|
9027
|
+
},
|
|
9028
|
+
{
|
|
9029
|
+
name: "index.ts",
|
|
9030
|
+
content: prefixTailwindClasses(`export { PowerUpCard } from "./power-up-card";
|
|
9031
|
+
export type { PowerUpCardProps } from "./types";
|
|
9032
|
+
`, prefix)
|
|
9033
|
+
}
|
|
9034
|
+
]
|
|
9035
|
+
},
|
|
9036
|
+
"pricing-card": {
|
|
9037
|
+
name: "pricing-card",
|
|
9038
|
+
description: "A pricing tier card with plan name, pricing, feature checklist, CTA button, and optional popularity badge and addon footer",
|
|
9039
|
+
category: "custom",
|
|
9040
|
+
dependencies: [
|
|
9041
|
+
"clsx",
|
|
9042
|
+
"tailwind-merge",
|
|
9043
|
+
"lucide-react"
|
|
9044
|
+
],
|
|
9045
|
+
internalDependencies: [
|
|
9046
|
+
"button",
|
|
9047
|
+
"badge"
|
|
9048
|
+
],
|
|
9049
|
+
isMultiFile: true,
|
|
9050
|
+
directory: "pricing-card",
|
|
9051
|
+
mainFile: "pricing-card.tsx",
|
|
9052
|
+
files: [
|
|
9053
|
+
{
|
|
9054
|
+
name: "pricing-card.tsx",
|
|
9055
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
9056
|
+
import { cn } from "../../../lib/utils";
|
|
9057
|
+
import { Button } from "../button";
|
|
9058
|
+
import { Badge } from "../badge";
|
|
9059
|
+
import { CircleCheck } from "lucide-react";
|
|
9060
|
+
import type { PricingCardProps } from "./types";
|
|
9061
|
+
|
|
9062
|
+
/**
|
|
9063
|
+
* PricingCard displays a plan tier with pricing, features, and a CTA button.
|
|
9064
|
+
* Supports current-plan state (outlined button), popularity badge, and an
|
|
9065
|
+
* optional add-on footer.
|
|
9066
|
+
*
|
|
9067
|
+
* @example
|
|
9068
|
+
* \`\`\`tsx
|
|
9069
|
+
* <PricingCard
|
|
9070
|
+
* planName="Compact"
|
|
9071
|
+
* price="2,5000"
|
|
9072
|
+
* planDetails="3 Users | 12 Month plan"
|
|
9073
|
+
* description="For small teams that need a WhatsApp-first plan"
|
|
9074
|
+
* headerBgColor="#d7eae9"
|
|
9075
|
+
* features={["WhatsApp Campaigns", "Missed Call Tracking"]}
|
|
9076
|
+
* onCtaClick={() => console.log("selected")}
|
|
9077
|
+
* onFeatureDetails={() => console.log("details")}
|
|
9078
|
+
* />
|
|
9079
|
+
* \`\`\`
|
|
9080
|
+
*/
|
|
9081
|
+
const PricingCard = React.forwardRef<HTMLDivElement, PricingCardProps>(
|
|
9082
|
+
(
|
|
9083
|
+
{
|
|
9084
|
+
planName,
|
|
9085
|
+
price,
|
|
9086
|
+
period = "/Month",
|
|
9087
|
+
planDetails,
|
|
9088
|
+
planIcon,
|
|
9089
|
+
description,
|
|
9090
|
+
headerBgColor,
|
|
9091
|
+
features = [],
|
|
9092
|
+
isCurrentPlan = false,
|
|
9093
|
+
showPopularBadge = false,
|
|
9094
|
+
badgeText = "MOST POPULAR",
|
|
9095
|
+
ctaText,
|
|
9096
|
+
onCtaClick,
|
|
9097
|
+
onFeatureDetails,
|
|
9098
|
+
addon,
|
|
9099
|
+
usageDetails,
|
|
9100
|
+
className,
|
|
9101
|
+
...props
|
|
9102
|
+
},
|
|
9103
|
+
ref
|
|
9104
|
+
) => {
|
|
9105
|
+
const buttonText =
|
|
9106
|
+
ctaText || (isCurrentPlan ? "Current plan" : "Select plan");
|
|
9107
|
+
|
|
9108
|
+
return (
|
|
9109
|
+
<div
|
|
9110
|
+
ref={ref}
|
|
9111
|
+
className={cn(
|
|
9112
|
+
"flex flex-col gap-6 rounded-t-xl rounded-b-lg border border-semantic-border-layout p-4",
|
|
9113
|
+
className
|
|
9114
|
+
)}
|
|
9115
|
+
{...props}
|
|
9116
|
+
>
|
|
9117
|
+
{/* Header */}
|
|
9118
|
+
<div
|
|
9119
|
+
className="flex flex-col gap-4 rounded-t-xl rounded-b-lg p-4"
|
|
9120
|
+
style={
|
|
9121
|
+
headerBgColor ? { backgroundColor: headerBgColor } : undefined
|
|
9122
|
+
}
|
|
9123
|
+
>
|
|
9124
|
+
{/* Plan name + badge */}
|
|
9125
|
+
<div className="flex items-center gap-4">
|
|
9126
|
+
<h3 className="text-xl font-semibold text-semantic-text-primary m-0">
|
|
9127
|
+
{planName}
|
|
9128
|
+
</h3>
|
|
9129
|
+
{showPopularBadge && (
|
|
9130
|
+
<Badge
|
|
9131
|
+
size="sm"
|
|
9132
|
+
className="bg-[#e3fdfe] text-[#119ba8] uppercase tracking-wider font-semibold"
|
|
9133
|
+
>
|
|
9134
|
+
{badgeText}
|
|
9135
|
+
</Badge>
|
|
9136
|
+
)}
|
|
9137
|
+
</div>
|
|
9138
|
+
|
|
9139
|
+
{/* Price */}
|
|
9140
|
+
<div className="flex flex-col gap-2.5">
|
|
9141
|
+
<div className="flex items-end gap-1">
|
|
9142
|
+
<span className="text-4xl leading-[44px] text-semantic-text-primary">
|
|
9143
|
+
\u20B9{price}
|
|
9144
|
+
</span>
|
|
9145
|
+
<span className="text-sm text-semantic-text-muted tracking-[0.035px]">
|
|
9146
|
+
{period}
|
|
9147
|
+
</span>
|
|
9148
|
+
</div>
|
|
9149
|
+
{planDetails && (
|
|
9150
|
+
<p className="text-sm tracking-[0.035px] text-semantic-text-primary m-0">
|
|
9151
|
+
{planDetails}
|
|
9152
|
+
</p>
|
|
9153
|
+
)}
|
|
9154
|
+
</div>
|
|
9155
|
+
|
|
9156
|
+
{/* Plan icon */}
|
|
9157
|
+
{planIcon && <div className="size-[30px]">{planIcon}</div>}
|
|
9158
|
+
|
|
9159
|
+
{/* Description */}
|
|
9160
|
+
{description && (
|
|
9161
|
+
<p className="text-sm text-semantic-text-secondary tracking-[0.035px] m-0">
|
|
9162
|
+
{description}
|
|
9163
|
+
</p>
|
|
9164
|
+
)}
|
|
9165
|
+
|
|
9166
|
+
{/* Feature details link + CTA */}
|
|
9167
|
+
<div className="flex flex-col gap-3.5 w-full">
|
|
9168
|
+
{onFeatureDetails && (
|
|
9169
|
+
<div className="flex justify-center">
|
|
9170
|
+
<Button
|
|
9171
|
+
variant="link"
|
|
9172
|
+
className="text-semantic-text-link p-0 h-auto min-w-0"
|
|
9173
|
+
onClick={onFeatureDetails}
|
|
9174
|
+
>
|
|
9175
|
+
Feature details
|
|
9176
|
+
</Button>
|
|
9177
|
+
</div>
|
|
9178
|
+
)}
|
|
9179
|
+
<Button
|
|
9180
|
+
variant={isCurrentPlan ? "outline" : "default"}
|
|
9181
|
+
className="w-full"
|
|
9182
|
+
onClick={onCtaClick}
|
|
9183
|
+
>
|
|
9184
|
+
{buttonText}
|
|
9185
|
+
</Button>
|
|
9186
|
+
</div>
|
|
9187
|
+
</div>
|
|
9188
|
+
|
|
9189
|
+
{/* Features */}
|
|
9190
|
+
{features.length > 0 && (
|
|
9191
|
+
<div className="flex flex-col gap-4">
|
|
9192
|
+
<p className="text-sm font-semibold text-semantic-text-primary tracking-[0.014px] uppercase m-0">
|
|
9193
|
+
Includes
|
|
9194
|
+
</p>
|
|
9195
|
+
<div className="flex flex-col gap-4">
|
|
9196
|
+
{features.map((feature, index) => {
|
|
9197
|
+
const text =
|
|
9198
|
+
typeof feature === "string" ? feature : feature.text;
|
|
9199
|
+
const isBold =
|
|
9200
|
+
typeof feature !== "string" && feature.bold;
|
|
9201
|
+
return (
|
|
9202
|
+
<div key={index} className="flex items-start gap-2">
|
|
9203
|
+
<CircleCheck className="size-[18px] text-semantic-text-secondary shrink-0 mt-0.5" />
|
|
9204
|
+
<span
|
|
9205
|
+
className={cn(
|
|
9206
|
+
"text-sm text-semantic-text-secondary tracking-[0.035px]",
|
|
9207
|
+
isBold && "font-semibold"
|
|
9208
|
+
)}
|
|
9209
|
+
>
|
|
9210
|
+
{text}
|
|
9211
|
+
</span>
|
|
9212
|
+
</div>
|
|
9213
|
+
);
|
|
9214
|
+
})}
|
|
9215
|
+
</div>
|
|
9216
|
+
</div>
|
|
9217
|
+
)}
|
|
9218
|
+
|
|
9219
|
+
{/* Addon */}
|
|
9220
|
+
{addon && (
|
|
9221
|
+
<div className="flex items-center gap-2.5 rounded-md bg-[var(--color-info-25)] border border-[#f3f5f6] pl-4 py-2.5">
|
|
9222
|
+
{addon.icon && (
|
|
9223
|
+
<div className="size-5 shrink-0">{addon.icon}</div>
|
|
9224
|
+
)}
|
|
9225
|
+
<span className="text-sm text-semantic-text-primary tracking-[0.035px]">
|
|
9226
|
+
{addon.text}
|
|
9227
|
+
</span>
|
|
9228
|
+
</div>
|
|
9229
|
+
)}
|
|
9230
|
+
|
|
9231
|
+
{/* Usage Details */}
|
|
9232
|
+
{usageDetails && usageDetails.length > 0 && (
|
|
9233
|
+
<div className="flex flex-col gap-2.5 rounded-md bg-[var(--color-info-25)] border border-[#f3f5f6] px-4 py-2.5">
|
|
9234
|
+
{usageDetails.map((detail, index) => (
|
|
9235
|
+
<div key={index} className="flex items-start gap-2">
|
|
9236
|
+
<span className="size-1.5 rounded-full bg-semantic-primary shrink-0 mt-[7px]" />
|
|
9237
|
+
<span className="text-sm text-semantic-text-primary tracking-[0.035px]">
|
|
9238
|
+
<strong>{detail.label}:</strong> {detail.value}
|
|
9239
|
+
</span>
|
|
9240
|
+
</div>
|
|
9241
|
+
))}
|
|
9242
|
+
</div>
|
|
9243
|
+
)}
|
|
9244
|
+
</div>
|
|
9245
|
+
);
|
|
9246
|
+
}
|
|
9247
|
+
);
|
|
9248
|
+
|
|
9249
|
+
PricingCard.displayName = "PricingCard";
|
|
9250
|
+
|
|
9251
|
+
export { PricingCard };
|
|
9252
|
+
`, prefix)
|
|
9253
|
+
},
|
|
9254
|
+
{
|
|
9255
|
+
name: "plan-icons.tsx",
|
|
9256
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
9257
|
+
|
|
9258
|
+
interface PlanIconProps extends React.SVGAttributes<SVGElement> {
|
|
9259
|
+
className?: string;
|
|
9260
|
+
}
|
|
9261
|
+
|
|
9262
|
+
const CompactCarIcon = React.forwardRef<SVGSVGElement, PlanIconProps>(
|
|
9263
|
+
({ className, ...props }, ref) => (
|
|
9264
|
+
<svg
|
|
9265
|
+
ref={ref}
|
|
9266
|
+
className={className}
|
|
9267
|
+
viewBox="0 0 30 19"
|
|
9268
|
+
fill="none"
|
|
9269
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9270
|
+
{...props}
|
|
9271
|
+
>
|
|
9272
|
+
<ellipse cx="25.2" cy="14.72" rx="3.33" ry="3.03" fill="white" />
|
|
9273
|
+
<path
|
|
9274
|
+
d="M25.12 11.21c-1.95 0-3.5 1.56-3.5 3.5 0 1.95 1.55 3.5 3.5 3.5 1.94 0 3.5-1.55 3.5-3.5 0-1.94-1.56-3.5-3.5-3.5zm0 5.45c-1.09 0-2.02-.93-2.02-2.02s.93-2.02 2.02-2.02 2.02.93 2.02 2.02-.93 2.02-2.02 2.02z"
|
|
9275
|
+
stroke="#2BBAC8"
|
|
9276
|
+
strokeLinejoin="round"
|
|
9277
|
+
/>
|
|
9278
|
+
<ellipse cx="4.09" cy="14.72" rx="3.33" ry="3.03" fill="white" />
|
|
9279
|
+
<path
|
|
9280
|
+
d="M4.26 11.21c-1.95 0-3.5 1.56-3.5 3.5 0 1.95 1.55 3.5 3.5 3.5 1.94 0 3.5-1.55 3.5-3.5 0-1.94-1.56-3.5-3.5-3.5zm0 5.45c-1.09 0-2.02-.93-2.02-2.02s.93-2.02 2.02-2.02 2.02.93 2.02 2.02-.93 2.02-2.02 2.02z"
|
|
9281
|
+
stroke="#2BBAC8"
|
|
9282
|
+
strokeLinejoin="round"
|
|
9283
|
+
/>
|
|
9284
|
+
<path
|
|
9285
|
+
d="M28.85 12.38c-.08-.16-.31-.31-.39-.47-.16-.39-.16-1.09-.23-1.48-.31-1.17-1.17-2.02-2.02-2.72-1.64-1.25-3.66-2.57-5.45-3.74C18 2.11 15.85.78 12.35.63c-1.79-.08-4.51 0-6.23.23-.15 0-1.4.31-1.24.62 1.25.23.55.93.24 1.63-.31.62-1.09 2.49-1.64 2.8-.15 0-.23.08-.31 0-.23-.31.16-1.4.31-1.71.16-.47.86-1.4.93-1.79 0-.31 0-.7-.39-.62L2.62 4.75c-.62 1.17-.62 2.18-.78 3.42-.15 1.56-1.09 2.88-1.24 4.36 0 .16 0 .39 0 .54.08.31.23.31.47.08.54-1.17 1.71-2.02 3.11-2.02 1.4 0 3.5 1.56 3.5 3.5s-.08 1.86-.23 2.25l.85-.08h11.75c.78-.08 1.4-.47 1.56-1.24 0-1.79 1.56-3.35 3.5-3.35 1.95 0 3.5 1.56 3.5 3.5 0 1.95-.16.93 0 1.09 1.09-.54.86-2.57.23-3.35v-.08z"
|
|
9286
|
+
fill="white"
|
|
9287
|
+
stroke="currentColor"
|
|
9288
|
+
strokeWidth="1.2"
|
|
9289
|
+
strokeLinejoin="round"
|
|
9290
|
+
/>
|
|
9291
|
+
<path
|
|
9292
|
+
d="M10.02 1.41c3.81-.23 8.56 1.4 11.44 3.89 2.88 2.49 1.79 1.64.16 1.79-3.58-.31-7.16-.62-10.74-.93-.86 0-2.65 0-3.27-.47-.62-.47-.54-1.87-.23-2.72.54-1.25 1.4-1.48 2.64-1.56z"
|
|
9293
|
+
stroke="currentColor"
|
|
9294
|
+
strokeWidth="1.2"
|
|
9295
|
+
strokeLinejoin="round"
|
|
9296
|
+
/>
|
|
9297
|
+
</svg>
|
|
9298
|
+
)
|
|
9299
|
+
);
|
|
9300
|
+
CompactCarIcon.displayName = "CompactCarIcon";
|
|
9301
|
+
|
|
9302
|
+
const SedanCarIcon = React.forwardRef<SVGSVGElement, PlanIconProps>(
|
|
9303
|
+
({ className, ...props }, ref) => (
|
|
9304
|
+
<svg
|
|
9305
|
+
ref={ref}
|
|
9306
|
+
className={className}
|
|
9307
|
+
viewBox="0 0 31 13"
|
|
9308
|
+
fill="none"
|
|
9309
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9310
|
+
{...props}
|
|
9311
|
+
>
|
|
9312
|
+
<ellipse cx="24.98" cy="9.51" rx="2.19" ry="2.56" fill="white" />
|
|
9313
|
+
<path
|
|
9314
|
+
d="M24.8 7.16c-1.33 0-2.38 1.09-2.38 2.45 0 1.37 1.05 2.46 2.38 2.46 1.33 0 2.38-1.09 2.38-2.46 0-1.36-1.05-2.45-2.38-2.45zm0 3.82c-.74 0-1.38-.66-1.38-1.42 0-.76.64-1.42 1.38-1.42.74 0 1.38.66 1.38 1.42 0 .76-.64 1.42-1.38 1.42z"
|
|
9315
|
+
stroke="#2BBAC8"
|
|
9316
|
+
strokeLinejoin="round"
|
|
9317
|
+
/>
|
|
9318
|
+
<ellipse cx="6.33" cy="9.51" rx="2.19" ry="2.56" fill="white" />
|
|
9319
|
+
<path
|
|
9320
|
+
d="M6.32 7.16c-1.32 0-2.38 1.09-2.38 2.45 0 1.37 1.06 2.46 2.38 2.46 1.33 0 2.39-1.09 2.39-2.46 0-1.36-1.06-2.45-2.39-2.45zm0 3.82c-.74 0-1.38-.66-1.38-1.42 0-.76.64-1.42 1.38-1.42.74 0 1.38.66 1.38 1.42 0 .76-.64 1.42-1.38 1.42z"
|
|
9321
|
+
stroke="#2BBAC8"
|
|
9322
|
+
strokeLinejoin="round"
|
|
9323
|
+
/>
|
|
9324
|
+
<path
|
|
9325
|
+
d="M29.7 7.79l-.24.81c0 .08-.16.4-.23.49-.24.16-1.97.57-2.05.49.24-1.22-.47-2.6-1.57-3 -2.05-.81-4.09 1.05-3.54 3.24H8.99v-.81c0-.32-.39-1.05-.55-1.3C7.03 5.6 4.27 6.33 3.8 8.6c-.47 2.27 0 .49 0 .49l-2.28-.41C.81 8.27.49 7.14.73 6.33c.23-.81.39-.57.39-.73.08-.49-.16-1.62.16-2.03.31-.4 1.97-.4 2.44-.57 1.42-.4 2.76-1.22 4.17-1.62 2.91-.89 6.61-1.05 9.53 0 2.91 1.05 3.7 1.95 5.51 2.51 1.81.57 4.09.65 5.83 1.62 1.73.97.62 1.05.93 1.78v.57z"
|
|
9326
|
+
fill="white"
|
|
9327
|
+
stroke="currentColor"
|
|
9328
|
+
strokeWidth="1.3"
|
|
9329
|
+
strokeLinejoin="round"
|
|
9330
|
+
/>
|
|
9331
|
+
<path
|
|
9332
|
+
d="M13.48 1.38l.63 2.6 4.8.16c0-.32 0-.64.32-.89-1.58-1.38-3.78-1.78-5.83-1.87h.08z"
|
|
9333
|
+
stroke="currentColor"
|
|
9334
|
+
strokeWidth="1.3"
|
|
9335
|
+
strokeLinejoin="round"
|
|
9336
|
+
/>
|
|
9337
|
+
<path
|
|
9338
|
+
d="M8.99 1.87s-.63.97-.63 1.05c0 .16.16.65.24.81l4.41.16-.39-2.51c-.87 0-1.81 0-2.68.16-.87.16-.87.16-.95.24v.09z"
|
|
9339
|
+
stroke="currentColor"
|
|
9340
|
+
strokeWidth="1.3"
|
|
9341
|
+
strokeLinejoin="round"
|
|
9342
|
+
/>
|
|
9343
|
+
<path
|
|
9344
|
+
d="M6.08 3.81h1.18l1.26-1.78c-.47.32-2.2.81-2.36 1.3-.16.49 0 .32 0 .49h-.08z"
|
|
9345
|
+
stroke="currentColor"
|
|
9346
|
+
strokeWidth="1.3"
|
|
9347
|
+
strokeLinejoin="round"
|
|
9348
|
+
/>
|
|
9349
|
+
</svg>
|
|
9350
|
+
)
|
|
9351
|
+
);
|
|
9352
|
+
SedanCarIcon.displayName = "SedanCarIcon";
|
|
9353
|
+
|
|
9354
|
+
const SuvCarIcon = React.forwardRef<SVGSVGElement, PlanIconProps>(
|
|
9355
|
+
({ className, ...props }, ref) => (
|
|
9356
|
+
<svg
|
|
9357
|
+
ref={ref}
|
|
9358
|
+
className={className}
|
|
9359
|
+
viewBox="0 0 32 15"
|
|
9360
|
+
fill="none"
|
|
9361
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9362
|
+
{...props}
|
|
9363
|
+
>
|
|
9364
|
+
<ellipse cx="25.57" cy="11.14" rx="2.65" ry="2.78" fill="white" />
|
|
9365
|
+
<ellipse cx="9.12" cy="11.14" rx="2.89" ry="2.78" fill="white" />
|
|
9366
|
+
<path
|
|
9367
|
+
d="M25.32 8.18c-1.61 0-2.9 1.3-2.9 2.94 0 1.63 1.29 2.93 2.9 2.93 1.62 0 2.9-1.3 2.9-2.93 0-1.64-1.28-2.94-2.9-2.94zm0 4.57c-.9 0-1.68-.78-1.68-1.7 0-.91.78-1.69 1.68-1.69.9 0 1.68.78 1.68 1.7 0 .91-.78 1.69-1.68 1.69z"
|
|
9368
|
+
stroke="#2BBAC8"
|
|
9369
|
+
strokeWidth="1.2"
|
|
9370
|
+
strokeLinejoin="round"
|
|
9371
|
+
/>
|
|
9372
|
+
<ellipse cx="9.14" cy="11.09" rx="1.4" ry="1.37" fill="white" />
|
|
9373
|
+
<path
|
|
9374
|
+
d="M8.96 8.18c-1.61 0-2.9 1.3-2.9 2.94 0 1.63 1.29 2.93 2.9 2.93 1.61 0 2.9-1.3 2.9-2.93 0-1.64-1.29-2.94-2.9-2.94zm0 4.57c-.9 0-1.68-.78-1.68-1.7 0-.91.78-1.69 1.68-1.69.9 0 1.68.78 1.68 1.7 0 .91-.78 1.69-1.68 1.69z"
|
|
9375
|
+
stroke="#2BBAC8"
|
|
9376
|
+
strokeWidth="1.2"
|
|
9377
|
+
strokeLinejoin="round"
|
|
9378
|
+
/>
|
|
9379
|
+
<path
|
|
9380
|
+
d="M30.6 10.78l-.26.99c-.3 1-.36.56-1.09.64.48-3.15-2.66-5.79-5.13-3.7-.43.37-1.18 1.52-1.18 2.1v1.5H12.06c.33-2.62-1.84-5.01-4.24-4.06-1.53.61-2.13 2.39-1.98 4.06-1.61-.14-3.18.68-3.39-1.7-.05-.6.07-1.21-.04-1.8-.65-.34-1.63.37-1.75-.77C.57 7.13.59 4.97.67 4.03c.03-.33.06-.79.43-.87.28-.06 1.83-.08 1.83.26v1.49l.29-.06c.67-1.75.59-3.97 2.76-4.15 3.76-.3 7.87.23 11.67.02 1.75.22 4.02 3.02 5.39 4.18 1.24.15 2.5.24 3.73.44.5.09 1.95.3 2.31.56.7.49.57 2.79.67 2.91.02.03.37.04.56.26.19.23.12.47.28.67v1.03z"
|
|
9381
|
+
fill="white"
|
|
9382
|
+
stroke="currentColor"
|
|
9383
|
+
strokeWidth="1.2"
|
|
9384
|
+
strokeLinejoin="round"
|
|
9385
|
+
/>
|
|
9386
|
+
<path
|
|
9387
|
+
d="M14.32 1.53c.25 1.41.16 2.98.61 4.32h6.22l.1-.21c-.48-1.34-1.41-2.72-2.51-3.53-.15-.11-.86-.59-.98-.59h-3.44z"
|
|
9388
|
+
stroke="currentColor"
|
|
9389
|
+
strokeWidth="1.2"
|
|
9390
|
+
strokeLinejoin="round"
|
|
9391
|
+
/>
|
|
9392
|
+
<path
|
|
9393
|
+
d="M9.71 1.53l-.19 4.32h4.33c-.17-1.3-.17-2.78-.38-4.06-.02-.12-.04-.2-.14-.26H9.71z"
|
|
9394
|
+
stroke="currentColor"
|
|
9395
|
+
strokeWidth="1.2"
|
|
9396
|
+
strokeLinejoin="round"
|
|
9397
|
+
/>
|
|
9398
|
+
<path
|
|
9399
|
+
d="M8.58 5.84l.29-4.07-.09-.31c-1.1.07-2.89-.32-3.46.95-.23.51-.74 2.67-.74 3.2 0 .12.01.13.1.21h3.91v-.01z"
|
|
9400
|
+
stroke="currentColor"
|
|
9401
|
+
strokeWidth="1.2"
|
|
9402
|
+
strokeLinejoin="round"
|
|
9403
|
+
/>
|
|
9404
|
+
</svg>
|
|
9405
|
+
)
|
|
9406
|
+
);
|
|
9407
|
+
SuvCarIcon.displayName = "SuvCarIcon";
|
|
9408
|
+
|
|
9409
|
+
export { CompactCarIcon, SedanCarIcon, SuvCarIcon };
|
|
9410
|
+
`, prefix)
|
|
9411
|
+
},
|
|
9412
|
+
{
|
|
9413
|
+
name: "types.ts",
|
|
9414
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
9415
|
+
|
|
9416
|
+
/**
|
|
9417
|
+
* Add-on info displayed at the bottom of the pricing card.
|
|
9418
|
+
*/
|
|
9419
|
+
export interface PricingCardAddon {
|
|
9420
|
+
/** Icon rendered in the addon section */
|
|
9421
|
+
icon?: React.ReactNode;
|
|
9422
|
+
/** Addon description text */
|
|
9423
|
+
text: string;
|
|
9424
|
+
}
|
|
9425
|
+
|
|
9426
|
+
/**
|
|
9427
|
+
* A single usage detail item (e.g., "Usage: Includes 2,000 AI conversations/month").
|
|
9428
|
+
*/
|
|
9429
|
+
export interface UsageDetail {
|
|
9430
|
+
/** Bold label (e.g., "Usage") */
|
|
9431
|
+
label: string;
|
|
9432
|
+
/** Value text (e.g., "Includes 2,000 AI conversations/month") */
|
|
9433
|
+
value: string;
|
|
9434
|
+
}
|
|
9435
|
+
|
|
9436
|
+
/**
|
|
9437
|
+
* A feature can be a plain string or an object with bold styling.
|
|
9438
|
+
*/
|
|
9439
|
+
export type PricingCardFeature = string | { text: string; bold?: boolean };
|
|
9440
|
+
|
|
9441
|
+
/**
|
|
9442
|
+
* Props for the PricingCard component.
|
|
9443
|
+
*/
|
|
9444
|
+
export interface PricingCardProps
|
|
9445
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
9446
|
+
/** Plan name displayed in the header (e.g., "Compact", "Sedan", "SUV") */
|
|
9447
|
+
planName: string;
|
|
9448
|
+
/** Price amount as formatted string (e.g., "2,5000") */
|
|
9449
|
+
price: string;
|
|
9450
|
+
/** Billing period label (default: "/Month") */
|
|
9451
|
+
period?: string;
|
|
9452
|
+
/** Plan detail line (e.g., "3 Users | 12 Month plan") */
|
|
9453
|
+
planDetails?: React.ReactNode;
|
|
9454
|
+
/** Plan icon or illustration */
|
|
9455
|
+
planIcon?: React.ReactNode;
|
|
9456
|
+
/** Plan description text */
|
|
9457
|
+
description?: string;
|
|
9458
|
+
/** Background color for the header section */
|
|
9459
|
+
headerBgColor?: string;
|
|
9460
|
+
/** List of included features shown with checkmarks. Supports bold items via object form. */
|
|
9461
|
+
features?: PricingCardFeature[];
|
|
9462
|
+
/** Whether this is the currently active plan (shows outlined button) */
|
|
9463
|
+
isCurrentPlan?: boolean;
|
|
9464
|
+
/** Show a popularity badge next to the plan name */
|
|
9465
|
+
showPopularBadge?: boolean;
|
|
9466
|
+
/** Custom badge text (defaults to "MOST POPULAR") */
|
|
9467
|
+
badgeText?: string;
|
|
9468
|
+
/** Custom CTA button text (overrides default "Select plan" / "Current plan") */
|
|
9469
|
+
ctaText?: string;
|
|
9470
|
+
/** Callback when CTA button is clicked */
|
|
9471
|
+
onCtaClick?: () => void;
|
|
9472
|
+
/** Callback when "Feature details" link is clicked */
|
|
9473
|
+
onFeatureDetails?: () => void;
|
|
9474
|
+
/** Add-on info displayed at the bottom of the card */
|
|
9475
|
+
addon?: PricingCardAddon;
|
|
9476
|
+
/** Usage details displayed in a bulleted list at the bottom (e.g., AIO plan) */
|
|
9477
|
+
usageDetails?: UsageDetail[];
|
|
9478
|
+
}
|
|
9479
|
+
`, prefix)
|
|
9480
|
+
},
|
|
9481
|
+
{
|
|
9482
|
+
name: "index.ts",
|
|
9483
|
+
content: prefixTailwindClasses(`export { PricingCard } from "./pricing-card";
|
|
9484
|
+
export { CompactCarIcon, SedanCarIcon, SuvCarIcon } from "./plan-icons";
|
|
9485
|
+
export type {
|
|
9486
|
+
PricingCardProps,
|
|
9487
|
+
PricingCardAddon,
|
|
9488
|
+
PricingCardFeature,
|
|
9489
|
+
UsageDetail,
|
|
9490
|
+
} from "./types";
|
|
9491
|
+
`, prefix)
|
|
9492
|
+
}
|
|
9493
|
+
]
|
|
9494
|
+
},
|
|
9495
|
+
"pricing-page": {
|
|
9496
|
+
name: "pricing-page",
|
|
9497
|
+
description: "A full pricing page layout composing plan-type tabs, billing toggle, pricing cards grid, power-ups section, and let-us-drive managed services section",
|
|
9498
|
+
category: "custom",
|
|
9499
|
+
dependencies: [
|
|
9500
|
+
"clsx",
|
|
9501
|
+
"tailwind-merge",
|
|
9502
|
+
"lucide-react"
|
|
9503
|
+
],
|
|
9504
|
+
internalDependencies: [
|
|
9505
|
+
"button",
|
|
9506
|
+
"page-header",
|
|
9507
|
+
"pricing-toggle",
|
|
9508
|
+
"pricing-card",
|
|
9509
|
+
"power-up-card",
|
|
9510
|
+
"let-us-drive-card"
|
|
9511
|
+
],
|
|
9512
|
+
isMultiFile: true,
|
|
9513
|
+
directory: "pricing-page",
|
|
9514
|
+
mainFile: "pricing-page.tsx",
|
|
9515
|
+
files: [
|
|
9516
|
+
{
|
|
9517
|
+
name: "pricing-page.tsx",
|
|
9518
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
9519
|
+
import { cn } from "../../../lib/utils";
|
|
9520
|
+
import { PageHeader } from "../page-header";
|
|
9521
|
+
import { Button } from "../button";
|
|
9522
|
+
import { PricingToggle } from "../pricing-toggle/pricing-toggle";
|
|
9523
|
+
import { PricingCard } from "../pricing-card/pricing-card";
|
|
9524
|
+
import { PowerUpCard } from "../power-up-card/power-up-card";
|
|
9525
|
+
import { LetUsDriveCard } from "../let-us-drive-card/let-us-drive-card";
|
|
9526
|
+
import { ExternalLink } from "lucide-react";
|
|
9527
|
+
import type { PricingPageProps } from "./types";
|
|
8570
9528
|
|
|
8571
9529
|
/**
|
|
8572
|
-
*
|
|
8573
|
-
*
|
|
8574
|
-
*
|
|
9530
|
+
* PricingPage composes all plan-selection sub-components into a full
|
|
9531
|
+
* page layout: header, plan-type tabs with billing toggle, pricing
|
|
9532
|
+
* cards grid, power-ups section, and let-us-drive section.
|
|
9533
|
+
*
|
|
9534
|
+
* Supports controlled or uncontrolled tab / billing state.
|
|
8575
9535
|
*
|
|
8576
9536
|
* @example
|
|
8577
9537
|
* \`\`\`tsx
|
|
8578
|
-
*
|
|
8579
|
-
*
|
|
8580
|
-
*
|
|
8581
|
-
*
|
|
8582
|
-
*
|
|
8583
|
-
*
|
|
8584
|
-
*
|
|
8585
|
-
*
|
|
9538
|
+
* <PricingPage
|
|
9539
|
+
* tabs={[
|
|
9540
|
+
* { label: "Team-Led Plans", value: "team" },
|
|
9541
|
+
* { label: "Go-AI First", value: "ai" },
|
|
9542
|
+
* ]}
|
|
9543
|
+
* planCards={compactCard, sedanCard, suvCard}
|
|
9544
|
+
* powerUpCards={[truecaller, tollFree, autoDialer]}
|
|
9545
|
+
* letUsDriveCards={[onboarding, accountMgr, managed]}
|
|
8586
9546
|
* />
|
|
8587
9547
|
* \`\`\`
|
|
8588
9548
|
*/
|
|
8589
|
-
|
|
8590
|
-
HTMLDivElement,
|
|
8591
|
-
PaymentOptionCardModalProps
|
|
8592
|
-
>(
|
|
9549
|
+
const PricingPage = React.forwardRef<HTMLDivElement, PricingPageProps>(
|
|
8593
9550
|
(
|
|
8594
9551
|
{
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
9552
|
+
title = "Select business plan",
|
|
9553
|
+
headerActions,
|
|
9554
|
+
tabs = [],
|
|
9555
|
+
activeTab: controlledTab,
|
|
9556
|
+
onTabChange,
|
|
9557
|
+
showBillingToggle = false,
|
|
9558
|
+
billingPeriod: controlledBilling,
|
|
9559
|
+
onBillingPeriodChange,
|
|
9560
|
+
planCards = [],
|
|
9561
|
+
powerUpCards = [],
|
|
9562
|
+
powerUpsTitle = "Power-ups and charges",
|
|
9563
|
+
featureComparisonText = "See full feature comparison",
|
|
9564
|
+
onFeatureComparisonClick,
|
|
9565
|
+
letUsDriveCards = [],
|
|
9566
|
+
letUsDriveTitle = "Let us drive \u2014 Full-service management",
|
|
8607
9567
|
className,
|
|
9568
|
+
...props
|
|
8608
9569
|
},
|
|
8609
9570
|
ref
|
|
8610
9571
|
) => {
|
|
8611
|
-
|
|
8612
|
-
|
|
9572
|
+
// Internal state for uncontrolled mode
|
|
9573
|
+
const [internalTab, setInternalTab] = React.useState(
|
|
9574
|
+
tabs[0]?.value ?? ""
|
|
9575
|
+
);
|
|
9576
|
+
const [internalBilling, setInternalBilling] = React.useState<
|
|
9577
|
+
"monthly" | "yearly"
|
|
9578
|
+
>("monthly");
|
|
9579
|
+
|
|
9580
|
+
const currentTab = controlledTab ?? internalTab;
|
|
9581
|
+
const currentBilling = controlledBilling ?? internalBilling;
|
|
9582
|
+
|
|
9583
|
+
const handleTabChange = (value: string) => {
|
|
9584
|
+
if (!controlledTab) setInternalTab(value);
|
|
9585
|
+
onTabChange?.(value);
|
|
8613
9586
|
};
|
|
8614
9587
|
|
|
9588
|
+
const handleBillingChange = (period: "monthly" | "yearly") => {
|
|
9589
|
+
if (!controlledBilling) setInternalBilling(period);
|
|
9590
|
+
onBillingPeriodChange?.(period);
|
|
9591
|
+
};
|
|
9592
|
+
|
|
9593
|
+
const hasPowerUps = powerUpCards.length > 0;
|
|
9594
|
+
const hasLetUsDrive = letUsDriveCards.length > 0;
|
|
9595
|
+
|
|
8615
9596
|
return (
|
|
8616
|
-
<
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
{
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
9597
|
+
<div
|
|
9598
|
+
ref={ref}
|
|
9599
|
+
className={cn("flex flex-col bg-card", className)}
|
|
9600
|
+
{...props}
|
|
9601
|
+
>
|
|
9602
|
+
{/* \u2500\u2500\u2500\u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500 */}
|
|
9603
|
+
<PageHeader
|
|
9604
|
+
title={title}
|
|
9605
|
+
actions={headerActions}
|
|
9606
|
+
layout="horizontal"
|
|
9607
|
+
/>
|
|
9608
|
+
|
|
9609
|
+
{/* \u2500\u2500\u2500\u2500\u2500 Plan Selection Area \u2500\u2500\u2500\u2500\u2500 */}
|
|
9610
|
+
<div className="flex flex-col gap-6 px-6 py-6">
|
|
9611
|
+
{/* Tabs + billing toggle */}
|
|
9612
|
+
{tabs.length > 0 && (
|
|
9613
|
+
<PricingToggle
|
|
9614
|
+
tabs={tabs}
|
|
9615
|
+
activeTab={currentTab}
|
|
9616
|
+
onTabChange={handleTabChange}
|
|
9617
|
+
showBillingToggle={showBillingToggle}
|
|
9618
|
+
billingPeriod={currentBilling}
|
|
9619
|
+
onBillingPeriodChange={handleBillingChange}
|
|
9620
|
+
/>
|
|
9621
|
+
)}
|
|
9622
|
+
|
|
9623
|
+
{/* Plan cards grid */}
|
|
9624
|
+
{planCards.length > 0 && (
|
|
9625
|
+
<div
|
|
9626
|
+
className={cn(
|
|
9627
|
+
"grid gap-6 justify-center",
|
|
9628
|
+
planCards.length <= 2
|
|
9629
|
+
? "grid-cols-1 md:grid-cols-2 max-w-[960px] mx-auto"
|
|
9630
|
+
: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
|
|
9631
|
+
)}
|
|
9632
|
+
>
|
|
9633
|
+
{planCards.map((cardProps, index) => (
|
|
9634
|
+
<PricingCard key={index} {...cardProps} />
|
|
9635
|
+
))}
|
|
9636
|
+
</div>
|
|
9637
|
+
)}
|
|
9638
|
+
</div>
|
|
9639
|
+
|
|
9640
|
+
{/* \u2500\u2500\u2500\u2500\u2500 Power-ups Section \u2500\u2500\u2500\u2500\u2500 */}
|
|
9641
|
+
{hasPowerUps && (
|
|
9642
|
+
<div className="bg-semantic-bg-ui px-6 py-[60px]">
|
|
9643
|
+
<div className="flex flex-col gap-4">
|
|
9644
|
+
{/* Section header */}
|
|
9645
|
+
<div className="flex items-center justify-between">
|
|
9646
|
+
<h2 className="text-lg font-semibold text-semantic-text-primary m-0">
|
|
9647
|
+
{powerUpsTitle}
|
|
9648
|
+
</h2>
|
|
9649
|
+
{onFeatureComparisonClick && (
|
|
9650
|
+
<Button
|
|
9651
|
+
variant="link"
|
|
9652
|
+
className="text-semantic-text-link p-0 h-auto min-w-0 gap-1"
|
|
9653
|
+
onClick={onFeatureComparisonClick}
|
|
9654
|
+
>
|
|
9655
|
+
{featureComparisonText}
|
|
9656
|
+
<ExternalLink className="size-3.5" />
|
|
9657
|
+
</Button>
|
|
9658
|
+
)}
|
|
9659
|
+
</div>
|
|
9660
|
+
|
|
9661
|
+
{/* Power-up cards */}
|
|
9662
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
9663
|
+
{powerUpCards.map((cardProps, index) => (
|
|
9664
|
+
<PowerUpCard key={index} {...cardProps} />
|
|
9665
|
+
))}
|
|
9666
|
+
</div>
|
|
9667
|
+
</div>
|
|
9668
|
+
</div>
|
|
9669
|
+
)}
|
|
9670
|
+
|
|
9671
|
+
{/* \u2500\u2500\u2500\u2500\u2500 Let Us Drive Section \u2500\u2500\u2500\u2500\u2500 */}
|
|
9672
|
+
{hasLetUsDrive && (
|
|
9673
|
+
<div className="bg-card px-6 py-[60px]">
|
|
9674
|
+
<div className="flex flex-col gap-4">
|
|
9675
|
+
{/* Section header */}
|
|
9676
|
+
<h2 className="text-lg font-semibold text-semantic-text-primary m-0">
|
|
9677
|
+
{letUsDriveTitle}
|
|
9678
|
+
</h2>
|
|
9679
|
+
|
|
9680
|
+
{/* Service cards */}
|
|
9681
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
9682
|
+
{letUsDriveCards.map((cardProps, index) => (
|
|
9683
|
+
<LetUsDriveCard key={index} {...cardProps} />
|
|
9684
|
+
))}
|
|
9685
|
+
</div>
|
|
9686
|
+
</div>
|
|
9687
|
+
</div>
|
|
9688
|
+
)}
|
|
9689
|
+
</div>
|
|
8643
9690
|
);
|
|
8644
9691
|
}
|
|
8645
9692
|
);
|
|
8646
9693
|
|
|
8647
|
-
|
|
9694
|
+
PricingPage.displayName = "PricingPage";
|
|
9695
|
+
|
|
9696
|
+
export { PricingPage };
|
|
8648
9697
|
`, prefix)
|
|
8649
9698
|
},
|
|
8650
9699
|
{
|
|
8651
9700
|
name: "types.ts",
|
|
8652
9701
|
content: prefixTailwindClasses(`import * as React from "react";
|
|
9702
|
+
import type { PricingCardProps } from "../pricing-card/types";
|
|
9703
|
+
import type { PowerUpCardProps } from "../power-up-card/types";
|
|
9704
|
+
import type { LetUsDriveCardProps } from "../let-us-drive-card/types";
|
|
9705
|
+
import type { PricingToggleTab } from "../pricing-toggle/types";
|
|
8653
9706
|
|
|
8654
|
-
|
|
8655
|
-
* A single payment option entry.
|
|
8656
|
-
*/
|
|
8657
|
-
export interface PaymentOption {
|
|
8658
|
-
/** Unique identifier for this option */
|
|
8659
|
-
id: string;
|
|
8660
|
-
/** Icon rendered inside a rounded container (e.g. an SVG or Lucide icon) */
|
|
8661
|
-
icon: React.ReactNode;
|
|
8662
|
-
/** Primary label (e.g. "Net banking") */
|
|
8663
|
-
title: string;
|
|
8664
|
-
/** Secondary description (e.g. "Pay securely through your bank") */
|
|
8665
|
-
description: string;
|
|
8666
|
-
}
|
|
9707
|
+
export type { PricingToggleTab };
|
|
8667
9708
|
|
|
8668
9709
|
/**
|
|
8669
|
-
* Props for the
|
|
9710
|
+
* Props for the PricingPage component.
|
|
9711
|
+
*
|
|
9712
|
+
* PricingPage is a layout compositor that orchestrates PricingToggle,
|
|
9713
|
+
* PricingCard, PowerUpCard, LetUsDriveCard, and PageHeader into
|
|
9714
|
+
* the full plan selection page.
|
|
8670
9715
|
*/
|
|
8671
|
-
export interface
|
|
8672
|
-
|
|
9716
|
+
export interface PricingPageProps
|
|
9717
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
9718
|
+
/* \u2500\u2500\u2500\u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500 */
|
|
9719
|
+
|
|
9720
|
+
/** Page title (default: "Select business plan") */
|
|
8673
9721
|
title?: string;
|
|
8674
|
-
/**
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
/**
|
|
8695
|
-
|
|
9722
|
+
/** Actions rendered on the right side of the header (e.g., number-type dropdown) */
|
|
9723
|
+
headerActions?: React.ReactNode;
|
|
9724
|
+
|
|
9725
|
+
/* \u2500\u2500\u2500\u2500\u2500 Tabs & Billing \u2500\u2500\u2500\u2500\u2500 */
|
|
9726
|
+
|
|
9727
|
+
/** Plan type tabs shown in the pill selector */
|
|
9728
|
+
tabs?: PricingToggleTab[];
|
|
9729
|
+
/** Currently active tab value (controlled). Falls back to first tab when unset. */
|
|
9730
|
+
activeTab?: string;
|
|
9731
|
+
/** Callback when the active tab changes */
|
|
9732
|
+
onTabChange?: (value: string) => void;
|
|
9733
|
+
/** Whether to show the monthly/yearly billing toggle */
|
|
9734
|
+
showBillingToggle?: boolean;
|
|
9735
|
+
/** Current billing period (controlled) */
|
|
9736
|
+
billingPeriod?: "monthly" | "yearly";
|
|
9737
|
+
/** Callback when the billing period changes */
|
|
9738
|
+
onBillingPeriodChange?: (period: "monthly" | "yearly") => void;
|
|
9739
|
+
|
|
9740
|
+
/* \u2500\u2500\u2500\u2500\u2500 Plan Cards \u2500\u2500\u2500\u2500\u2500 */
|
|
9741
|
+
|
|
9742
|
+
/** Array of plan card props to render in the main pricing grid */
|
|
9743
|
+
planCards?: PricingCardProps[];
|
|
9744
|
+
|
|
9745
|
+
/* \u2500\u2500\u2500\u2500\u2500 Power-ups Section \u2500\u2500\u2500\u2500\u2500 */
|
|
9746
|
+
|
|
9747
|
+
/** Array of power-up card props */
|
|
9748
|
+
powerUpCards?: PowerUpCardProps[];
|
|
9749
|
+
/** Power-ups section heading (default: "Power-ups and charges") */
|
|
9750
|
+
powerUpsTitle?: string;
|
|
9751
|
+
/** Feature comparison link text (default: "See full feature comparison") */
|
|
9752
|
+
featureComparisonText?: string;
|
|
9753
|
+
/** Callback when the feature comparison link is clicked */
|
|
9754
|
+
onFeatureComparisonClick?: () => void;
|
|
9755
|
+
|
|
9756
|
+
/* \u2500\u2500\u2500\u2500\u2500 Let Us Drive Section \u2500\u2500\u2500\u2500\u2500 */
|
|
9757
|
+
|
|
9758
|
+
/** Array of let-us-drive card props */
|
|
9759
|
+
letUsDriveCards?: LetUsDriveCardProps[];
|
|
9760
|
+
/** Let-us-drive section heading (default: "Let us drive \u2014 Full-service management") */
|
|
9761
|
+
letUsDriveTitle?: string;
|
|
8696
9762
|
}
|
|
9763
|
+
`, prefix)
|
|
9764
|
+
},
|
|
9765
|
+
{
|
|
9766
|
+
name: "index.ts",
|
|
9767
|
+
content: prefixTailwindClasses(`export { PricingPage } from "./pricing-page";
|
|
9768
|
+
export type { PricingPageProps, PricingToggleTab } from "./types";
|
|
9769
|
+
`, prefix)
|
|
9770
|
+
}
|
|
9771
|
+
]
|
|
9772
|
+
},
|
|
9773
|
+
"pricing-toggle": {
|
|
9774
|
+
name: "pricing-toggle",
|
|
9775
|
+
description: "A plan type tab selector with billing period toggle for pricing pages. Pill-shaped tabs switch plan categories, and an optional switch toggles between monthly/yearly billing.",
|
|
9776
|
+
category: "custom",
|
|
9777
|
+
dependencies: [
|
|
9778
|
+
"clsx",
|
|
9779
|
+
"tailwind-merge",
|
|
9780
|
+
"@radix-ui/react-switch@^1.2.6"
|
|
9781
|
+
],
|
|
9782
|
+
internalDependencies: [
|
|
9783
|
+
"switch"
|
|
9784
|
+
],
|
|
9785
|
+
isMultiFile: true,
|
|
9786
|
+
directory: "pricing-toggle",
|
|
9787
|
+
mainFile: "pricing-toggle.tsx",
|
|
9788
|
+
files: [
|
|
9789
|
+
{
|
|
9790
|
+
name: "pricing-toggle.tsx",
|
|
9791
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
9792
|
+
import { cn } from "../../../lib/utils";
|
|
9793
|
+
import { Switch } from "../switch";
|
|
9794
|
+
import type { PricingToggleProps } from "./types";
|
|
8697
9795
|
|
|
8698
9796
|
/**
|
|
8699
|
-
*
|
|
8700
|
-
*
|
|
8701
|
-
*
|
|
9797
|
+
* PricingToggle provides a plan type tab selector with an optional
|
|
9798
|
+
* billing period toggle. The pill-shaped tabs switch between plan
|
|
9799
|
+
* categories (e.g. "Team-Led Plans" vs "Go-AI First"), and the
|
|
9800
|
+
* billing toggle switches between monthly/yearly pricing.
|
|
9801
|
+
*
|
|
9802
|
+
* @example
|
|
9803
|
+
* \`\`\`tsx
|
|
9804
|
+
* <PricingToggle
|
|
9805
|
+
* tabs={[
|
|
9806
|
+
* { label: "Team-Led Plans", value: "team" },
|
|
9807
|
+
* { label: "Go-AI First", value: "ai" },
|
|
9808
|
+
* ]}
|
|
9809
|
+
* activeTab="team"
|
|
9810
|
+
* onTabChange={(value) => setActiveTab(value)}
|
|
9811
|
+
* showBillingToggle
|
|
9812
|
+
* billingPeriod="monthly"
|
|
9813
|
+
* onBillingPeriodChange={(period) => setBillingPeriod(period)}
|
|
9814
|
+
* />
|
|
9815
|
+
* \`\`\`
|
|
8702
9816
|
*/
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
9817
|
+
const PricingToggle = React.forwardRef<HTMLDivElement, PricingToggleProps>(
|
|
9818
|
+
(
|
|
9819
|
+
{
|
|
9820
|
+
tabs,
|
|
9821
|
+
activeTab,
|
|
9822
|
+
onTabChange,
|
|
9823
|
+
showBillingToggle = false,
|
|
9824
|
+
billingPeriod = "monthly",
|
|
9825
|
+
onBillingPeriodChange,
|
|
9826
|
+
monthlyLabel = "Monthly",
|
|
9827
|
+
yearlyLabel = "Yearly (Save 20%)",
|
|
9828
|
+
className,
|
|
9829
|
+
...props
|
|
9830
|
+
},
|
|
9831
|
+
ref
|
|
9832
|
+
) => {
|
|
9833
|
+
const isYearly = billingPeriod === "yearly";
|
|
9834
|
+
|
|
9835
|
+
return (
|
|
9836
|
+
<div
|
|
9837
|
+
ref={ref}
|
|
9838
|
+
className={cn("flex flex-col items-center gap-4", className)}
|
|
9839
|
+
{...props}
|
|
9840
|
+
>
|
|
9841
|
+
{/* Plan type tabs */}
|
|
9842
|
+
<div className="inline-flex items-start gap-1 rounded-full bg-semantic-bg-ui p-1">
|
|
9843
|
+
{tabs.map((tab) => {
|
|
9844
|
+
const isActive = tab.value === activeTab;
|
|
9845
|
+
return (
|
|
9846
|
+
<button
|
|
9847
|
+
key={tab.value}
|
|
9848
|
+
type="button"
|
|
9849
|
+
role="tab"
|
|
9850
|
+
aria-selected={isActive}
|
|
9851
|
+
className={cn(
|
|
9852
|
+
"h-10 shrink-0 rounded-full px-4 py-1 text-base transition-colors",
|
|
9853
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-semantic-brand focus-visible:ring-offset-2",
|
|
9854
|
+
isActive
|
|
9855
|
+
? "bg-semantic-brand font-semibold text-white shadow-sm"
|
|
9856
|
+
: "font-normal text-semantic-text-primary"
|
|
9857
|
+
)}
|
|
9858
|
+
onClick={() => onTabChange(tab.value)}
|
|
9859
|
+
>
|
|
9860
|
+
{tab.label}
|
|
9861
|
+
</button>
|
|
9862
|
+
);
|
|
9863
|
+
})}
|
|
9864
|
+
</div>
|
|
9865
|
+
|
|
9866
|
+
{/* Billing period toggle */}
|
|
9867
|
+
{showBillingToggle && (
|
|
9868
|
+
<div className="flex items-center gap-4">
|
|
9869
|
+
<span
|
|
9870
|
+
className={cn(
|
|
9871
|
+
"text-sm font-semibold tracking-[0.014px]",
|
|
9872
|
+
!isYearly
|
|
9873
|
+
? "text-semantic-text-secondary"
|
|
9874
|
+
: "text-semantic-text-muted"
|
|
9875
|
+
)}
|
|
9876
|
+
>
|
|
9877
|
+
{monthlyLabel}
|
|
9878
|
+
</span>
|
|
9879
|
+
<Switch
|
|
9880
|
+
size="sm"
|
|
9881
|
+
checked={isYearly}
|
|
9882
|
+
onCheckedChange={(checked) =>
|
|
9883
|
+
onBillingPeriodChange?.(checked ? "yearly" : "monthly")
|
|
9884
|
+
}
|
|
9885
|
+
/>
|
|
9886
|
+
<span
|
|
9887
|
+
className={cn(
|
|
9888
|
+
"text-sm font-semibold tracking-[0.014px]",
|
|
9889
|
+
isYearly
|
|
9890
|
+
? "text-semantic-text-secondary"
|
|
9891
|
+
: "text-semantic-text-muted"
|
|
9892
|
+
)}
|
|
9893
|
+
>
|
|
9894
|
+
{yearlyLabel}
|
|
9895
|
+
</span>
|
|
9896
|
+
</div>
|
|
9897
|
+
)}
|
|
9898
|
+
</div>
|
|
9899
|
+
);
|
|
9900
|
+
}
|
|
9901
|
+
);
|
|
9902
|
+
|
|
9903
|
+
PricingToggle.displayName = "PricingToggle";
|
|
9904
|
+
|
|
9905
|
+
export { PricingToggle };
|
|
9906
|
+
`, prefix)
|
|
9907
|
+
},
|
|
9908
|
+
{
|
|
9909
|
+
name: "types.ts",
|
|
9910
|
+
content: prefixTailwindClasses(`/** A single tab option in the plan tab selector */
|
|
9911
|
+
export interface PricingToggleTab {
|
|
9912
|
+
/** Display label for the tab */
|
|
9913
|
+
label: string;
|
|
9914
|
+
/** Unique value identifier for the tab */
|
|
9915
|
+
value: string;
|
|
9916
|
+
}
|
|
9917
|
+
|
|
9918
|
+
export interface PricingToggleProps
|
|
9919
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
9920
|
+
/** Array of tab options for the plan type selector */
|
|
9921
|
+
tabs: PricingToggleTab[];
|
|
9922
|
+
/** Currently active tab value (controlled) */
|
|
9923
|
+
activeTab: string;
|
|
9924
|
+
/** Callback when the active tab changes */
|
|
9925
|
+
onTabChange: (value: string) => void;
|
|
9926
|
+
/** Whether to show the billing period toggle below the tabs */
|
|
9927
|
+
showBillingToggle?: boolean;
|
|
9928
|
+
/** Current billing period \u2014 "monthly" or "yearly" (controlled) */
|
|
9929
|
+
billingPeriod?: "monthly" | "yearly";
|
|
9930
|
+
/** Callback when the billing period changes */
|
|
9931
|
+
onBillingPeriodChange?: (period: "monthly" | "yearly") => void;
|
|
9932
|
+
/** Left label for the billing toggle (default: "Monthly") */
|
|
9933
|
+
monthlyLabel?: string;
|
|
9934
|
+
/** Right label for the billing toggle (default: "Yearly (Save 20%)") */
|
|
9935
|
+
yearlyLabel?: string;
|
|
8709
9936
|
}
|
|
8710
9937
|
`, prefix)
|
|
8711
9938
|
},
|
|
8712
9939
|
{
|
|
8713
9940
|
name: "index.ts",
|
|
8714
|
-
content: prefixTailwindClasses(`export {
|
|
8715
|
-
export {
|
|
8716
|
-
export type {
|
|
8717
|
-
PaymentOptionCardProps,
|
|
8718
|
-
PaymentOptionCardModalProps,
|
|
8719
|
-
PaymentOption,
|
|
8720
|
-
} from "./types";
|
|
9941
|
+
content: prefixTailwindClasses(`export { PricingToggle } from "./pricing-toggle";
|
|
9942
|
+
export type { PricingToggleProps, PricingToggleTab } from "./types";
|
|
8721
9943
|
`, prefix)
|
|
8722
9944
|
}
|
|
8723
9945
|
]
|
|
@@ -8743,7 +9965,7 @@ export type {
|
|
|
8743
9965
|
{
|
|
8744
9966
|
name: "wallet-topup.tsx",
|
|
8745
9967
|
content: prefixTailwindClasses(`import * as React from "react";
|
|
8746
|
-
import {
|
|
9968
|
+
import { Ticket } from "lucide-react";
|
|
8747
9969
|
import { cn } from "../../../lib/utils";
|
|
8748
9970
|
import { Button } from "../button";
|
|
8749
9971
|
import { Input } from "../input";
|
|
@@ -8766,7 +9988,11 @@ function normalizeAmountOption(option: number | AmountOption): AmountOption {
|
|
|
8766
9988
|
* Format currency amount with symbol
|
|
8767
9989
|
*/
|
|
8768
9990
|
function formatCurrency(amount: number, symbol: string = "\u20B9"): string {
|
|
8769
|
-
|
|
9991
|
+
const hasDecimals = amount % 1 !== 0;
|
|
9992
|
+
return \`\${symbol}\${amount.toLocaleString("en-IN", {
|
|
9993
|
+
minimumFractionDigits: hasDecimals ? 2 : 0,
|
|
9994
|
+
maximumFractionDigits: hasDecimals ? 2 : 0,
|
|
9995
|
+
})}\`;
|
|
8770
9996
|
}
|
|
8771
9997
|
|
|
8772
9998
|
/**
|
|
@@ -8798,6 +10024,13 @@ export const WalletTopup = React.forwardRef<HTMLDivElement, WalletTopupProps>(
|
|
|
8798
10024
|
customAmountPlaceholder = "Enter amount",
|
|
8799
10025
|
customAmountLabel = "Custom Amount",
|
|
8800
10026
|
currencySymbol = "\u20B9",
|
|
10027
|
+
taxAmount: taxAmountProp,
|
|
10028
|
+
taxCalculator,
|
|
10029
|
+
taxLabel = "Taxes (GST)",
|
|
10030
|
+
rechargeAmountLabel = "Recharge amount",
|
|
10031
|
+
outstandingAmount,
|
|
10032
|
+
outstandingLabel = "Outstanding",
|
|
10033
|
+
topupLabel = "Top-up",
|
|
8801
10034
|
showVoucherLink = true,
|
|
8802
10035
|
voucherLinkText = "Have an offline code or voucher?",
|
|
8803
10036
|
voucherIcon = <Ticket className="size-4" />,
|
|
@@ -8894,6 +10127,10 @@ export const WalletTopup = React.forwardRef<HTMLDivElement, WalletTopupProps>(
|
|
|
8894
10127
|
};
|
|
8895
10128
|
|
|
8896
10129
|
const normalizedAmounts = amounts.map(normalizeAmountOption);
|
|
10130
|
+
const displayAmounts =
|
|
10131
|
+
outstandingAmount && outstandingAmount > 0
|
|
10132
|
+
? [{ value: 0 } as AmountOption, ...normalizedAmounts]
|
|
10133
|
+
: normalizedAmounts;
|
|
8897
10134
|
|
|
8898
10135
|
const handleAmountSelect = (value: number) => {
|
|
8899
10136
|
const newValue = selectedValue === value ? null : value;
|
|
@@ -8925,19 +10162,39 @@ export const WalletTopup = React.forwardRef<HTMLDivElement, WalletTopupProps>(
|
|
|
8925
10162
|
};
|
|
8926
10163
|
|
|
8927
10164
|
// Determine the effective pay amount
|
|
8928
|
-
const
|
|
8929
|
-
selectedValue ?? (customValue ? Number(customValue) :
|
|
10165
|
+
const baseSelection =
|
|
10166
|
+
selectedValue ?? (customValue ? Number(customValue) : null);
|
|
10167
|
+
|
|
10168
|
+
// Effective recharge amount (includes outstanding if present)
|
|
10169
|
+
const effectiveRechargeAmount =
|
|
10170
|
+
baseSelection !== null
|
|
10171
|
+
? outstandingAmount
|
|
10172
|
+
? outstandingAmount + baseSelection
|
|
10173
|
+
: baseSelection
|
|
10174
|
+
: 0;
|
|
10175
|
+
|
|
10176
|
+
// Tax computation
|
|
10177
|
+
const hasTax = taxCalculator !== undefined || taxAmountProp !== undefined;
|
|
10178
|
+
const computedTax =
|
|
10179
|
+
effectiveRechargeAmount > 0
|
|
10180
|
+
? taxCalculator
|
|
10181
|
+
? taxCalculator(effectiveRechargeAmount)
|
|
10182
|
+
: (taxAmountProp ?? 0)
|
|
10183
|
+
: 0;
|
|
10184
|
+
|
|
10185
|
+
// Total payable (recharge + tax)
|
|
10186
|
+
const totalPayable = effectiveRechargeAmount + computedTax;
|
|
8930
10187
|
|
|
8931
10188
|
const handlePay = () => {
|
|
8932
|
-
if (
|
|
8933
|
-
onPay?.(
|
|
10189
|
+
if (totalPayable > 0) {
|
|
10190
|
+
onPay?.(totalPayable);
|
|
8934
10191
|
}
|
|
8935
10192
|
};
|
|
8936
10193
|
|
|
8937
10194
|
const buttonText =
|
|
8938
10195
|
ctaText ||
|
|
8939
|
-
(
|
|
8940
|
-
? \`Pay \${formatCurrency(
|
|
10196
|
+
(totalPayable > 0
|
|
10197
|
+
? \`Pay \${formatCurrency(totalPayable, currencySymbol)} now\`
|
|
8941
10198
|
: "Select an amount");
|
|
8942
10199
|
|
|
8943
10200
|
return (
|
|
@@ -8974,8 +10231,15 @@ export const WalletTopup = React.forwardRef<HTMLDivElement, WalletTopupProps>(
|
|
|
8974
10231
|
{amountSectionLabel}
|
|
8975
10232
|
</label>
|
|
8976
10233
|
<div className="grid grid-cols-2 gap-4">
|
|
8977
|
-
{
|
|
10234
|
+
{displayAmounts.map((option) => {
|
|
8978
10235
|
const isSelected = selectedValue === option.value;
|
|
10236
|
+
const hasOutstanding =
|
|
10237
|
+
outstandingAmount !== undefined &&
|
|
10238
|
+
outstandingAmount > 0;
|
|
10239
|
+
const totalForOption = hasOutstanding
|
|
10240
|
+
? outstandingAmount + option.value
|
|
10241
|
+
: option.value;
|
|
10242
|
+
|
|
8979
10243
|
return (
|
|
8980
10244
|
<button
|
|
8981
10245
|
key={option.value}
|
|
@@ -8984,18 +10248,53 @@ export const WalletTopup = React.forwardRef<HTMLDivElement, WalletTopupProps>(
|
|
|
8984
10248
|
aria-checked={isSelected}
|
|
8985
10249
|
onClick={() => handleAmountSelect(option.value)}
|
|
8986
10250
|
className={cn(
|
|
8987
|
-
"flex
|
|
10251
|
+
"flex px-4 rounded text-sm transition-all cursor-pointer",
|
|
10252
|
+
hasOutstanding
|
|
10253
|
+
? "flex-col items-start gap-0.5 h-auto py-3"
|
|
10254
|
+
: "items-center h-10 py-2.5",
|
|
8988
10255
|
isSelected
|
|
8989
|
-
? "border border-semantic-
|
|
10256
|
+
? "border border-[var(--semantic-brand)] shadow-sm"
|
|
8990
10257
|
: "border border-semantic-border-input hover:border-semantic-text-muted"
|
|
8991
10258
|
)}
|
|
8992
10259
|
>
|
|
8993
|
-
<span
|
|
8994
|
-
{
|
|
8995
|
-
|
|
10260
|
+
<span
|
|
10261
|
+
className={cn(
|
|
10262
|
+
isSelected
|
|
10263
|
+
? "text-semantic-primary"
|
|
10264
|
+
: "text-semantic-text-primary",
|
|
10265
|
+
hasOutstanding && "font-medium"
|
|
10266
|
+
)}
|
|
10267
|
+
>
|
|
10268
|
+
{hasOutstanding
|
|
10269
|
+
? formatCurrency(
|
|
10270
|
+
totalForOption,
|
|
10271
|
+
currencySymbol
|
|
10272
|
+
)
|
|
10273
|
+
: option.label ||
|
|
10274
|
+
formatCurrency(
|
|
10275
|
+
option.value,
|
|
10276
|
+
currencySymbol
|
|
10277
|
+
)}
|
|
8996
10278
|
</span>
|
|
8997
|
-
{
|
|
8998
|
-
|
|
10279
|
+
{hasOutstanding && (
|
|
10280
|
+
<>
|
|
10281
|
+
<span className="text-xs text-semantic-text-muted">
|
|
10282
|
+
{outstandingLabel}:{" "}
|
|
10283
|
+
{formatCurrency(
|
|
10284
|
+
outstandingAmount,
|
|
10285
|
+
currencySymbol
|
|
10286
|
+
)}
|
|
10287
|
+
</span>
|
|
10288
|
+
<span className="text-xs text-semantic-text-muted">
|
|
10289
|
+
{topupLabel}:{" "}
|
|
10290
|
+
{option.value > 0
|
|
10291
|
+
? formatCurrency(
|
|
10292
|
+
option.value,
|
|
10293
|
+
currencySymbol
|
|
10294
|
+
)
|
|
10295
|
+
: "-"}
|
|
10296
|
+
</span>
|
|
10297
|
+
</>
|
|
8999
10298
|
)}
|
|
9000
10299
|
</button>
|
|
9001
10300
|
);
|
|
@@ -9016,6 +10315,31 @@ export const WalletTopup = React.forwardRef<HTMLDivElement, WalletTopupProps>(
|
|
|
9016
10315
|
/>
|
|
9017
10316
|
</div>
|
|
9018
10317
|
|
|
10318
|
+
{/* Recharge Summary */}
|
|
10319
|
+
{hasTax && effectiveRechargeAmount > 0 && (
|
|
10320
|
+
<div className="flex flex-col gap-2 rounded-lg bg-[var(--semantic-warning-surface)] px-4 py-3">
|
|
10321
|
+
<div className="flex items-center justify-between text-sm">
|
|
10322
|
+
<span className="text-semantic-text-primary">
|
|
10323
|
+
{rechargeAmountLabel}
|
|
10324
|
+
</span>
|
|
10325
|
+
<span className="text-semantic-text-primary font-medium">
|
|
10326
|
+
{formatCurrency(
|
|
10327
|
+
effectiveRechargeAmount,
|
|
10328
|
+
currencySymbol
|
|
10329
|
+
)}
|
|
10330
|
+
</span>
|
|
10331
|
+
</div>
|
|
10332
|
+
<div className="flex items-center justify-between text-sm">
|
|
10333
|
+
<span className="text-semantic-text-muted">
|
|
10334
|
+
{taxLabel}
|
|
10335
|
+
</span>
|
|
10336
|
+
<span className="text-semantic-text-muted">
|
|
10337
|
+
{formatCurrency(computedTax, currencySymbol)}
|
|
10338
|
+
</span>
|
|
10339
|
+
</div>
|
|
10340
|
+
</div>
|
|
10341
|
+
)}
|
|
10342
|
+
|
|
9019
10343
|
{/* Voucher Link or Voucher Code Input */}
|
|
9020
10344
|
{showVoucherLink && !showVoucherInput && (
|
|
9021
10345
|
<button
|
|
@@ -9067,7 +10391,7 @@ export const WalletTopup = React.forwardRef<HTMLDivElement, WalletTopupProps>(
|
|
|
9067
10391
|
className="w-full"
|
|
9068
10392
|
onClick={handlePay}
|
|
9069
10393
|
loading={loading}
|
|
9070
|
-
disabled={disabled ||
|
|
10394
|
+
disabled={disabled || totalPayable <= 0}
|
|
9071
10395
|
>
|
|
9072
10396
|
{buttonText}
|
|
9073
10397
|
</Button>
|
|
@@ -9136,6 +10460,24 @@ export interface WalletTopupProps {
|
|
|
9136
10460
|
/** Currency symbol (default: "\u20B9") */
|
|
9137
10461
|
currencySymbol?: string;
|
|
9138
10462
|
|
|
10463
|
+
// Tax / Summary
|
|
10464
|
+
/** Static tax amount to display in the summary section */
|
|
10465
|
+
taxAmount?: number;
|
|
10466
|
+
/** Function to dynamically compute tax from the recharge amount. Takes priority over taxAmount. */
|
|
10467
|
+
taxCalculator?: (amount: number) => number;
|
|
10468
|
+
/** Label for the tax line in the summary (default: "Taxes (GST)") */
|
|
10469
|
+
taxLabel?: string;
|
|
10470
|
+
/** Label for the recharge amount line in the summary (default: "Recharge amount") */
|
|
10471
|
+
rechargeAmountLabel?: string;
|
|
10472
|
+
|
|
10473
|
+
// Outstanding balance
|
|
10474
|
+
/** Outstanding balance. When set, auto-prepends an outstanding-only option and shows breakdowns in each amount button. */
|
|
10475
|
+
outstandingAmount?: number;
|
|
10476
|
+
/** Label for the outstanding breakdown in amount buttons (default: "Outstanding") */
|
|
10477
|
+
outstandingLabel?: string;
|
|
10478
|
+
/** Label for the topup breakdown in amount buttons (default: "Top-up") */
|
|
10479
|
+
topupLabel?: string;
|
|
10480
|
+
|
|
9139
10481
|
// Voucher link
|
|
9140
10482
|
/** Whether to show the voucher/code link */
|
|
9141
10483
|
showVoucherLink?: boolean;
|