diff --git a/app/components/layout/sidebar/sidebar.tsx b/app/components/layout/sidebar/sidebar.tsx index 758d85338..448634d96 100644 --- a/app/components/layout/sidebar/sidebar.tsx +++ b/app/components/layout/sidebar/sidebar.tsx @@ -12,6 +12,7 @@ import { useFetcher } from "@remix-run/react"; import type { VariantProps } from "class-variance-authority"; import { cva } from "class-variance-authority"; +import { MenuIcon } from "lucide-react"; import Input from "~/components/forms/input"; import { SwitchIcon } from "~/components/icons/library"; import { Button } from "~/components/shared/button"; @@ -306,7 +307,12 @@ const SidebarTrigger = forwardRef< }} {...props} > - + + Toggle Sidebar ); diff --git a/app/components/shared/button.tsx b/app/components/shared/button.tsx index 8aa4dde7b..9c3d23e16 100644 --- a/app/components/shared/button.tsx +++ b/app/components/shared/button.tsx @@ -1,6 +1,6 @@ import React from "react"; import { ExternalLinkIcon } from "@radix-ui/react-icons"; -import { Link } from "@remix-run/react"; +import { Link, type LinkProps } from "@remix-run/react"; import { tw } from "~/utils/tw"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "./hover-card"; import type { IconType } from "./icons-map"; @@ -8,8 +8,7 @@ import Icon from "../icons/icon"; import type { ButtonVariant, ButtonWidth } from "../layout/header/types"; /** - * Type for the disabled prop - * We export it as its being used in other places as well + * Type for the disabled prop that can either be a boolean or an object with additional info */ export type DisabledProp = | boolean @@ -18,26 +17,134 @@ export type DisabledProp = reason: React.ReactNode | string; }; -export interface ButtonProps { - as?: React.ComponentType | string; +/** + * Defines valid button size options + */ +export type ButtonSize = "xs" | "sm" | "md"; +/** + * Common button props that all button types share + */ +export interface CommonButtonProps { className?: string; variant?: ButtonVariant; width?: ButtonWidth; - size?: "xs" | "sm" | "md"; + size?: ButtonSize; icon?: IconType; - /** Disabled can be a boolean */ - disabled?: DisabledProp; - attachToInput?: boolean; onlyIconOnMobile?: boolean; - title?: string; + error?: string; + hideErrorText?: boolean; + children?: React.ReactNode; + disabled?: DisabledProp; + label?: string; // Add label here since it's used in BookLink + id?: string; // Add id as an optional prop since some buttons might need it +} + +/** + * Props specific to HTML button elements + */ +export interface HTMLButtonProps + extends Omit, + Omit< + React.ButtonHTMLAttributes, + keyof CommonButtonProps | "disabled" + > { + as?: "button"; + to?: never; + disabled?: DisabledProp; +} + +/** + * Props specific to Link components + */ +export interface LinkButtonProps + extends CommonButtonProps, + Omit { + as?: typeof Link; + to: string; + target?: string; prefetch?: "none" | "intent" | "render" | "viewport"; +} + +/** + * Props for custom component buttons + */ +export interface CustomComponentButtonProps extends CommonButtonProps { + as: React.ComponentType; [key: string]: any; } +/** + * Union type of all possible button prop combinations + */ +export type ButtonProps = + | HTMLButtonProps + | LinkButtonProps + | CustomComponentButtonProps; +/** + * Type guard to check if props are for a Link button + */ +function isLinkProps(props: ButtonProps): props is LinkButtonProps { + return "to" in props; +} + +/** + * Style mappings for button variants + */ +const variants: Record = { + primary: tw( + `border-primary-400 bg-primary-500 text-white focus:ring-2`, + "disabled:border-primary-300 disabled:bg-primary-300", + "enabled:hover:bg-primary-400" + ), + secondary: tw( + `border-gray-300 bg-white text-gray-700`, + "disabled:text-gray-500", + "enabled:hover:bg-gray-50" + ), + tertiary: tw( + `border-b border-primary/10 pb-1 leading-none`, + "disabled:text-gray-300" + ), + link: tw( + `border-none p-0 text-text-sm font-semibold text-primary-700 hover:text-primary-800` + ), + "block-link": tw( + "-mt-1 border-none px-2 py-1 text-[14px] font-normal hover:bg-primary-50 hover:text-primary-600" + ), + "block-link-gray": tw( + "-mt-1 border-none px-2 py-1 text-[14px] font-normal hover:bg-gray-50 hover:text-gray-600" + ), + danger: tw( + `border-error-600 bg-error-600 text-white focus:ring-2`, + "disabled:border-error-300 disabled:bg-error-300", + "enabled:hover:bg-error-800" + ), +}; + +/** + * Style mappings for button sizes + */ +const sizes: Record = { + xs: tw("px-2 py-[6px] text-xs"), + sm: tw("px-[14px] py-2"), + md: tw("px-4 py-[10px]"), +}; + +/** + * Style mappings for button widths + */ +const widths: Record = { + auto: "w-auto", + full: "w-full max-w-full", +}; + +/** + * Button component that supports multiple variants, sizes, and can render as different elements + */ export const Button = React.forwardRef( - function Button(props: ButtonProps, ref) { - let { + function Button( + { as = "button", className, variant = "primary", @@ -50,145 +157,91 @@ export const Button = React.forwardRef( onlyIconOnMobile, error, hideErrorText = false, - target, - } = props; - const Component: React.ComponentType | string = props?.to ? Link : as; - const baseButtonClasses = `inline-flex items-center justify-center rounded font-semibold text-center gap-2 max-w-xl border text-sm box-shadow-xs`; - - const variants = { - primary: tw( - `border-primary-400 bg-primary-500 text-white focus:ring-2`, - disabled ? "border-primary-300 bg-primary-300" : "hover:bg-primary-400" - ), - secondary: tw( - `border-gray-300 bg-white text-gray-700 `, - disabled ? "text-gray-500" : "hover:bg-gray-50" - ), - tertiary: tw( - `border-b border-primary/10 pb-1 leading-none`, - disabled ? "text-gray-300" : "" - ), - link: tw( - `border-none p-0 text-text-sm font-semibold text-primary-700 hover:text-primary-800` - ), - "block-link": tw( - "-mt-1 border-none px-2 py-1 text-[14px] font-normal hover:bg-primary-50 hover:text-primary-600" - ), - - "block-link-gray": tw( - "-mt-1 border-none px-2 py-1 text-[14px] font-normal hover:bg-gray-50 hover:text-gray-600" - ), - danger: tw( - `border-error-600 bg-error-600 text-white focus:ring-2`, - disabled ? "border-error-300 bg-error-300" : "hover:bg-error-800" - ), - }; + ...props + }, + ref + ) { + const Component = isLinkProps(props) ? Link : as; + const baseButtonClasses = + "inline-flex items-center justify-center rounded font-semibold text-center gap-2 max-w-xl border text-sm box-shadow-xs"; - const sizes = { - xs: tw("px-2 py-[6px] text-xs"), - sm: tw("px-[14px] py-2"), - md: tw("px-4 py-[10px]"), - }; + const isDisabled = + typeof disabled === "boolean" ? disabled : disabled !== undefined; + const disabledReason = + typeof disabled === "object" ? disabled.reason : undefined; + const disabledTitle = + typeof disabled === "object" ? disabled.title : undefined; - const widths = { - auto: "w-auto", - full: "w-full max-w-full", - }; + // Type guard for checking if props has target property + const hasTarget = (props: ButtonProps): props is LinkButtonProps => + "target" in props; + const newTab = hasTarget(props) && props.target === "_blank"; - const disabledStyles = disabled - ? "opacity-50 cursor-not-allowed" - : undefined; - const attachedStyles = attachToInput - ? tw(" rounded-l-none border-l-0") - : undefined; + const buttonContent = ( + <> + {icon && } + {children && ( + + {children} + {newTab && ( + + )} + + )} + + ); const finalStyles = tw( baseButtonClasses, - sizes[size], - variants[variant], - widths[width], - disabledStyles, - attachedStyles, - className, - error - ? "border-error-300 focus:border-error-300 focus:ring-error-100" - : "" + sizes[size as ButtonSize], // Type assertion to ensure correct indexing + variants[variant as ButtonVariant], + widths[width as ButtonWidth], + isDisabled && "cursor-not-allowed opacity-50", + attachToInput && "rounded-l-none border-l-0", + error && "border-error-300 focus:border-error-300 focus:ring-error-100", + className ); - const isDisabled = - disabled === undefined // If it is undefined, then it is not disabled - ? false - : typeof disabled === "boolean" - ? disabled - : true; // If it is an object, then it is disabled - const reason = typeof disabled === "object" ? disabled.reason : ""; - const disabledTitle = - typeof disabled === "object" ? disabled.title : undefined; - - const newTab = target === "_blank"; - - if (isDisabled) { + // Render disabled button with hover card + if (isDisabled && disabledReason) { return ( - + ) => { - e.preventDefault(); - }} - onClick={(e: React.MouseEvent) => { - e.preventDefault(); - }} + onMouseDown={(e: React.MouseEvent) => e.preventDefault()} + onClick={(e: React.MouseEvent) => e.preventDefault()} > - {icon && }{" "} - {children ? ( - - {children} - - ) : null} + {buttonContent} - {reason && ( - -
- {typeof disabledStyles === "string" - ? disabledTitle - : "Action disabled"} -
-

{reason}

-
- )} + +
+ {disabledTitle || "Action disabled"} +
+

