diff --git a/apps/portal/src/features/landing-page/views/PublicLandingView.tsx b/apps/portal/src/features/landing-page/views/PublicLandingView.tsx
index 8ae3395f..8e74cba6 100644
--- a/apps/portal/src/features/landing-page/views/PublicLandingView.tsx
+++ b/apps/portal/src/features/landing-page/views/PublicLandingView.tsx
@@ -1,1426 +1,48 @@
"use client";
-import Image from "next/image";
import Link from "next/link";
-import { useCallback, useEffect, useRef, useState } from "react";
+import { ArrowRight } from "lucide-react";
+import { useContactForm } from "@/features/landing-page/hooks";
import {
- ArrowRight,
- Wifi,
- Smartphone,
- Lock,
- BadgeCheck,
- Wrench,
- Building2,
- MapPin,
- Mail,
- MessageSquare,
- PhoneCall,
- Train,
- Server,
- Shield,
- Code,
- Settings,
- CheckCircle,
- AlertCircle,
- Check,
-} from "lucide-react";
-import { Spinner } from "@/components/atoms";
-import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
+ HeroSection,
+ TrustSection,
+ SolutionsCarousel,
+ PopularServicesSection,
+ RemoteSupportSection,
+ ContactSection,
+} from "@/features/landing-page/components";
-// =============================================================================
-// CUSTOM HOOKS
-// =============================================================================
-
-/**
- * useInView - Intersection Observer hook for scroll-triggered animations
- * Returns a ref and boolean indicating if element is in viewport
- */
-function useInView(options: IntersectionObserverInit = {}) {
- const ref = useRef
(null);
- const [isInView, setIsInView] = useState(false);
-
- useEffect(() => {
- const element = ref.current;
- if (!element) return;
-
- const observer = new IntersectionObserver(
- ([entry]) => {
- if (entry?.isIntersecting) {
- setIsInView(true);
- observer.disconnect(); // triggerOnce
- }
- },
- { threshold: 0.1, ...options }
- );
-
- observer.observe(element);
- return () => observer.disconnect();
- }, [options]);
-
- return [ref, isInView] as const;
-}
-
-// =============================================================================
-// TYPES & DATA
-// =============================================================================
-
-type ServiceCategory = "personal" | "business";
-
-interface ServiceItem {
- title: string;
- icon: React.ReactNode;
- href: string;
-}
-
-interface FormData {
- subject: string;
- name: string;
- email: string;
- phone: string;
- message: string;
-}
-
-interface FormErrors {
- subject?: string;
- name?: string;
- email?: string;
- message?: string;
-}
-
-interface FormTouched {
- subject?: boolean;
- name?: boolean;
- email?: boolean;
- phone?: boolean;
- message?: boolean;
-}
-
-const CONTACT_SUBJECTS = [
- { value: "", label: "Select a topic*" },
- { value: "internet", label: "Internet Service Inquiry" },
- { value: "sim", label: "Phone/SIM Plan Inquiry" },
- { value: "vpn", label: "VPN Service Inquiry" },
- { value: "business", label: "Business Solutions" },
- { value: "support", label: "Technical Support" },
- { value: "billing", label: "Billing Question" },
- { value: "other", label: "Other" },
-];
-
-const personalServices: ServiceItem[] = [
- {
- title: "Internet Plan",
- icon: ,
- href: "/services/internet",
- },
- {
- title: "Phone Plan",
- icon: ,
- href: "/services/sim",
- },
- { title: "VPN Service", icon: , href: "/services/vpn" },
- {
- title: "Onsite Support",
- icon: ,
- href: "/services/onsite",
- },
-];
-
-const businessServices: ServiceItem[] = [
- {
- title: "Office LAN Setup",
- icon: ,
- href: "/services/business",
- },
- {
- title: "Onsite & Remote Tech Support",
- icon: ,
- href: "/services/onsite",
- },
- {
- title: "Dedicated Internet Access",
- icon: ,
- href: "/services/business",
- },
- {
- title: "Data Center Service",
- icon: ,
- href: "/services/business",
- },
- {
- title: "Website Construction",
- icon: ,
- href: "/services/business",
- },
- {
- title: "Website Maintenance",
- icon: ,
- href: "/services/business",
- },
-];
-
-const services = [
- {
- title: "Internet Plans",
- description:
- "High-speed NTT fiber with English installation support. No Japanese paperwork, we handle everything for you.",
- icon: ,
- href: "/services/internet",
- },
- {
- title: "Phone Plans",
- description:
- "SIM cards on Japan's best network. Foreign credit cards accepted, no hanko required. Get connected in days.",
- icon: ,
- href: "/services/sim",
- },
- {
- title: "Business Solutions",
- description:
- "Enterprise IT for international companies. Dedicated internet, office networks, and bilingual tech support.",
- icon: ,
- href: "/services/business",
- },
- {
- title: "VPN",
- description:
- "Stream your favorite shows from home. Pre-configured router, just plug in and watch US/UK content.",
- icon: ,
- href: "/services/vpn",
- },
- {
- title: "Onsite Support",
- description:
- "English-speaking technicians at your home or office. Router setup, network issues, and device help.",
- icon: ,
- href: "/services/onsite",
- },
-];
-
-const supportDownloads = [
- {
- title: "Acronis Quick Assist",
- href: "https://www.acronis.com/en/products/cloud/quick-assist/download/",
- image: "/assets/images/arconis.png",
- description:
- "Secure remote desktop tool for quick troubleshooting. Our technicians can view your screen and resolve issues in real-time.",
- useCase: "Best for: General tech support & diagnostics",
- },
- {
- title: "TeamViewer QS",
- href: "https://get.teamviewer.com/tokyo",
- image: "/assets/images/teamviewer.png",
- description:
- "Industry-standard remote access software. Allows our team to securely connect to your device for hands-on assistance.",
- useCase: "Best for: Complex configurations & file transfers",
- },
-];
-
-// Mobile quick services (top 3 most popular)
-const mobileQuickServices = personalServices.slice(0, 3);
-
-// =============================================================================
-// FORM VALIDATION
-// =============================================================================
-
-function validateEmail(email: string): boolean {
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
-}
-
-function validateForm(data: FormData): FormErrors {
- const errors: FormErrors = {};
-
- if (!data.subject) {
- errors.subject = "Please select a topic";
- }
- if (!data.name.trim()) {
- errors.name = "Name is required";
- }
- if (!data.email.trim()) {
- errors.email = "Email is required";
- } else if (!validateEmail(data.email)) {
- errors.email = "Please enter a valid email address";
- }
- if (!data.message.trim()) {
- errors.message = "Message is required";
- } else if (data.message.trim().length < 10) {
- errors.message = "Message must be at least 10 characters";
- }
-
- return errors;
-}
-
-// =============================================================================
-// MAIN COMPONENT
-// =============================================================================
-
-/**
- * PublicLandingView - Marketing-focused landing page
- *
- * Purpose: Hook visitors, build trust, guide to shop.
- * Contains:
- * - Hero with tagline
- * - Solutions carousel
- * - Trust & Support sections
- */
export function PublicLandingView() {
- // Carousel state
- const carouselRef = useRef(null);
- const itemWidthRef = useRef(0);
- const [currentSlide, setCurrentSlide] = useState(0);
- const isScrollingRef = useRef(false);
- const autoScrollTimerRef = useRef(null);
- const touchStartXRef = useRef(0);
- const touchEndXRef = useRef(0);
-
- // Section state
- const [activeCategory, setActiveCategory] = useState("personal");
- const [remoteSupportTab, setRemoteSupportTab] = useState(0);
-
- // Form state
- const [formData, setFormData] = useState({
- subject: "",
- name: "",
- email: "",
- phone: "",
- message: "",
- });
- const [formErrors, setFormErrors] = useState({});
- const [formTouched, setFormTouched] = useState({});
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [submitStatus, setSubmitStatus] = useState<"idle" | "success" | "error">("idle");
-
- // Intersection Observer refs for animations
- const [heroRef, heroInView] = useInView();
- const [trustRef, trustInView] = useInView();
- const [solutionsRef, solutionsInView] = useInView();
- const [popularRef, popularInView] = useInView();
- const [supportRef, supportInView] = useInView();
- const [contactRef, contactInView] = useInView();
-
- // Hero CTA visibility for sticky mobile CTA
- const heroCTARef = useRef(null);
- const [showStickyCTA, setShowStickyCTA] = useState(false);
-
- // =============================================================================
- // CAROUSEL LOGIC
- // =============================================================================
-
- const computeItemWidth = useCallback(() => {
- const container = carouselRef.current;
- if (!container) return;
-
- const card = container.querySelector("[data-service-card]");
- if (!card) return;
-
- const gap =
- Number.parseFloat(getComputedStyle(container).columnGap || "0") ||
- Number.parseFloat(getComputedStyle(container).gap || "0") ||
- 24;
-
- itemWidthRef.current = card.getBoundingClientRect().width + gap;
- }, []);
-
- const scrollByOne = useCallback((direction: 1 | -1) => {
- const container = carouselRef.current;
- if (!container || !itemWidthRef.current || isScrollingRef.current) return;
-
- isScrollingRef.current = true;
-
- container.scrollBy({
- left: direction * itemWidthRef.current,
- behavior: "smooth",
- });
-
- setTimeout(() => {
- isScrollingRef.current = false;
- }, 500);
- }, []);
-
- const scrollToIndex = useCallback((index: number) => {
- const container = carouselRef.current;
- if (!container || !itemWidthRef.current) return;
-
- container.scrollTo({
- left: index * itemWidthRef.current,
- behavior: "smooth",
- });
- }, []);
-
- const updateCurrentSlide = useCallback(() => {
- const container = carouselRef.current;
- if (!container || !itemWidthRef.current) return;
-
- const scrollLeft = container.scrollLeft;
- const newIndex = Math.round(scrollLeft / itemWidthRef.current);
- setCurrentSlide(Math.max(0, Math.min(newIndex, services.length - 1)));
- }, []);
-
- const startAutoScroll = useCallback(() => {
- if (autoScrollTimerRef.current) {
- clearInterval(autoScrollTimerRef.current);
- }
-
- autoScrollTimerRef.current = setInterval(() => {
- scrollByOne(1);
- }, 5000);
- }, [scrollByOne]);
-
- const stopAutoScroll = useCallback(() => {
- if (autoScrollTimerRef.current) {
- clearInterval(autoScrollTimerRef.current);
- autoScrollTimerRef.current = null;
- }
- }, []);
-
- // Touch swipe handlers
- const handleTouchStart = useCallback((e: React.TouchEvent) => {
- const touch = e.touches[0];
- if (touch) {
- touchStartXRef.current = touch.clientX;
- touchEndXRef.current = touch.clientX;
- }
- }, []);
-
- const handleTouchMove = useCallback((e: React.TouchEvent) => {
- const touch = e.touches[0];
- if (touch) {
- touchEndXRef.current = touch.clientX;
- }
- }, []);
-
- const handleTouchEnd = useCallback(() => {
- const diff = touchStartXRef.current - touchEndXRef.current;
- const minSwipeDistance = 50;
-
- if (Math.abs(diff) > minSwipeDistance) {
- if (diff > 0) {
- scrollByOne(1); // Swipe left = next
- } else {
- scrollByOne(-1); // Swipe right = prev
- }
- }
- startAutoScroll();
- }, [scrollByOne, startAutoScroll]);
-
- // =============================================================================
- // FORM HANDLERS
- // =============================================================================
-
- const handleInputChange = useCallback(
- (e: React.ChangeEvent) => {
- const { name, value } = e.target;
- setFormData(prev => ({ ...prev, [name]: value }));
-
- // Clear error when user starts typing
- if (formErrors[name as keyof FormErrors]) {
- setFormErrors(prev => ({ ...prev, [name]: undefined }));
- }
- },
- [formErrors]
- );
-
- const handleInputBlur = useCallback(
- (e: React.FocusEvent) => {
- const { name } = e.target;
- setFormTouched(prev => ({ ...prev, [name]: true }));
-
- // Validate single field on blur
- const errors = validateForm(formData);
- if (errors[name as keyof FormErrors]) {
- setFormErrors(prev => ({ ...prev, [name]: errors[name as keyof FormErrors] }));
- }
- },
- [formData]
- );
-
- const handleSubmit = useCallback(
- async (e: React.FormEvent) => {
- e.preventDefault();
-
- // Validate all fields
- const errors = validateForm(formData);
- setFormErrors(errors);
- setFormTouched({ subject: true, name: true, email: true, message: true });
-
- if (Object.keys(errors).length > 0) {
- return;
- }
-
- setIsSubmitting(true);
- setSubmitStatus("idle");
-
- try {
- // Simulate API call - replace with actual endpoint
- await new Promise(resolve => {
- setTimeout(resolve, 1500);
- });
-
- // Success
- setSubmitStatus("success");
- setFormData({ subject: "", name: "", email: "", phone: "", message: "" });
- setFormTouched({});
- setFormErrors({});
-
- // Reset success message after 5 seconds
- setTimeout(() => setSubmitStatus("idle"), 5000);
- } catch {
- setSubmitStatus("error");
- } finally {
- setIsSubmitting(false);
- }
- },
- [formData]
- );
-
- // =============================================================================
- // EFFECTS
- // =============================================================================
-
- // Initialize carousel
- useEffect(() => {
- computeItemWidth();
-
- const handleResize = () => computeItemWidth();
- window.addEventListener("resize", handleResize);
-
- startAutoScroll();
-
- return () => {
- window.removeEventListener("resize", handleResize);
- stopAutoScroll();
- };
- }, [computeItemWidth, startAutoScroll, stopAutoScroll]);
-
- // Track carousel scroll position
- useEffect(() => {
- const container = carouselRef.current;
- if (!container) return;
-
- const handleScroll = () => {
- updateCurrentSlide();
- };
-
- container.addEventListener("scroll", handleScroll, { passive: true });
- return () => container.removeEventListener("scroll", handleScroll);
- }, [updateCurrentSlide]);
-
- // Sticky CTA visibility
- useEffect(() => {
- const ctaElement = heroCTARef.current;
- if (!ctaElement) return;
-
- const observer = new IntersectionObserver(
- ([entry]) => {
- if (entry) {
- setShowStickyCTA(!entry.isIntersecting);
- }
- },
- { threshold: 0 }
- );
-
- observer.observe(ctaElement);
- return () => observer.disconnect();
- }, []);
-
- // Pause auto-scroll on hover
- const handleMouseEnter = useCallback(() => {
- stopAutoScroll();
- }, [stopAutoScroll]);
-
- const handleMouseLeave = useCallback(() => {
- startAutoScroll();
- }, [startAutoScroll]);
-
- const handlePrevClick = useCallback(() => {
- scrollByOne(-1);
- startAutoScroll();
- }, [scrollByOne, startAutoScroll]);
-
- const handleNextClick = useCallback(() => {
- scrollByOne(1);
- startAutoScroll();
- }, [scrollByOne, startAutoScroll]);
-
- // =============================================================================
- // RENDER HELPERS
- // =============================================================================
-
- const getInputClassName = (fieldName: keyof FormErrors) => {
- const baseClass =
- "w-full rounded-md border px-4 py-3 text-sm focus-visible:outline-none focus-visible:ring-2 bg-white transition-colors";
- const hasError = formTouched[fieldName] && formErrors[fieldName];
- return `${baseClass} ${
- hasError
- ? "border-danger focus-visible:ring-danger/60"
- : "border-border focus-visible:ring-primary/60"
- }`;
- };
-
- // =============================================================================
- // RENDER
- // =============================================================================
+ const {
+ heroCTARef,
+ showStickyCTA,
+ formData,
+ formErrors,
+ formTouched,
+ isSubmitting,
+ submitStatus,
+ handleInputChange,
+ handleInputBlur,
+ handleSubmit,
+ } = useContactForm();
return (
- {/* Hero Section */}
-
}
- className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 py-12 sm:py-16 overflow-hidden transition-all duration-700 ${
- heroInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
- }`}
- >
- {/* Gradient Background */}
-
-
- {/* Dot Grid Pattern Overlay */}
-
-
- {/* Subtle gradient accent in corner */}
-
-
-
-
-
- A One Stop Solution
- for Your IT Needs
-
-
- No Japanese required. Get reliable internet, mobile, and VPN services with full
- English support. Serving expats and international businesses for over 20 years.
-
-
-
- Browse Services
-
-
-
- Need Assistance?
-
-
-
-
- {/* Mobile Quick Services - visible on mobile only */}
-
-
- {mobileQuickServices.map(service => (
-
-
-
-
- {service.title}
-
-
-
- ))}
-
-
-
-
- See All Services
-
-
-
-
-
-
- {/* Desktop Services Panel - hidden on mobile */}
-
-
- {/* Tab Switcher */}
-
-
-
-
-
- {/* Services Grid */}
-
- {(activeCategory === "personal" ? personalServices : businessServices).map(
- service => (
-
-
-
svg]:h-10 [&>svg]:w-10"
- : "[&>svg]:h-7 [&>svg]:w-7"
- }
- >
- {service.icon}
-
-
-
- {service.title}
-
-
- )
- )}
-
-
-
-
-
- {/* Gradient fade to next section */}
-
-
-
- {/* Trust and Excellence Section */}
-
}
- className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white transition-all duration-700 ${
- trustInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
- }`}
- >
- {/* Gradient fade to next section */}
-
-
-
-
-
-
-
-
-
-
- Built on Trust and Excellence
-
-
- For over two decades, we've been helping foreigners, expats, and
- international businesses in Japan navigate the tech landscape with confidence.
-
-
-
-
- {[
- "Full English support, no Japanese needed",
- "Foreign credit cards accepted",
- "Bilingual contracts and documentation",
- ].map(item => (
- -
-
- {item}
-
- ))}
-
-
- About our company
-
-
-
-
-
-
-
- {/* Solutions Carousel */}
-
}
- className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#e8f5ff] py-12 sm:py-14 transition-all duration-700 ${
- solutionsInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
- }`}
- >
- {/* Gradient fade to next section */}
-
-
-
-
Solutions
-
-
-
- {services.map((service, index) => (
-
-
- {service.icon}
- {service.title}
-
- {service.description}
-
-
-
- ))}
-
-
- {/* Navigation buttons with larger touch targets */}
-
-
-
-
-
-
- {/* Progress dots */}
-
- {services.map((_, index) => (
-
-
-
-
- {/* Most Popular Services Section */}
-
}
- className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-14 sm:py-16 transition-all duration-700 ${
- popularInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
- }`}
- >
-
- {/* Section Header */}
-
-
- Most Popular Services
-
-
- Get connected with ease. No Japanese required, no complicated paperwork, and full
- English support every step of the way.
-
-
-
- {/* Two-column card layout */}
-
- {/* Internet Plans Card */}
-
- {/* Popular Badge */}
-
-
- Popular
-
-
-
- {/* Icon Container */}
-
-
-
-
- {/* Title */}
-
Internet Plans
-
- {/* Description */}
-
- High-speed fiber internet on Japan's reliable NTT network. We handle all the
- Japanese paperwork and coordinate installation in English.
-
-
- {/* Feature List */}
-
- {[
- "Speeds up to 10 Gbps available",
- "English installation coordination",
- "No Japanese contracts to sign",
- "Foreign credit cards accepted",
- ].map(feature => (
- -
-
-
-
- {feature}
-
- ))}
-
-
- {/* CTA Button */}
-
- View Plans
-
-
-
-
- {/* Phone Plans Card */}
-
- {/* Popular Badge */}
-
-
- Popular
-
-
-
- {/* Icon Container */}
-
-
-
-
- {/* Title */}
-
Phone Plans
-
- {/* Description */}
-
- Mobile SIM cards with voice and data on Japan's top network. Sign up online, no
- hanko needed, and get your SIM delivered fast.
-
-
- {/* Feature List */}
-
- {[
- "Data-only and voice + data options",
- "Keep your number with MNP transfer",
- "eSIM available for instant activation",
- "Flexible plans with no long-term contracts",
- ].map(feature => (
- -
-
-
-
- {feature}
-
- ))}
-
-
- {/* CTA Button */}
-
- View Plans
-
-
-
-
-
-
-
- {/* Remote Support - Full section with mobile-optimized card layout */}
-
}
- className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-12 sm:py-16 transition-all duration-700 overflow-hidden ${
- supportInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
- }`}
- >
- {/* Gradient fade to next section */}
-
-
-
- {/* Section header with connection metaphor */}
-
-
-
- Remote Support
-
-
- Download one of our secure tools and let our technicians help you remotely
-
-
-
-
- {/* Mobile: Stacked Cards Layout */}
-
- {supportDownloads.map(item => (
-
- {/* Card accent line */}
-
-
-
-
- {/* Tool icon with animated ring */}
-
-
-
-
- {/* Pulse indicator */}
-
-
-
- {/* Content */}
-
-
{item.title}
-
- {item.useCase.replace("Best for: ", "")}
-
-
- {item.description}
-
-
-
-
- {/* Download button - full width on mobile for easy tapping */}
-
-
- Download {item.title.split(" ")[0]}
-
-
-
- ))}
-
-
- {/* Desktop: Tabbed Layout (improved) */}
-
-
- {/* Tab buttons */}
-
- {supportDownloads.map((item, index) => (
-
- ))}
-
-
- {/* Tab content */}
- {(() => {
- const currentDownload = supportDownloads[remoteSupportTab];
- if (!currentDownload) return null;
- return (
-
-
-
-
-
-
-
- {currentDownload.useCase}
-
-
- {currentDownload.title}
-
-
- {currentDownload.description}
-
-
-
- Download Now
-
-
-
-
-
- );
- })()}
-
-
-
-
-
- {/* Contact Section */}
-
}
- className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#e8f5ff] py-14 sm:py-16 transition-all duration-700 ${
- contactInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
- }`}
- >
-
-
CONTACT US
-
-
-
-
-
- By Online Form (Anytime)
-
-
- {/* Form Status Messages */}
- {submitStatus === "success" && (
-
- Thank you for contacting us. We'll get back to you within 24 hours.
-
- )}
- {submitStatus === "error" && (
-
- Please try again or contact us directly by phone.
-
- )}
-
-
-
-
-
-
- By Chat (Anytime)
-
-
- Click the bottom right “Chat Button” to reach our team anytime.
-
-
-
-
By Phone (9:30-18:00 JST)
-
-
-
Toll Free within Japan
-
0120-660-470
-
From Overseas (may incur calling rates)
-
+81-3-3560-1006
-
-
-
-
-
-
-
-
-
-
-
-
-
- Access
-
-
- Subway Oedo Line / Nanboku Line
-
- Short distance walk from exit 6 of Azabu-Juban Station
-
- (1F of our building is Domino's Pizza)
-
-
-
-
-
-
- Address
-
-
- 3F Azabu Maruka Bldg.,
-
- 3-8-2 Higashi Azabu, Minato-ku,
-
- Tokyo 106-0044
-
- Tel: 03-3560-1006 Fax: 03-3560-1007
-
-
-
-
-
-
-
-
+
+
+
+
+
+
{/* Sticky Mobile CTA */}
{showStickyCTA && (
diff --git a/apps/portal/src/features/services/components/base/AddressForm.tsx b/apps/portal/src/features/services/components/base/AddressForm.tsx
index 4e3d72e2..90abc0fc 100644
--- a/apps/portal/src/features/services/components/base/AddressForm.tsx
+++ b/apps/portal/src/features/services/components/base/AddressForm.tsx
@@ -238,12 +238,12 @@ export function AddressForm({
const baseInputClasses = `w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 transition-colors ${
hasError
? "border-red-300 focus:ring-red-500 focus:border-red-500"
- : "border-gray-300 focus:ring-blue-500 focus:border-blue-500"
- } ${disabled ? "bg-gray-50 cursor-not-allowed" : "bg-white"}`;
+ : "border-border focus:ring-blue-500 focus:border-blue-500"
+ } ${disabled ? "bg-muted cursor-not-allowed" : "bg-card"}`;
return (
-