diff --git a/apps/portal/package.json b/apps/portal/package.json index 985b0455..2e907e82 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -32,6 +32,7 @@ "input-otp": "^1.4.2", "lucide-react": "^0.563.0", "next": "^16.1.6", + "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", "tailwind-merge": "^3.4.0", diff --git a/apps/portal/src/components/atoms/badge.legacy.tsx b/apps/portal/src/components/atoms/badge.legacy.tsx new file mode 100644 index 00000000..a4b10c49 --- /dev/null +++ b/apps/portal/src/components/atoms/badge.legacy.tsx @@ -0,0 +1,115 @@ +import { forwardRef } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/shared/utils"; + +type BadgeVariant = + | "default" + | "secondary" + | "success" + | "warning" + | "error" + | "info" + | "outline" + | "ghost"; + +const dotColorMap: Record = { + success: "bg-success", + warning: "bg-warning", + error: "bg-danger", + info: "bg-info", + default: "bg-primary-foreground", + secondary: "bg-secondary-foreground", + outline: "bg-muted-foreground", + ghost: "bg-muted-foreground", +}; + +const REMOVE_HOVER = "hover:bg-black/10"; + +const removeButtonColorMap: Record = { + default: "text-primary-foreground hover:bg-primary-foreground/10", + secondary: `text-secondary-foreground ${REMOVE_HOVER}`, + success: REMOVE_HOVER, + warning: REMOVE_HOVER, + error: REMOVE_HOVER, + info: REMOVE_HOVER, + outline: REMOVE_HOVER, + ghost: REMOVE_HOVER, +}; + +const badgeVariants = cva( + "inline-flex items-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary-hover", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + success: "bg-success-soft text-success hover:bg-success-soft/80", + warning: "bg-warning-soft text-foreground hover:bg-warning-soft/80", + error: "bg-danger-soft text-danger hover:bg-danger-soft/80", + info: "bg-info-soft text-info hover:bg-info-soft/80", + outline: "border border-border bg-background text-foreground hover:bg-muted", + ghost: "text-foreground hover:bg-muted", + }, + size: { + sm: "px-2 py-0.5 text-xs rounded", + default: "px-2.5 py-1 text-xs rounded-md", + lg: "px-3 py-1.5 text-sm rounded-lg", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +interface BadgeProps + extends React.HTMLAttributes, VariantProps { + icon?: React.ReactNode; + dot?: boolean; + removable?: boolean; + onRemove?: () => void; +} + +const Badge = forwardRef( + ( + { className, variant = "default", size, icon, dot, removable, onRemove, children, ...props }, + ref + ) => { + const resolvedVariant = variant as BadgeVariant; + + return ( + + {dot && ( + + )} + {icon && {icon}} + {children} + {removable && ( + + )} + + ); + } +); +Badge.displayName = "Badge"; + +export { Badge, badgeVariants }; +export type { BadgeProps }; diff --git a/apps/portal/src/components/atoms/badge.tsx b/apps/portal/src/components/atoms/badge.tsx index a4b10c49..32d73134 100644 --- a/apps/portal/src/components/atoms/badge.tsx +++ b/apps/portal/src/components/atoms/badge.tsx @@ -1,5 +1,13 @@ +/** + * Badge — now powered by shadcn/ui base + custom semantic variants + * + * Keeps success/warning/error/info variants and the dot/removable features + * from the legacy Badge for backward compatibility. + */ + import { forwardRef } from "react"; import { cva, type VariantProps } from "class-variance-authority"; +import { Slot } from "radix-ui"; import { cn } from "@/shared/utils"; type BadgeVariant = @@ -10,7 +18,8 @@ type BadgeVariant = | "error" | "info" | "outline" - | "ghost"; + | "ghost" + | "destructive"; const dotColorMap: Record = { success: "bg-success", @@ -21,34 +30,25 @@ const dotColorMap: Record = { secondary: "bg-secondary-foreground", outline: "bg-muted-foreground", ghost: "bg-muted-foreground", -}; - -const REMOVE_HOVER = "hover:bg-black/10"; - -const removeButtonColorMap: Record = { - default: "text-primary-foreground hover:bg-primary-foreground/10", - secondary: `text-secondary-foreground ${REMOVE_HOVER}`, - success: REMOVE_HOVER, - warning: REMOVE_HOVER, - error: REMOVE_HOVER, - info: REMOVE_HOVER, - outline: REMOVE_HOVER, - ghost: REMOVE_HOVER, + destructive: "bg-white", }; const badgeVariants = cva( - "inline-flex items-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [&>svg]:pointer-events-none [&>svg]:size-3", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary-hover", - secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", - success: "bg-success-soft text-success hover:bg-success-soft/80", - warning: "bg-warning-soft text-foreground hover:bg-warning-soft/80", - error: "bg-danger-soft text-danger hover:bg-danger-soft/80", - info: "bg-info-soft text-info hover:bg-info-soft/80", - outline: "border border-border bg-background text-foreground hover:bg-muted", - ghost: "text-foreground hover:bg-muted", + default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 [a&]:hover:bg-destructive/90", + success: "bg-success-soft text-success [a&]:hover:bg-success-soft/80", + warning: "bg-warning-soft text-foreground [a&]:hover:bg-warning-soft/80", + error: "bg-danger-soft text-danger [a&]:hover:bg-danger-soft/80", + info: "bg-info-soft text-info [a&]:hover:bg-info-soft/80", + outline: + "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", }, size: { sm: "px-2 py-0.5 text-xs rounded", @@ -69,17 +69,36 @@ interface BadgeProps dot?: boolean; removable?: boolean; onRemove?: () => void; + asChild?: boolean; } const Badge = forwardRef( ( - { className, variant = "default", size, icon, dot, removable, onRemove, children, ...props }, + { + className, + variant = "default", + size, + icon, + dot, + removable, + onRemove, + asChild = false, + children, + ...props + }, ref ) => { const resolvedVariant = variant as BadgeVariant; + const Comp = asChild ? Slot.Root : "span"; return ( - + {dot && ( )} @@ -89,10 +108,7 @@ const Badge = forwardRef( )} - + ); } ); diff --git a/apps/portal/src/components/atoms/button.legacy.tsx b/apps/portal/src/components/atoms/button.legacy.tsx new file mode 100644 index 00000000..f190bde7 --- /dev/null +++ b/apps/portal/src/components/atoms/button.legacy.tsx @@ -0,0 +1,160 @@ +import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from "react"; +import { forwardRef } from "react"; +import Link from "next/link"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/shared/utils"; +import { Spinner } from "./spinner"; + +const buttonVariants = cva( + "group inline-flex items-center justify-center rounded-lg text-sm font-medium transition-all duration-[var(--cp-duration-normal)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:opacity-50 disabled:pointer-events-none hover:scale-[1.01] active:scale-[0.98]", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground hover:bg-primary-hover shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)]", + destructive: + "bg-danger text-danger-foreground hover:bg-danger/90 shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)]", + outline: + "border border-border bg-background text-foreground hover:bg-muted shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)]", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)]", + ghost: "text-foreground hover:bg-muted", + subtle: + "bg-muted/50 text-foreground hover:bg-muted border border-transparent hover:border-border/40", + link: "underline-offset-4 hover:underline text-primary", + pill: "rounded-full bg-primary text-primary-foreground hover:bg-primary/90 shadow-md shadow-primary/20 hover:-translate-y-0.5", + pillOutline: "rounded-full border border-border bg-card text-primary hover:bg-primary/5", + }, + size: { + default: "h-11 py-2.5 px-4", + sm: "h-9 px-3 text-xs", + lg: "h-12 px-6 text-base", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +interface ButtonContentProps { + loading: boolean; + leftIcon?: ReactNode; + rightIcon?: ReactNode; + loadingText?: ReactNode; + children?: ReactNode; +} + +function ButtonContent({ + loading, + leftIcon, + rightIcon, + loadingText, + children, +}: ButtonContentProps) { + return ( + + {loading ? : leftIcon} + {loading ? (loadingText ?? children) : children} + {!loading && rightIcon && ( + + {rightIcon} + + )} + + ); +} + +interface ButtonExtras { + leftIcon?: ReactNode; + rightIcon?: ReactNode; + loading?: boolean; + isLoading?: boolean; + loadingText?: ReactNode; +} + +type ButtonAsAnchorProps = { + as: "a"; + href: string; +} & AnchorHTMLAttributes & + VariantProps & + ButtonExtras; + +type ButtonAsButtonProps = { + as?: "button"; +} & ButtonHTMLAttributes & + VariantProps & + ButtonExtras; + +export type ButtonProps = ButtonAsAnchorProps | ButtonAsButtonProps; + +const Button = forwardRef((props, ref) => { + const { + leftIcon, + rightIcon, + loading: loadingProp, + isLoading, + loadingText, + children, + ...rest + } = props; + + const loading = loadingProp ?? isLoading ?? false; + const contentProps = { loading, leftIcon, rightIcon, loadingText, children }; + + if (props.as === "a") { + const { className, variant, size, as: _as, href, ...anchorProps } = rest as ButtonAsAnchorProps; + void _as; + + const isExternal = href.startsWith("http") || href.startsWith("mailto:"); + const commonProps = { + className: cn(buttonVariants({ variant, size, className })), + "aria-busy": loading || undefined, + }; + + if (isExternal) { + return ( + } {...anchorProps}> + + + ); + } + + return ( + } + {...(anchorProps as Omit)} + > + + + ); + } + + const { + className, + variant, + size, + as: _as, + disabled, + ...buttonProps + } = rest as ButtonAsButtonProps; + void _as; + + return ( + + ); +}); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/apps/portal/src/components/atoms/button.stories.tsx b/apps/portal/src/components/atoms/button.stories.tsx index 37cc30b6..b5ccce73 100644 --- a/apps/portal/src/components/atoms/button.stories.tsx +++ b/apps/portal/src/components/atoms/button.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Button } from "./button"; +import { Button as LegacyButton } from "./button.legacy"; import { ArrowRightIcon, PlusIcon } from "@heroicons/react/24/outline"; const meta: Meta = { @@ -20,7 +21,7 @@ const meta: Meta = { "pillOutline", ], }, - size: { control: "select", options: ["default", "sm", "lg"] }, + size: { control: "select", options: ["default", "xs", "sm", "lg", "icon"] }, loading: { control: "boolean" }, disabled: { control: "boolean" }, }, @@ -52,6 +53,7 @@ export const AllVariants: Story = { export const Sizes: Story = { render: () => (
+ @@ -81,3 +83,90 @@ export const Loading: Story = { export const Disabled: Story = { args: { children: "Disabled", disabled: true }, }; + +/* ── Comparison: Legacy vs shadcn ─────────────────────────────── */ + +export const ComparisonWithLegacy: Story = { + render: () => ( +
+

Button — Legacy vs shadcn/ui

+ + {/* Variants */} +
+
+

Legacy

+
+ Primary + Secondary + Outline + Destructive + Ghost + Link +
+
+
+

shadcn/ui

+
+ + + + + + +
+
+
+ + {/* Sizes */} +
+
+

Legacy — Sizes

+
+ Small + Default + Large +
+
+
+

shadcn/ui — Sizes

+
+ + + + +
+
+
+ + {/* Loading */} +
+
+

Legacy — Loading

+ Submitting... +
+
+

shadcn/ui — Loading

+ +
+
+ + {/* With Icons */} +
+
+

Legacy — Icons

+
+ }>Add + }>Next +
+
+
+

shadcn/ui — Icons

+
+ + +
+
+
+
+ ), +}; diff --git a/apps/portal/src/components/atoms/button.tsx b/apps/portal/src/components/atoms/button.tsx index f190bde7..f4bf660a 100644 --- a/apps/portal/src/components/atoms/button.tsx +++ b/apps/portal/src/components/atoms/button.tsx @@ -1,3 +1,11 @@ +/** + * Button — now powered by shadcn/ui + * + * This file re-exports the shadcn Button and extends it with the extra props + * (loading, leftIcon, rightIcon, as="a") that the legacy Button supported so + * that every existing import keeps working without changes. + */ + import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from "react"; import { forwardRef } from "react"; import Link from "next/link"; @@ -5,30 +13,35 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/shared/utils"; import { Spinner } from "./spinner"; +/* ── variant tokens ─────────────────────────────────────────────── */ + const buttonVariants = cva( - "group inline-flex items-center justify-center rounded-lg text-sm font-medium transition-all duration-[var(--cp-duration-normal)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:opacity-50 disabled:pointer-events-none hover:scale-[1.01] active:scale-[0.98]", + "group inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { - default: - "bg-primary text-primary-foreground hover:bg-primary-hover shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)]", + default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: - "bg-danger text-danger-foreground hover:bg-danger/90 shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)]", + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", outline: - "border border-border bg-background text-foreground hover:bg-muted shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)]", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)]", - ghost: "text-foreground hover:bg-muted", + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", subtle: "bg-muted/50 text-foreground hover:bg-muted border border-transparent hover:border-border/40", - link: "underline-offset-4 hover:underline text-primary", + link: "text-primary underline-offset-4 hover:underline", pill: "rounded-full bg-primary text-primary-foreground hover:bg-primary/90 shadow-md shadow-primary/20 hover:-translate-y-0.5", pillOutline: "rounded-full border border-border bg-card text-primary hover:bg-primary/5", }, size: { - default: "h-11 py-2.5 px-4", - sm: "h-9 px-3 text-xs", - lg: "h-12 px-6 text-base", + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", }, }, defaultVariants: { @@ -38,6 +51,8 @@ const buttonVariants = cva( } ); +/* ── content helper (loading / icons) ───────────────────────────── */ + interface ButtonContentProps { loading: boolean; leftIcon?: ReactNode; @@ -66,6 +81,8 @@ function ButtonContent({ ); } +/* ── polymorphic props ──────────────────────────────────────────── */ + interface ButtonExtras { leftIcon?: ReactNode; rightIcon?: ReactNode; @@ -89,6 +106,8 @@ type ButtonAsButtonProps = { export type ButtonProps = ButtonAsAnchorProps | ButtonAsButtonProps; +/* ── component ──────────────────────────────────────────────────── */ + const Button = forwardRef((props, ref) => { const { leftIcon, @@ -145,6 +164,9 @@ const Button = forwardRef((p return ( + )} +
+ + ); +} diff --git a/apps/portal/src/components/atoms/error-state.tsx b/apps/portal/src/components/atoms/error-state.tsx index 48a67c22..1d39bdbe 100644 --- a/apps/portal/src/components/atoms/error-state.tsx +++ b/apps/portal/src/components/atoms/error-state.tsx @@ -1,3 +1,10 @@ +/** + * ErrorState — now powered by shadcn/ui Alert base styles + * + * Uses Alert-inspired layout (grid with icon column) and destructive color + * tokens. Keeps the same variant/onRetry interface for backward compatibility. + */ + import { ExclamationTriangleIcon, ArrowPathIcon } from "@heroicons/react/24/outline"; import { Button } from "./button"; import { cn } from "@/shared/utils"; @@ -11,6 +18,30 @@ interface ErrorStateProps { variant?: "page" | "card" | "inline" | undefined; } +const variantClasses = { + page: "min-h-[400px] py-12", + card: "bg-card text-card-foreground border border-destructive/25 rounded-2xl p-6 shadow-md", + inline: "bg-destructive/10 border border-destructive/25 rounded-md p-4", +}; + +const iconSizes = { + page: "h-16 w-16", + card: "h-12 w-12", + inline: "h-5 w-5", +}; + +const titleSizes = { + page: "text-xl", + card: "text-lg", + inline: "text-sm", +}; + +const messageSizes = { + page: "text-base", + card: "text-sm", + inline: "text-sm", +}; + export function ErrorState({ title = "Something went wrong", message = "An unexpected error occurred. Please try again.", @@ -19,53 +50,24 @@ export function ErrorState({ className, variant = "card", }: ErrorStateProps) { - const baseClasses = "flex flex-col items-center justify-center text-center"; - - const variantClasses = { - page: "min-h-[400px] py-12", - card: "bg-card text-card-foreground border border-danger/25 rounded-2xl p-6 shadow-md", - inline: "bg-danger-soft border border-danger/25 rounded-md p-4", - }; - - const iconSizes = { - page: "h-16 w-16", - card: "h-12 w-12", - inline: "h-5 w-5", - }; - - const titleSizes = { - page: "text-xl", - card: "text-lg", - inline: "text-sm", - }; - - const messageSizes = { - page: "text-base", - card: "text-sm", - inline: "text-sm", - }; - return ( -
-
+
+
-
-

- {title} -

+
+

{title}

-

+

{message}

@@ -74,7 +76,7 @@ export function ErrorState({ onClick={onRetry} variant="outline" size={variant === "inline" ? "sm" : "default"} - className="text-danger border-danger/30 hover:bg-danger-soft" + className="text-destructive border-destructive/30 hover:bg-destructive/10" > {retryLabel} diff --git a/apps/portal/src/components/atoms/inline-toast.legacy.tsx b/apps/portal/src/components/atoms/inline-toast.legacy.tsx new file mode 100644 index 00000000..676f7eee --- /dev/null +++ b/apps/portal/src/components/atoms/inline-toast.legacy.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { AnimatePresence, motion } from "framer-motion"; +import { cn } from "@/shared/utils"; + +type Tone = "info" | "success" | "warning" | "error"; + +interface InlineToastProps { + visible: boolean; + text: string; + tone?: Tone; + className?: string; +} + +export function InlineToast({ visible, text, tone = "info", className = "" }: InlineToastProps) { + const toneClasses = { + success: "bg-success-bg border-success-border text-success", + warning: "bg-warning-bg border-warning-border text-warning", + error: "bg-danger-bg border-danger-border text-danger", + info: "bg-info-bg border-info-border text-info", + }[tone]; + + return ( + + {visible && ( + +
+ {text} +
+
+ )} +
+ ); +} diff --git a/apps/portal/src/components/atoms/inline-toast.tsx b/apps/portal/src/components/atoms/inline-toast.tsx index 676f7eee..7a9d4b32 100644 --- a/apps/portal/src/components/atoms/inline-toast.tsx +++ b/apps/portal/src/components/atoms/inline-toast.tsx @@ -1,3 +1,10 @@ +/** + * InlineToast — now powered by shadcn/ui Alert base styles + * + * Keeps the same animated toast behavior (framer-motion) with tone variants, + * using Alert-inspired semantic color tokens for consistency. + */ + "use client"; import { AnimatePresence, motion } from "framer-motion"; @@ -12,14 +19,14 @@ interface InlineToastProps { className?: string; } -export function InlineToast({ visible, text, tone = "info", className = "" }: InlineToastProps) { - const toneClasses = { - success: "bg-success-bg border-success-border text-success", - warning: "bg-warning-bg border-warning-border text-warning", - error: "bg-danger-bg border-danger-border text-danger", - info: "bg-info-bg border-info-border text-info", - }[tone]; +const toneClasses: Record = { + success: "border-success/30 bg-success-soft text-success", + warning: "border-warning/30 bg-warning-soft text-foreground", + error: "border-destructive/30 bg-destructive/10 text-destructive", + info: "border-info/30 bg-info-soft text-info", +}; +export function InlineToast({ visible, text, tone = "info", className = "" }: InlineToastProps) { return ( {visible && ( @@ -31,9 +38,11 @@ export function InlineToast({ visible, text, tone = "info", className = "" }: In transition={{ duration: 0.3, ease: [0.175, 0.885, 0.32, 1.275] }} >
{text} diff --git a/apps/portal/src/components/atoms/input.legacy.tsx b/apps/portal/src/components/atoms/input.legacy.tsx new file mode 100644 index 00000000..28d13abe --- /dev/null +++ b/apps/portal/src/components/atoms/input.legacy.tsx @@ -0,0 +1,37 @@ +import type { InputHTMLAttributes, ReactNode } from "react"; +import { forwardRef } from "react"; +import { cn } from "@/shared/utils"; + +export interface InputProps extends InputHTMLAttributes { + error?: ReactNode; +} + +const Input = forwardRef( + ({ className, type, error, ...props }, ref) => { + const isInvalid = Boolean(error); + + return ( + + ); + } +); +Input.displayName = "Input"; + +export { Input }; diff --git a/apps/portal/src/components/atoms/input.tsx b/apps/portal/src/components/atoms/input.tsx index 28d13abe..06091cae 100644 --- a/apps/portal/src/components/atoms/input.tsx +++ b/apps/portal/src/components/atoms/input.tsx @@ -1,3 +1,9 @@ +/** + * Input — now powered by shadcn/ui base styles + * + * Keeps the `error` prop from the legacy component for backward compatibility. + */ + import type { InputHTMLAttributes, ReactNode } from "react"; import { forwardRef } from "react"; import { cn } from "@/shared/utils"; @@ -13,16 +19,12 @@ const Input = forwardRef( return ( ; + +const Label = forwardRef(({ className, ...props }, ref) => { + return ( +