{disabledReason}

+
); } + + // Render normal button return ( <> - {icon && }{" "} - {children ? ( - - {children}{" "} - {newTab && ( - - )} - - ) : null} + {buttonContent} {!hideErrorText && error && (
{error}
@@ -197,3 +250,5 @@ export const Button = React.forwardRef( ); } ); + +Button.displayName = "Button"; diff --git a/app/components/shared/generic-add-to-bookings-actions-dropdown.tsx b/app/components/shared/generic-add-to-bookings-actions-dropdown.tsx index 626c65cab..9f26c6c44 100644 --- a/app/components/shared/generic-add-to-bookings-actions-dropdown.tsx +++ b/app/components/shared/generic-add-to-bookings-actions-dropdown.tsx @@ -1,7 +1,10 @@ import { ChevronRightIcon } from "@radix-ui/react-icons"; import { useHydrated } from "remix-utils/use-hydrated"; import Icon from "~/components/icons/icon"; -import type { ButtonProps, DisabledProp } from "~/components/shared/button"; +import type { + CommonButtonProps, + DisabledProp, +} from "~/components/shared/button"; import { Button } from "~/components/shared/button"; import { @@ -17,16 +20,21 @@ import When from "../when/when"; type IndexType = "kit" | "asset"; -export interface BookLink extends ButtonProps { +/** + * Extend CommonButtonProps instead of ButtonProps for the interface + */ +export interface BookLink extends CommonButtonProps { indexType: IndexType; + to: string; // Add required properties from LinkButtonProps + id: string; // Make id required for BookLink + icon?: "bookings" | "booking-exist"; // Narrow down the icon types } - const ConditionalActionsDropdown = ({ links, label, disabledTrigger, }: { - links: ButtonProps[]; + links: BookLink[]; label: string; disabledTrigger?: DisabledProp; diff --git a/app/components/table.tsx b/app/components/table.tsx index 9ffe7ec51..c314f5448 100644 --- a/app/components/table.tsx +++ b/app/components/table.tsx @@ -27,7 +27,7 @@ export function Table({
-
+
- + - +