No IconButton, CTAButton, SubmitButton, or LoadingButton component exists — but these 7 visual patterns recur across the source platform, written by hand per file instead of reused. Each is its own installable item here.
ContactMarketing.tsx — fixed bottom-right on every main-layout page.
npx chandra-asri-ui add whatsapp-ctaimport { Button } from "@/app/components/ui/button";
import { MessageCircle } from "lucide-react";
/**
* Floating WhatsApp CTA (ContactMarketing.tsx) — base Button size="lg"
* with a manual override instead of an existing rounded/pill variant.
* The source uses a WhatsApp brand icon (FaWhatsapp); approximated here
* with lucide-react's generic MessageCircle since brand icons aren't
* part of this bundle's icon set. Shown inline here; the real one is
* fixed bottom-right on every page.
*/
export function WhatsappCtaDemo() {
return (
<Button
size="lg"
className="bg-dark-80 hover:bg-dark-80 gap-2 rounded-full font-semibold text-white"
>
<MessageCircle size={18} />
Chat with us
</Button>
);
}
CallToAction.tsx— the pill CTA shape used across Chandra Asri sites. Conceptually the same as the base Button's rounded/roundedOutline variants, worth mapping to those instead of hand-writing.
npx chandra-asri-ui add pill-cta-hero/**
* Pill CTA "hero/section" (CallToAction.tsx) — the pill CTA shape
* used across Chandra Asri sites: a white pill on a solid brand-blue background.
* Conceptually equivalent to the base Button's rounded/roundedOutline
* variants, but written by hand instead of reusing them.
*/
export function PillCtaHeroDemo() {
return (
<div className="bg-primary-70 flex items-center justify-center gap-4 rounded-lg p-8">
<button
type="button"
className="text-primary-70 hover:bg-gray-80 rounded-full bg-white px-6 py-3 text-sm font-medium sm:px-12 sm:py-4 sm:text-base"
>
Get Started
</button>
<button
type="button"
className="hover:text-primary-70 rounded-full border border-white px-6 py-3 text-sm font-medium text-white hover:bg-white sm:px-12 sm:py-4 sm:text-base"
>
Learn More
</button>
</div>
);
}
ServicesSection.tsx
npx chandra-asri-ui add outline-pill-buttonimport { ArrowRight } from "lucide-react";
/**
* Outline pill "Learn more" (ServicesSection.tsx) — a raw <button>,
* not a Button variant, though it's visually close to `outline`.
*/
export function OutlinePillLearnMoreDemo() {
return (
<button
type="button"
className="border-primary-70 text-primary-70 hover:bg-primary-70 mt-4 inline-flex items-center gap-2 rounded-full border px-4 py-1.5 text-sm transition hover:text-white sm:text-base"
>
Learn more
<ArrowRight size={16} />
</button>
);
}
ResourcesHero.tsx & ResourcesHeroV2.tsx (identical duplicates).
npx chandra-asri-ui add dropdown-pill-buttonimport { ChevronDown } from "lucide-react";
/**
* Pill dropdown trigger (ResourcesHero.tsx / ResourcesHeroV2.tsx,
* identical duplicates) — a raw <button>, normally inside a
* DropdownMenuTrigger asChild.
*/
export function PurpleDropdownPillDemo() {
return (
<button
type="button"
className="bg-purple-70 flex w-fit items-center gap-2 rounded-full px-5 py-2 text-xl font-bold text-white"
>
Resources
<ChevronDown size={20} />
</button>
);
}
SeoTools.tsx — a heavier, shadowed round-button pattern, distinct from the buttonVariants-based Carousel controls in Derived Components.
npx chandra-asri-ui add carousel-arrow-nav-buttonimport { ChevronLeft, ChevronRight } from "lucide-react";
/**
* Round arrow nav buttons + dots (SeoTools.tsx) — a different,
* heavier round-button pattern than CarouselPrevious/Next in
* Derived Components (shadow, bigger, not built on buttonVariants).
*/
export function CarouselArrowNavDemo() {
return (
<div className="flex items-center gap-4">
<button
type="button"
aria-label="Previous"
className="flex h-12 w-12 items-center justify-center rounded-full border border-gray-200 bg-white text-gray-700 shadow-sm transition-all hover:bg-gray-50 disabled:opacity-30"
>
<ChevronLeft size={20} />
</button>
<button
type="button"
aria-label="Next"
className="bg-primary-70 hover:bg-primary-80 flex h-12 w-12 items-center justify-center rounded-full text-white shadow-md transition-all disabled:opacity-30"
>
<ChevronRight size={20} />
</button>
<div className="flex items-center gap-1.5 pl-2">
<span className="bg-primary-70 h-2 w-10 rounded-full transition-all duration-500" />
<span className="h-2 w-4 rounded-full bg-gray-200 transition-all duration-500 hover:bg-gray-300" />
<span className="h-2 w-4 rounded-full bg-gray-200 transition-all duration-500 hover:bg-gray-300" />
</div>
</div>
);
}
WebAnalyzerSidebar.tsx — two patterns in one file: a plain underlined text link, and a card-styled selectable option.
npx chandra-asri-ui add small-text-button/**
* Small text buttons (WebAnalyzerSidebar.tsx) — two distinct
* patterns in the same file: a plain underlined text link, and a
* card-styled selectable option button.
*/
export function SmallTextButtonDemo() {
return (
<div className="flex flex-col items-start gap-4">
<button
type="button"
className="text-primary-80 hover:text-primary-100 text-xs font-normal underline"
>
Clear
</button>
<button
type="button"
className="border-primary-20 bg-primary-10 text-primary-110 hover:bg-primary-20 flex items-center gap-2 rounded-lg border px-4 py-3 text-left text-sm transition-all"
>
Keyword suggestion
</button>
</div>
);
}
FooterCompliance.tsx — text-only, styled like a link even though it drives an accordion.
npx chandra-asri-ui add disclosure-button"use client";
import { Minus, Plus } from "lucide-react";
import { useState } from "react";
/**
* Disclosure/accordion trigger (FooterCompliance.tsx) — text-only,
* no background/border, styled like a link even though it functions as
* an accordion trigger.
*/
export function DisclosureButtonDemo() {
const [open, setOpen] = useState(false);
return (
<div className="w-full max-w-sm">
<button
type="button"
onClick={() => setOpen((value) => !value)}
className="hover:text-primary-70 flex w-full items-center justify-between gap-1 text-start"
>
Cookie Policy
{open ? <Minus size={16} /> : <Plus size={16} />}
</button>
{open && (
<p className="text-muted mt-2 text-sm">
This site uses cookies to improve your experience.
</p>
)}
</div>
);
}