@cloudwick/astral-ui-cli 1.0.0 → 2.0.1
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
CHANGED
|
@@ -1071,7 +1071,7 @@ async function promptForConfig({
|
|
|
1071
1071
|
// package.json
|
|
1072
1072
|
var package_default = {
|
|
1073
1073
|
name: "@cloudwick/astral-ui-cli",
|
|
1074
|
-
version: "
|
|
1074
|
+
version: "2.0.1",
|
|
1075
1075
|
description: "CLI for installing Astral UI components in any codebase",
|
|
1076
1076
|
main: "dist/index.js",
|
|
1077
1077
|
bin: {
|
|
@@ -1342,8 +1342,9 @@ var REGISTRY = {
|
|
|
1342
1342
|
"name": "accordion",
|
|
1343
1343
|
"description": "The Accordion is a collapsible container component that manages expansion and collapse states to show/hide content. It uses a compound component pattern with `Accordion.Header` and `Accordion.Body` subcomponents. **When to use:** - Displaying FAQs or help documentation - Organizing lengthy forms or content into sections - Creating collapsible navigation menus - Hiding optional or advanced settings until needed **Component Architecture:** - Built with React compound component pattern - Styled with Tailwind CSS and class-variance-authority (cva) - Uses CSS transitions for smooth expand/collapse animations - Manages internal state or can be controlled externally",
|
|
1344
1344
|
"dependencies": [
|
|
1345
|
-
"
|
|
1346
|
-
"react"
|
|
1345
|
+
"tailwind-variants",
|
|
1346
|
+
"react",
|
|
1347
|
+
"tailwind-merge"
|
|
1347
1348
|
],
|
|
1348
1349
|
"internalDependencies": [
|
|
1349
1350
|
"adpIcon",
|
|
@@ -1352,15 +1353,15 @@ var REGISTRY = {
|
|
|
1352
1353
|
"files": [
|
|
1353
1354
|
{
|
|
1354
1355
|
"name": "index.ts",
|
|
1355
|
-
"content": 'export {\n Accordion,\n type IAccordionProps,\n type IAccordionHeaderProps,\n type IAccordionBodyProps,\n type TAccordionSize,\n type IAccordionType\n} from "./accordion";\n\nexport {
|
|
1356
|
+
"content": 'export {\n Accordion,\n type IAccordionProps,\n type IAccordionHeaderProps,\n type IAccordionBodyProps,\n type TAccordionSize,\n type IAccordionType\n} from "./accordion";\n\nexport { accordionVariants } from "./accordionVariants";\n'
|
|
1356
1357
|
},
|
|
1357
1358
|
{
|
|
1358
1359
|
"name": "accordionVariants.ts",
|
|
1359
|
-
"content": 'import {
|
|
1360
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const accordionVariants = tv({\n slots: {\n base: [\n "bg-white dark:bg-iridium w-full min-w-[200px] rounded max-h-screen",\n "flex flex-col overflow-hidden border border-gray-200 dark:border-secondary-500",\n "text-secondary-500 dark:text-secondary-50"\n ],\n header: "w-full flex items-center dark:text-secondary-50 py-3",\n content: "transition-all duration-600 w-full"\n },\n variants: {\n size: {\n xs: { header: "text-base gap-3 px-3", content: "text-xs px-3" },\n sm: { header: "text-md gap-4 px-4", content: "text-base px-4" },\n md: { header: "text-lg gap-5 px-5", content: "text-md px-5" },\n lg: { header: "text-xl gap-6 px-6", content: "text-lg px-6" },\n xl: { header: "text-heading-xs gap-6 px-6", content: "text-xl px-6" }\n },\n expanded: {\n true: { content: "dark:text-secondary-200 overflow-y-auto pb-4 text-secondary-400" }\n }\n },\n defaultVariants: {\n size: "sm",\n expanded: false\n }\n});\n'
|
|
1360
1361
|
},
|
|
1361
1362
|
{
|
|
1362
1363
|
"name": "accordion.tsx",
|
|
1363
|
-
"content": 'import * as React from "react";\nimport { useEffect, useState, Children, type ReactElement } from "react";\
|
|
1364
|
+
"content": 'import * as React from "react";\nimport { useEffect, useState, useMemo, Children, type ReactElement } from "react";\nimport { cn } from "tailwind-variants";\n\nimport { ADPIcon, iconList } from "../adpIcon";\nimport { Button } from "../button";\nimport { accordionVariants } from "./accordionVariants";\n\nexport type TAccordionSize = "xs" | "sm" | "md" | "lg" | "xl";\n\nexport interface IAccordionHeaderProps {\n children: React.ReactNode;\n /**\n * Overwrite the styles for AccordionHeader by passing space separated class names\n */\n className?: string;\n}\n\nconst AccordionHeader = React.forwardRef<\n HTMLSpanElement,\n IAccordionHeaderProps\n>(({ children, className }, ref ) => (\n <span ref={ref} className={className}>{children}</span>\n));\n\nAccordionHeader.displayName = "AccordionHeader";\n\nexport interface IAccordionBodyProps {\n children: React.ReactNode | React.ReactElement;\n /**\n * Overwrite the styles for AccordionBody by passing space separated class names\n */\n className?: string;\n}\n\nconst AccordionBody = React.forwardRef<\n HTMLDivElement,\n IAccordionBodyProps\n>(({ children, className }, ref ) => (\n <div ref={ref} className={className}>{children}</div>\n));\n\nAccordionBody.displayName = "AccordionBody";\n\nexport interface IAccordionProps extends React.HTMLAttributes<HTMLDivElement> {\n children?: Array<ReactElement<IAccordionBodyProps | IAccordionHeaderProps>>;\n /**\n * Expand or collapse the Accordion\n */\n expand?: boolean;\n /**\n * Set it to false if you want the Accordion to Expand or Collapse only on click of the arrow\n */\n clickableHeader?: boolean;\n /**\n * Size of the Accordion Header (Text and Spacing)\n * @default sm\n */\n size?: TAccordionSize;\n /**\n * Overwrite the styles for Accordion by passing space separated class names\n */\n className?: string;\n}\n\nconst arrowSizeMapping: Record<TAccordionSize, "xs" | "sm"> = {\n xs: "xs",\n sm: "xs",\n md: "xs",\n lg: "sm",\n xl: "sm"\n};\n\nconst AccordionComponent = React.forwardRef<HTMLDivElement, IAccordionProps>(\n ({\n children,\n expand,\n clickableHeader = true,\n className,\n size = "sm",\n ...props\n }, ref ) => {\n const [ expanded, setExpanded ] = useState<boolean>( false );\n const [ bodyProps, setBodyProps ] = useState<IAccordionBodyProps>();\n const [ headerProps, setHeaderProps ] = useState<IAccordionBodyProps>();\n\n const clickHandler = () => setExpanded(( isExpanded ) => !isExpanded );\n\n useEffect(() => setExpanded( Boolean( expand )), [expand]);\n\n useEffect(() => {\n if ( children ) {\n Children.forEach( children, ( child: ReactElement<IAccordionBodyProps | IAccordionHeaderProps> ) => {\n if ( child.type === AccordionHeader || child.type === ( Accordion as any ).Header ) {\n setHeaderProps( child.props );\n } else if ( child.type === AccordionBody || child.type === ( Accordion as any ).Body ) {\n setBodyProps( child.props );\n }\n });\n }\n }, [children]);\n\n const { base, header, content } = useMemo(\n () => accordionVariants({ size, expanded }),\n [ size, expanded ]\n );\n\n if ( !children ) {\n return null;\n }\n\n return (\n <div\n ref={ref}\n className={base({ class: className })}\n {...props}\n >\n {clickableHeader ? (\n <button\n onClick={clickHandler}\n type="button"\n className={header({ class: [ "w-full justify-start", headerProps?.className ] })}\n aria-expanded={expanded ? "true" : "false"}\n aria-controls="bodySection"\n data-testid="accordionHeader"\n >\n <ADPIcon\n className={cn(\n "text-gray-500 dark:text-gray-800 transition-all duration-700 rtl:rotate-180",\n expanded && "rotate-90"\n )}\n size={arrowSizeMapping[size]}\n icon={iconList.rightArrow}\n fixedWidth\n />\n {headerProps?.children}\n </button>\n ) : (\n <div\n aria-expanded={expanded ? "true" : "false"}\n aria-controls="bodySection"\n className={header({ class: headerProps?.className })}\n data-testid="accordionHeader"\n >\n <Button variant="text" onClick={clickHandler} color="secondary">\n <ADPIcon\n className={cn(\n "text-gray-500 dark:text-gray-800 transition-all duration-700 rtl:rotate-180",\n expanded && "rotate-90"\n )}\n size={arrowSizeMapping[size]}\n icon={iconList.rightArrow}\n fixedWidth\n />\n </Button>\n {headerProps?.children}\n </div>\n )}\n\n <div\n data-testid="accordionBody"\n className={content({ class: bodyProps?.className })}\n >\n {expanded && bodyProps?.children}\n </div>\n </div>\n );\n }\n);\n\nAccordionComponent.displayName = "Accordion";\n\n// Define the type that includes subcomponents\nexport interface IAccordionType extends React.ForwardRefExoticComponent<IAccordionProps & React.RefAttributes<HTMLDivElement>> {\n Header: typeof AccordionHeader;\n Body: typeof AccordionBody;\n}\n\n// Add subcomponents with type assertion\n( AccordionComponent as any ).Header = AccordionHeader;\n( AccordionComponent as any ).Body = AccordionBody;\n\n// Export with proper type\nexport const Accordion = AccordionComponent as IAccordionType;\n'
|
|
1364
1365
|
},
|
|
1365
1366
|
{
|
|
1366
1367
|
"name": "README.md",
|
|
@@ -1374,7 +1375,8 @@ var REGISTRY = {
|
|
|
1374
1375
|
"description": "The ADPIcon component renders icons from the icon library with consistent sizing and styling. It supports both predefined icons from the icon library and custom SVG icons. **When to use:** - Button icons - Navigation icons - Status indicators - Decorative elements - Loading spinners - Custom brand icons **Component Architecture:** - Styled with Tailwind CSS and cva - Multiple size variants - Spin animation support - Fixed width option - Custom SVG support with validation - Forwards refs to SVG element",
|
|
1375
1376
|
"dependencies": [
|
|
1376
1377
|
"react",
|
|
1377
|
-
"
|
|
1378
|
+
"tailwind-variants",
|
|
1379
|
+
"tailwind-merge"
|
|
1378
1380
|
],
|
|
1379
1381
|
"internalDependencies": [],
|
|
1380
1382
|
"files": [
|
|
@@ -1388,11 +1390,11 @@ var REGISTRY = {
|
|
|
1388
1390
|
},
|
|
1389
1391
|
{
|
|
1390
1392
|
"name": "adpIconVariants.ts",
|
|
1391
|
-
"content": 'import {
|
|
1393
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const adpIconVariants = tv({\n base: "fill-current",\n variants: {\n variant: {\n default: ""\n },\n fixedWidth: {\n true: "aspect-auto text-center"\n },\n padded: {\n true: "p-1 rounded-sm"\n },\n spin: {\n true: "animate-spin-2s"\n }\n },\n defaultVariants: {\n variant: "default",\n fixedWidth: false,\n padded: false,\n spin: false\n }\n});\n\n// Export the size proportions for calculations\nexport const iconSizeProportions = {\n xxs: 0.5,\n xs: 0.667,\n sm: 0.835,\n md: 1,\n lg: 1.25,\n xl: 1.5,\n "2xl": 2\n};\n'
|
|
1392
1394
|
},
|
|
1393
1395
|
{
|
|
1394
1396
|
"name": "adpIcon.tsx",
|
|
1395
|
-
"content": 'import * as React from "react";\
|
|
1397
|
+
"content": 'import * as React from "react";\n\nimport { adpIconVariants, iconSizeProportions } from "./adpIconVariants";\nimport { type IIconDetails, type TIconType, iconList, icons } from "./svgObjects";\n\nexport { iconList };\n\n/**\n * Icon component for ADP icons\n *\n * @example\n * ```tsx\n * <ADPIcon icon="overview" size="md" />\n * ```\n */\nexport interface IADPIconProps extends Omit<React.SVGAttributes<SVGSVGElement>, "size"> {\n /**\n * Icon name from the list of available icons\n * @default undefined\n */\n icon?: TIconType;\n /**\n * Size of the icon. The size is proportional to the initial size of the icon.\n * @default "sm"\n */\n size?: "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl";\n /**\n * Adds a spinning animation to the icon\n */\n spin?: boolean;\n /**\n * adds padding and rounded corners to the icon\n */\n padded?: boolean;\n /**\n * Title attribute for the icon element\n * @default undefined\n */\n title?: string;\n /**\n * Data attribute for icon-name, override the icon name attribute\n */\n data_icon_name?: string;\n /**\n * Makes the icon of fixed width and center aligned\n * @default false\n */\n fixedWidth?: boolean;\n /**\n * Custom SVG object to be used instead of one of the default icons.\n * The custom SVG object should have the following properties:\n * - SVGPath: ReactElement (required) (SVG path element)\n * - xOffset: number (optional)\n * - yOffset: number (optional)\n * - initialHeight: number (optional)\n * - initialWidth: number (optional)\n * @default undefined\n */\n customSVG?: IIconDetails;\n}\n\n/**\n * Astral UI Icon component\n *\n * This component renders an SVG icon from the set of predefined icons or a custom SVG object.\n */\nexport const ADPIcon = React.forwardRef<SVGSVGElement, IADPIconProps>(\n ({\n icon,\n size = "sm",\n spin = false,\n padded = false,\n title,\n data_icon_name,\n fixedWidth = false,\n customSVG,\n className,\n ...props\n }, ref ) => {\n if ( typeof customSVG === "undefined" && ( !icon || !icons?.[icon])) {\n console.error( `Please provide a valid icon (${icon}) or a customSVG object.` );\n return null;\n }\n\n const {\n xOffset = 0,\n yOffset = 0,\n initialHeight = 24,\n initialWidth = 24,\n SVGPath\n } = ( customSVG ?? icons[icon!]) as IIconDetails;\n\n const requiredHeight = ( iconSizeProportions[size] + ( padded ? 0.25 : 0 )) * initialHeight;\n const requiredWidth = ( iconSizeProportions[size] + ( padded ? 0.25 : 0 )) * ( fixedWidth ? 24 : initialWidth );\n\n const iconClasses = adpIconVariants({ spin, padded, fixedWidth, class: className });\n\n return (\n <svg\n data-testid="icon"\n className={iconClasses}\n width={requiredWidth}\n height={requiredHeight}\n viewBox={`${xOffset} ${yOffset} ${initialWidth} ${initialHeight}`}\n fill="none"\n data-icon={data_icon_name ?? icon}\n xmlns="http://www.w3.org/2000/svg"\n aria-hidden="true"\n ref={ref}\n {...props}\n >\n <>\n {title && <title>{title}</title>}\n {SVGPath}\n </>\n </svg>\n );\n }\n);\n\nADPIcon.displayName = "ADPIcon";\n'
|
|
1396
1398
|
},
|
|
1397
1399
|
{
|
|
1398
1400
|
"name": "README.md",
|
|
@@ -1405,22 +1407,23 @@ var REGISTRY = {
|
|
|
1405
1407
|
"name": "avatar",
|
|
1406
1408
|
"description": "The Avatar component displays user profile images or initials in a circular or rounded container. **When to use:** - User profile displays - Comment sections - Team member lists - Chat interfaces - Author attribution **Component Architecture:** - Styled with Tailwind CSS and cva - Automatic fallback to initials - Multiple size variants - Support for images and placeholder text",
|
|
1407
1409
|
"dependencies": [
|
|
1408
|
-
"
|
|
1409
|
-
"react"
|
|
1410
|
+
"tailwind-variants",
|
|
1411
|
+
"react",
|
|
1412
|
+
"tailwind-merge"
|
|
1410
1413
|
],
|
|
1411
1414
|
"internalDependencies": [],
|
|
1412
1415
|
"files": [
|
|
1413
1416
|
{
|
|
1414
1417
|
"name": "index.ts",
|
|
1415
|
-
"content": 'export { Avatar, type IAvatarProps } from "./avatar";\nexport { avatarVariants
|
|
1418
|
+
"content": 'export { Avatar, type IAvatarProps } from "./avatar";\nexport { avatarVariants } from "./avatarVariants";'
|
|
1416
1419
|
},
|
|
1417
1420
|
{
|
|
1418
1421
|
"name": "avatarVariants.ts",
|
|
1419
|
-
"content": 'import {
|
|
1422
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const avatarVariants = tv({\n slots: {\n base: [\n "relative flex shrink-0 overflow-hidden rounded-full border-1.5 border-white",\n "bg-primary-100 text-primary-500 dark:border-secondary-500 dark:bg-secondary-600 dark:text-primary-400"\n ],\n image: "aspect-square h-full w-full",\n fallback: "flex h-full w-full items-center justify-center rounded-full font-medium"\n },\n variants: {\n size: {\n sm: { base: "h-7 w-7 text-xxs" },\n md: { base: "h-8 w-8 text-xs" },\n lg: { base: "h-9 w-9 text-md" },\n xl: { base: "h-11 w-11 text-lg" }\n }\n },\n defaultVariants: {\n size: "md"\n }\n});\n'
|
|
1420
1423
|
},
|
|
1421
1424
|
{
|
|
1422
1425
|
"name": "avatar.tsx",
|
|
1423
|
-
"content": 'import * as React from "react";\
|
|
1426
|
+
"content": 'import * as React from "react";\nimport { avatarVariants } from "./avatarVariants";\n\n/**\n * Avatar component for displaying user profile images with fallback support\n */\nexport interface IAvatarProps extends React.HTMLAttributes<HTMLDivElement> {\n /** The image source URL */\n img?: string;\n /** The label/name to display as fallback */\n label?: string;\n /** The size of the avatar */\n size?: "sm" | "md" | "lg" | "xl";\n /** Alt text for the avatar image */\n alt?: string;\n /** If true, the label will be abbreviated to its initials */\n abbreviate?: boolean;\n}\n\n/**\n * Avatar component for displaying user profile images with fallback support\n */\nexport const Avatar = React.forwardRef<HTMLDivElement, IAvatarProps>(\n ({ className, img, label, size = "md", alt, abbreviate = true, ...props }, ref ) => {\n const [ error, setError ] = React.useState( false );\n const { base, image, fallback } = avatarVariants({ size });\n\n // Generate initials from label for fallback\n const getInitials = ( name: string ): string => {\n if ( !name ) {\n return "";\n }\n const nameParts = name.split( " " ).filter( Boolean );\n if ( nameParts.length === 1 ) {\n return nameParts[0].charAt( 0 ).toUpperCase();\n }\n return ( nameParts[0].charAt( 0 ) + nameParts[nameParts.length - 1].charAt( 0 )).toUpperCase();\n };\n\n if ( !label ) {\n return null;\n }\n\n return (\n <div\n ref={ref}\n className={base({ class: className })}\n {...props}\n >\n {img && !error ? (\n <img\n src={img}\n alt={alt || label}\n loading="lazy"\n className={image()}\n onError={() => setError( true )}\n />\n ) : (\n <div className={fallback()}>\n <span>{abbreviate ? getInitials( label ) : label}</span>\n </div>\n )}\n </div>\n );\n }\n);\n\nAvatar.displayName = "Avatar";\n'
|
|
1424
1427
|
},
|
|
1425
1428
|
{
|
|
1426
1429
|
"name": "README.md",
|
|
@@ -1433,8 +1436,9 @@ var REGISTRY = {
|
|
|
1433
1436
|
"name": "avatarGroup",
|
|
1434
1437
|
"description": "The AvatarGroup component displays multiple avatars in a stacked or inline layout, with overflow indicator. **When to use:** - Showing team members - Displaying contributors - Showing participants in a chat or meeting - User lists with limited space **Component Architecture:** - Composed of multiple Avatar components - Styled with Tailwind CSS and cva - Supports overflow count display - Multiple layout options",
|
|
1435
1438
|
"dependencies": [
|
|
1436
|
-
"
|
|
1437
|
-
"react"
|
|
1439
|
+
"tailwind-variants",
|
|
1440
|
+
"react",
|
|
1441
|
+
"tailwind-merge"
|
|
1438
1442
|
],
|
|
1439
1443
|
"internalDependencies": [
|
|
1440
1444
|
"avatar"
|
|
@@ -1446,11 +1450,11 @@ var REGISTRY = {
|
|
|
1446
1450
|
},
|
|
1447
1451
|
{
|
|
1448
1452
|
"name": "avatarGroupVariants.ts",
|
|
1449
|
-
"content": 'import {
|
|
1453
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const avatarGroupVariants = tv({\n base: "inline-flex items-center flex-wrap -space-x-3",\n variants: {\n clickable: {\n true: "cursor-pointer hover:opacity-90 transition-opacity",\n false: ""\n }\n },\n defaultVariants: {\n clickable: false\n }\n});\n'
|
|
1450
1454
|
},
|
|
1451
1455
|
{
|
|
1452
1456
|
"name": "avatarGroup.tsx",
|
|
1453
|
-
"content": 'import * as React from "react";\n\nimport { abbreviateNumber
|
|
1457
|
+
"content": 'import * as React from "react";\n\nimport { abbreviateNumber } from "@utils";\nimport { avatarGroupVariants } from "./avatarGroupVariants";\nimport { Avatar, type IAvatarProps } from "../avatar";\n\n/**\n * AvatarGroup component for displaying a group of avatars with overflow handling\n */\nexport interface IAvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Accept children\n */\n children: React.ReactElement<IAvatarProps> | Array<React.ReactElement<IAvatarProps>>;\n /**\n * Callback to be called when the AvatarGroup is clicked\n */\n onClick?: () => void;\n /**\n * Maximum items to display in the group before the custom avatar is shown\n * @default 4\n */\n maxItems?: number;\n /**\n * Size of the Avatars\n * @default md\n */\n size?: IAvatarProps["size"];\n /**\n * Overwrite the styles for the AvatarGroup by passing space separated class names\n */\n className?: string;\n}\n\n/**\n * Filter children to only include valid Avatar components with a label\n */\nconst filterChildren = (\n children: React.ReactElement<IAvatarProps> | Array<React.ReactElement<IAvatarProps>>\n) => {\n const filteredArray: React.ReactElement<IAvatarProps>[] = [];\n\n if ( Array.isArray( children )) {\n children.forEach(( child: React.ReactElement<IAvatarProps> ) => {\n const { label } = child.props;\n if ( child.type === Avatar && label ) {\n filteredArray.push( child );\n }\n });\n } else {\n const { label } = children.props;\n if ( children.type === Avatar && label ) {\n filteredArray.push( children );\n }\n }\n\n return filteredArray;\n};\n\n/**\n * AvatarGroup component for displaying a group of avatars with overflow handling\n */\nexport const AvatarGroup = React.forwardRef<HTMLDivElement, IAvatarGroupProps>(\n ({ size = "md", maxItems = 4, className, children, onClick, ...props }, ref ) => {\n const setMaxItems = maxItems && maxItems > 1 ? maxItems : 4;\n const filteredChildren = React.useMemo(() => filterChildren( children ), [children]);\n\n const isClickable = Boolean( onClick );\n\n // Create the avatar elements\n const createAvatarElements = () => {\n return (\n <>\n {filteredChildren.slice( 0, setMaxItems ).map(( avatar, index ) => (\n <Avatar\n {...avatar.props}\n key={`avatar-group-avatar-${index}`}\n size={size}\n />\n ))}\n {filteredChildren.length > setMaxItems && (\n <Avatar\n label={`+${abbreviateNumber( filteredChildren.length - setMaxItems, 1, false )}`}\n abbreviate={false}\n size={size}\n />\n )}\n </>\n );\n };\n\n if ( filteredChildren.length === 0 ) {\n return null;\n }\n\n const groupClasses = avatarGroupVariants({ clickable: isClickable, class: className });\n\n return (\n <div\n ref={ref}\n className={groupClasses}\n onClick={onClick}\n role={onClick ? "button" : undefined}\n tabIndex={onClick ? 0 : undefined}\n {...props}\n >\n {createAvatarElements()}\n </div>\n );\n }\n);\n\nAvatarGroup.displayName = "AvatarGroup";\n'
|
|
1454
1458
|
},
|
|
1455
1459
|
{
|
|
1456
1460
|
"name": "README.md",
|
|
@@ -1463,8 +1467,9 @@ var REGISTRY = {
|
|
|
1463
1467
|
"name": "badge",
|
|
1464
1468
|
"description": "The Badge component displays small count indicators, status labels, or categorical information. **When to use:** - Status indicators (active, pending, error) - Notification counts - Category labels - Version tags - Feature flags **Component Architecture:** - Styled with Tailwind CSS and cva - Multiple color variants - Size variants - Optional dismiss functionality",
|
|
1465
1469
|
"dependencies": [
|
|
1466
|
-
"
|
|
1467
|
-
"react"
|
|
1470
|
+
"tailwind-variants",
|
|
1471
|
+
"react",
|
|
1472
|
+
"tailwind-merge"
|
|
1468
1473
|
],
|
|
1469
1474
|
"internalDependencies": [],
|
|
1470
1475
|
"files": [
|
|
@@ -1474,15 +1479,15 @@ var REGISTRY = {
|
|
|
1474
1479
|
},
|
|
1475
1480
|
{
|
|
1476
1481
|
"name": "badgeVariants.ts",
|
|
1477
|
-
"content": 'import {
|
|
1482
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const badgeVariants = tv({\n base: "inline-flex justify-center items-center gap-1 rounded-full font-medium ease-in-out duration-300 bg-transparent",\n variants: {\n variant: {\n filled: "",\n ghost: "",\n outline: "",\n text: "p-0"\n },\n size: {\n sm: "text-xs py-1 px-1.5",\n md: "text-base py-1 px-2",\n lg: "text-md py-1 px-2"\n },\n color: {\n primary: "text-primary-500 dark:text-primary-400",\n secondary: "text-secondary-500 dark:text-secondary-200",\n success: "text-success-500 dark:text-success-400",\n warning: "text-warning-500 dark:text-warning-400",\n error: "text-error-500 dark:text-error-400"\n }\n },\n compoundVariants: [\n // Primary variants\n {\n variant: "filled",\n color: "primary",\n class: "text-white bg-primary-500 dark:text-iridium dark:bg-primary-400"\n },\n {\n variant: "ghost",\n color: "primary",\n class: "text-primary-500 bg-primary-50 dark:text-primary-200 dark:bg-primary-900"\n },\n {\n variant: "outline",\n color: "primary",\n class: "border border-primary-100 dark:text-primary-400 dark:bg-iridium dark:border-primary-400"\n },\n {\n variant: "text",\n color: "primary",\n class: "dark:text-primary-400"\n },\n\n // Secondary variants\n {\n variant: "filled",\n color: "secondary",\n class: "bg-gray-100 dark:text-secondary-200 dark:bg-secondary-600"\n },\n {\n variant: "ghost",\n color: "secondary",\n class: "bg-gray-100 dark:text-secondary-200 dark:bg-secondary-600"\n },\n {\n variant: "outline",\n color: "secondary",\n class: "border border-gray-100 dark:text-secondary-200 dark:bg-iridium dark:border-secondary-500"\n },\n {\n variant: "text",\n color: "secondary",\n class: "dark:text-secondary-200"\n },\n\n // Success variants\n {\n variant: "filled",\n color: "success",\n class: "text-white bg-success-500 dark:text-iridium dark:bg-success-400"\n },\n {\n variant: "ghost",\n color: "success",\n class: "text-success-700 bg-success-50 dark:text-success-200 dark:bg-success-900"\n },\n {\n variant: "outline",\n color: "success",\n class: "border border-success-100 dark:text-success-400 dark:bg-iridium dark:border-success-400"\n },\n {\n variant: "text",\n color: "success",\n class: "dark:text-success-200"\n },\n\n // Warning variants\n {\n variant: "filled",\n color: "warning",\n class: "text-white bg-warning-500 dark:text-iridium dark:bg-warning-400"\n },\n {\n variant: "ghost",\n color: "warning",\n class: "text-warning-700 bg-warning-50 dark:text-warning-200 dark:bg-warning-900"\n },\n {\n variant: "outline",\n color: "warning",\n class: "border border-warning-100 dark:text-warning-400"\n },\n {\n variant: "text",\n color: "warning",\n class: "dark:text-warning-200"\n },\n\n // Error variants\n {\n variant: "filled",\n color: "error",\n class: "text-white bg-error-500 dark:text-iridium dark:bg-error-400"\n },\n {\n variant: "ghost",\n color: "error",\n class: "text-error-700 bg-error-50 dark:text-error-200 dark:bg-error-900"\n },\n {\n variant: "outline",\n color: "error",\n class: "border border-error-100 dark:text-error-400 dark:border-error-400"\n },\n {\n variant: "text",\n color: "error",\n class: "dark:text-error-200"\n }\n ],\n defaultVariants: {\n variant: "filled",\n size: "md",\n color: "primary"\n }\n});\n'
|
|
1478
1483
|
},
|
|
1479
1484
|
{
|
|
1480
1485
|
"name": "badge.tsx",
|
|
1481
|
-
"content": 'import * as React from "react";\nimport {
|
|
1486
|
+
"content": 'import * as React from "react";\nimport { badgeVariants } from "./badgeVariants";\n\n/**\n * Badge component for displaying small counts, labels, or statuses\n */\nexport interface IBadgeProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Text to be displayed inside the badge */\n label?: React.ReactNode;\n /** Icon to be displayed before the label */\n prefixIcon?: React.ReactNode;\n /** Icon to be displayed after the label */\n suffixIcon?: React.ReactNode;\n /** Badge appearance style */\n variant?: "filled" | "ghost" | "outline" | "text";\n /** Badge size */\n size?: "sm" | "md" | "lg";\n /** Badge color scheme */\n color?: "primary" | "secondary" | "success" | "warning" | "error";\n}\n\n/**\n * Badge component for displaying small counts, labels, or statuses\n */\nexport const Badge = React.forwardRef<HTMLDivElement, IBadgeProps>(\n (\n {\n className,\n label,\n prefixIcon,\n suffixIcon,\n variant = "filled",\n size = "md",\n color = "primary",\n ...props\n },\n ref\n ) => {\n const classes = badgeVariants({ variant, size, color, class: className });\n\n return (\n <div\n ref={ref}\n className={classes}\n {...props}\n >\n {prefixIcon && <span className="mr-1 inline-flex">{prefixIcon}</span>}\n {label && <span>{label}</span>}\n {suffixIcon && <span className="ml-1 inline-flex">{suffixIcon}</span>}\n </div>\n );\n }\n);\n\nBadge.displayName = "Badge";\n'
|
|
1482
1487
|
},
|
|
1483
1488
|
{
|
|
1484
1489
|
"name": "README.md",
|
|
1485
|
-
"content": "# Badge Component\n\n## Component Overview\n\nThe Badge component displays small count indicators, status labels, or categorical information.\n\n**When to use:**\n- Status indicators (active, pending, error)\n- Notification counts\n- Category labels\n- Version tags\n- Feature flags\n\n**Component Architecture:**\n- Styled with Tailwind CSS and cva\n- Multiple color variants\n- Size variants\n- Optional dismiss functionality\n\n## Basic Usage\n\n```tsx\nimport { Badge } from \"@components\";\n\nexport function Example() {\n return <Badge>New</Badge>;\n}\n```\n\n**Note:** Update the import path based on your project structure. If you used the Astral UI CLI, the typical import path would be `@components/ui/badge` or `@components/badge`.\n\n## Component Props\n\n| Prop | Type | Default | Required | Description |\n|------|------|---------|----------|-------------|\n| `label` | `
|
|
1490
|
+
"content": "# Badge Component\n\n## Component Overview\n\nThe Badge component displays small count indicators, status labels, or categorical information.\n\n**When to use:**\n- Status indicators (active, pending, error)\n- Notification counts\n- Category labels\n- Version tags\n- Feature flags\n\n**Component Architecture:**\n- Styled with Tailwind CSS and cva\n- Multiple color variants\n- Size variants\n- Optional dismiss functionality\n\n## Basic Usage\n\n```tsx\nimport { Badge } from \"@components\";\n\nexport function Example() {\n return <Badge>New</Badge>;\n}\n```\n\n**Note:** Update the import path based on your project structure. If you used the Astral UI CLI, the typical import path would be `@components/ui/badge` or `@components/badge`.\n\n## Component Props\n\n| Prop | Type | Default | Required | Description |\n|------|------|---------|----------|-------------|\n| `label` | `React.ReactNode` | `undefined` | No | Text to be displayed inside the badge |\n| `prefixIcon` | `React.ReactNode` | `undefined` | No | Icon to be displayed before the label |\n| `suffixIcon` | `React.ReactNode` | `undefined` | No | Icon to be displayed after the label |\n| `variant` | `'filled' \\| 'ghost' \\| 'outline' \\| 'text'` | `'filled'` | No | Badge appearance style |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` | No | Badge size |\n| `color` | `'primary' \\| 'secondary' \\| 'success' \\| 'warning' \\| 'error'` | `'primary'` | No | Badge color scheme |\n| `className` | `string` | `undefined` | No | Additional CSS classes |\n\n**Note:** This component forwards refs to the container div element (`HTMLDivElement`).\n\n## Practical Examples\n\n### Status Badges\n\n```tsx\nimport { Badge } from \"@components\";\n\nfunction StatusBadges() {\n return (\n <div className=\"flex gap-2\">\n <Badge color=\"success\">Active</Badge>\n <Badge color=\"warning\">Pending</Badge>\n <Badge color=\"error\">Failed</Badge>\n <Badge color=\"secondary\">Draft</Badge>\n </div>\n );\n}\n```\n\n### Notification Count\n\n```tsx\nimport { Badge } from \"@components\";\nimport { Button } from \"@components\";\n\nfunction NotificationButton() {\n return (\n <div className=\"relative inline-block\">\n <Button prefixIcon=\"bell\">Notifications</Button>\n <Badge \n className=\"absolute -top-2 -right-2\" \n size=\"sm\"\n color=\"error\"\n >\n 5\n </Badge>\n </div>\n );\n}\n```\n"
|
|
1486
1491
|
}
|
|
1487
1492
|
],
|
|
1488
1493
|
"directories": []
|
|
@@ -1491,8 +1496,9 @@ var REGISTRY = {
|
|
|
1491
1496
|
"name": "breadcrumbs",
|
|
1492
1497
|
"description": "The Breadcrumbs component provides hierarchical navigation showing the user's location within the application structure. **When to use:** - Multi-level navigation structures - File system or folder navigation - E-commerce category navigation - Documentation sites - Any hierarchical content structure **Component Architecture:** - Styled with Tailwind CSS and cva - Supports custom separators - Keyboard accessible - SEO-friendly structure",
|
|
1493
1498
|
"dependencies": [
|
|
1494
|
-
"
|
|
1495
|
-
"react"
|
|
1499
|
+
"tailwind-variants",
|
|
1500
|
+
"react",
|
|
1501
|
+
"tailwind-merge"
|
|
1496
1502
|
],
|
|
1497
1503
|
"internalDependencies": [
|
|
1498
1504
|
"adpIcon"
|
|
@@ -1500,19 +1506,19 @@ var REGISTRY = {
|
|
|
1500
1506
|
"files": [
|
|
1501
1507
|
{
|
|
1502
1508
|
"name": "index.ts",
|
|
1503
|
-
"content": '
|
|
1509
|
+
"content": 'export { Breadcrumbs, type IBreadcrumbsProps } from "./breadcrumbs";\nexport { BreadcrumbItem, type IBreadcrumbItemProps } from "./breadcrumbItem";\nexport { breadcrumbsVariants } from "./breadcrumbsVariants";'
|
|
1504
1510
|
},
|
|
1505
1511
|
{
|
|
1506
1512
|
"name": "breadcrumbsVariants.ts",
|
|
1507
|
-
"content": 'import {
|
|
1513
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const breadcrumbsVariants = tv({\n slots: {\n root: "hidden md:block",\n list: "list-none inline-flex flex-wrap items-center gap-2",\n separator: "select-none text-secondary-300 dark:text-secondary-200",\n svgIcon: "h-3",\n dotSeparator: "text-lg leading-none",\n item: "text-base text-secondary-300 dark:text-secondary-300"\n },\n variants: {\n separatorType: {\n arrow: {},\n dot: {}\n },\n active: {\n true: { item: "font-semibold text-primary-500 dark:text-primary-400 cursor-default" },\n false: {}\n }\n },\n defaultVariants: {\n separatorType: "arrow",\n active: false\n }\n});\n'
|
|
1508
1514
|
},
|
|
1509
1515
|
{
|
|
1510
1516
|
"name": "breadcrumbs.tsx",
|
|
1511
|
-
"content": 'import * as React from "react";\nimport {
|
|
1517
|
+
"content": 'import * as React from "react";\nimport { breadcrumbsVariants } from "./breadcrumbsVariants";\nimport { BreadcrumbItem, type IBreadcrumbItemProps } from "./breadcrumbItem";\nimport { ADPIcon, iconList } from "../adpIcon";\n\n/**\n * Props for the Breadcrumbs component\n */\nexport interface IBreadcrumbsProps extends React.HTMLAttributes<HTMLElement> {\n /**\n * The maximum number of items to display in the breadcrumbs\n * @default 10\n * @range 2 - maxItems\n */\n maxItems?: number;\n /**\n * Accepts Breadcrumbs.Item[]\n */\n children?: React.ReactElement<IBreadcrumbItemProps> | Array<React.ReactElement<IBreadcrumbItemProps>>;\n /**\n * Determines the separator type: "arrow" for ">", "dot" for "."\n * @default "arrow"\n */\n separatorType?: "arrow" | "dot";\n}\n\n/**\n * Separator component for breadcrumb items\n */\nconst BreadcrumbSeparator = ({\n separatorType = "arrow"\n}: {\n separatorType?: "arrow" | "dot"\n}): React.ReactElement => {\n const { separator, svgIcon, dotSeparator } = breadcrumbsVariants({ separatorType });\n return (\n <li\n className={separator()}\n aria-hidden="true"\n >\n {separatorType === "arrow" ? (\n <ADPIcon\n size="xs"\n className={svgIcon({ class: "rtl:rotate-180" })}\n icon={iconList.rightArrow}\n />\n ) : (\n // Using Unicode character {"\\u2022"} for the dot separator\n <span className={dotSeparator()}>{"\\u2022"}</span>\n )}\n </li>\n );\n};\n\n/**\n * Breadcrumbs component for navigation\n */\nconst BreadcrumbsComponent = React.forwardRef<HTMLElement, IBreadcrumbsProps>(\n ({\n maxItems = 10,\n className,\n children,\n separatorType = "arrow",\n ...props\n }, ref ) => {\n const [ showAll, toggleShowAll ] = React.useReducer(( state ) => !state, false );\n const [ crumbsList, setCrumbsList ] = React.useState<Array<React.ReactElement<IBreadcrumbItemProps>>>([]);\n const maxDisplayItems = Math.max( maxItems, 2 );\n\n React.useEffect(() => {\n const crumbs = [];\n if ( Array.isArray( children )) {\n const totalItems = children.length - 1;\n if (( totalItems < maxDisplayItems ) || showAll ) {\n for ( let i = 0; i < totalItems; i++ ) {\n if ( !React.isValidElement( children[i])) {\n return;\n }\n crumbs.push(\n React.cloneElement( children[i], { active: false, key: i }),\n <BreadcrumbSeparator key={`separator${i}`} separatorType={separatorType} />\n );\n }\n } else {\n if ( React.isValidElement( children[0])) {\n crumbs.push(\n React.cloneElement( children[0], { active: false, key: "first-item" }),\n <BreadcrumbSeparator key="first-item-separator" separatorType={separatorType} />\n );\n }\n crumbs.push(\n React.cloneElement( <BreadcrumbItem>\n <button onClick={toggleShowAll} title={`${totalItems - maxDisplayItems + 1} more item(s)`}>\n <span className="text-base">...</span>\n </button>\n </BreadcrumbItem>, { active: false, key: "more-items" }),\n <BreadcrumbSeparator key="more-items-separator" separatorType={separatorType} />\n );\n for ( let j = totalItems - maxDisplayItems + 2; j < totalItems; j++ ) {\n if ( !React.isValidElement( children[j])) {\n return;\n }\n crumbs.push(\n React.cloneElement( children[j], { active: false, key: j }),\n <BreadcrumbSeparator key={`${j}-separator`} separatorType={separatorType} />\n );\n }\n }\n crumbs.push( React.cloneElement( children[totalItems], { active: true, key: "lastItem" }));\n } else if ( React.isValidElement( children )) {\n const clone = React.cloneElement( children, { active: true });\n crumbs.push( clone );\n }\n setCrumbsList( crumbs );\n }, [ children, maxDisplayItems, showAll, separatorType ]);\n\n const { root, list } = breadcrumbsVariants();\n\n return crumbsList?.length > 0\n ? <nav\n ref={ref as React.RefObject<HTMLElement>}\n aria-label="Breadcrumb"\n className={root({ class: className })}\n {...props}\n >\n <ol className={list()}>\n {crumbsList}\n </ol>\n </nav>\n : null;\n }\n);\n\nBreadcrumbsComponent.displayName = "Breadcrumbs";\n\n// Define the compound component type\ntype TBreadcrumbsCompoundComponent = typeof BreadcrumbsComponent & {\n Item: typeof BreadcrumbItem;\n};\n\n// Create the compound component by casting and attaching subcomponents\nconst Breadcrumbs = BreadcrumbsComponent as TBreadcrumbsCompoundComponent;\nBreadcrumbs.Item = BreadcrumbItem;\n\n// Export the compound component and individual components\nexport { Breadcrumbs, BreadcrumbItem };\n'
|
|
1512
1518
|
},
|
|
1513
1519
|
{
|
|
1514
1520
|
"name": "breadcrumbItem.tsx",
|
|
1515
|
-
"content": 'import * as React from "react";\nimport {
|
|
1521
|
+
"content": 'import * as React from "react";\nimport { breadcrumbsVariants } from "./breadcrumbsVariants";\n\n/**\n * Props for the BreadcrumbItem component\n */\nexport interface IBreadcrumbItemProps extends React.HTMLAttributes<HTMLLIElement> {\n /**\n * Location href for the breadcrumbs item anchor element\n */\n href?: string;\n /**\n * Target property for the breadcrumbs item anchor element.\n */\n target?: "_blank" | "_self";\n /**\n * When set to true the user can pass any custom link component as children instead of using the default <a> tag.\n */\n customLink?: boolean;\n /**\n * Make the current breadcrumbs item as active\n */\n active?: boolean;\n}\n\n/**\n * Individual item in a breadcrumb trail\n */\nexport const BreadcrumbItem = React.forwardRef<HTMLLIElement, IBreadcrumbItemProps>(\n ({\n href,\n active = false,\n target = "_self",\n customLink = false,\n className,\n children,\n ...props\n }, ref ) => {\n const { item } = breadcrumbsVariants({ active });\n\n return children ? (\n <li\n ref={ref}\n className={item({ class: className })}\n {...( active ? { "aria-current": "page" } : {})}\n {...props}\n >\n {active || customLink\n ? children\n : <a href={href} target={target} rel="noopener noreferrer">\n {children}\n </a>\n }\n </li>\n ) : null;\n }\n);\n\nBreadcrumbItem.displayName = "BreadcrumbItem";\n'
|
|
1516
1522
|
},
|
|
1517
1523
|
{
|
|
1518
1524
|
"name": "README.md",
|
|
@@ -1525,9 +1531,10 @@ var REGISTRY = {
|
|
|
1525
1531
|
"name": "button",
|
|
1526
1532
|
"description": "The Button component is a versatile interactive element used to trigger actions, submit forms, or navigate between pages. It provides multiple visual styles, sizes, colors, and states including loading and disabled states. **When to use:** - Primary and secondary actions in forms and dialogs - Call-to-action elements on pages - Navigation triggers - Form submissions - Destructive actions (with error color) **Component Architecture:** - Built with React forwardRef for ref forwarding - Styled with Tailwind CSS and class-variance-authority (cva) - Integrates with Tooltip component for hover tooltips - Supports icon integration via ADPIcon or custom React nodes",
|
|
1527
1533
|
"dependencies": [
|
|
1528
|
-
"
|
|
1534
|
+
"tailwind-variants",
|
|
1529
1535
|
"react",
|
|
1530
|
-
"@floating-ui/react"
|
|
1536
|
+
"@floating-ui/react",
|
|
1537
|
+
"tailwind-merge"
|
|
1531
1538
|
],
|
|
1532
1539
|
"internalDependencies": [
|
|
1533
1540
|
"tooltip",
|
|
@@ -1540,11 +1547,11 @@ var REGISTRY = {
|
|
|
1540
1547
|
},
|
|
1541
1548
|
{
|
|
1542
1549
|
"name": "buttonVariants.ts",
|
|
1543
|
-
"content": 'import { cva } from "class-variance-authority";\n\nexport const buttonVariants = cva(\n "flex items-center justify-center rounded ease-in-out duration-300 w-fit disabled:cursor-not-allowed outline-none focus:transition-none",\n {\n variants: {\n variant: {\n filled: "",\n outline: "bg-transparent",\n shade: "bg-transparent",\n text: "p-0"\n },\n color: {\n primary: "",\n secondary: "",\n success: "",\n warning: "",\n error: ""\n },\n size: {\n xs: "p-2 text-xs gap-2",\n sm: "p-2.5 text-base gap-2",\n md: "p-2.5 text-md gap-2",\n lg: "p-3.5 text-md gap-2",\n xl: "p-[18px] text-lg gap-3"\n },\n hasChildren: {\n true: ""\n }\n },\n compoundVariants: [\n // PRIMARY COLOR VARIANTS\n {\n variant: "filled",\n color: "primary",\n className: `text-white bg-primary-500 hover:bg-primary-700 hover:text-white \n focus-visible:text-white focus-visible:bg-primary-400 active:bg-primary-500 \n disabled:bg-primary-200 disabled:text-white focus-visible:ring \n focus-visible:ring-[#99CBFA] focus-visible:active:bg-primary-500\n dark:text-iridium dark:bg-primary-400 dark:hover:bg-primary-300 dark:active:bg-primary-400\n dark:focus-visible:ring dark:focus-visible:ring-[#99CBFA] dark:focus-visible:active:bg-primary-300\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "outline",\n color: "primary",\n className: `text-primary-500 hover:text-primary-700 focus-visible:text-primary-400 disabled:text-primary-200\n border border-primary-500 hover:bg-primary-100 hover:border-primary-700\n focus-visible:bg-primary-50 focus-visible:border-primary-500 active:bg-transparent\n disabled:border-primary-200 disabled:hover:bg-secondary-50\n focus-visible:ring focus-visible:ring-[#99CBFA] focus-visible:active:bg-transparent\n dark:text-primary-400 dark:border-primary-400 dark:hover:border-primary-300 dark:hover:text-primary-300 \n dark:active:text-primary-400 dark:active:border-primary-400\n dark:focus-visible:ring dark:focus-visible:ring-[#99CBFA] dark:focus-visible:border-primary-300 \n dark:focus-visible:text-primary-300 dark:focus-visible:active:text-primary-400 dark:focus-visible:active:border-primary-400\n dark:disabled:border-none dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "shade",\n color: "primary",\n className: `text-primary-500 hover:text-primary-700 focus-visible:text-primary-400 disabled:text-primary-200\n hover:bg-primary-100 focus-visible:bg-primary-50 active:bg-transparent disabled:bg-transparent\n dark:text-primary-400 dark:hover:bg-primary-300 dark:hover:text-iridium dark:active:bg-transparent dark:active:text-primary-400\n dark:focus-visible:bg-primary-200 dark:focus-visible:text-iridium dark:focus-visible:active:bg-transparent dark:focus-visible:active:text-primary-400\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "text",\n color: "primary",\n className: `text-primary-500 hover:text-primary-700 focus-visible:text-primary-400 disabled:text-primary-200\n active:text-primary-500\n dark:text-primary-400 dark:hover:text-primary-300 dark:active:text-primary-500 dark:focus-visible:text-primary-200\n dark:disabled:text-secondary-400`\n },\n\n // SECONDARY COLOR VARIANTS\n {\n variant: "filled",\n color: "secondary",\n className: `text-secondary-500 focus-visible:text-secondary-400\n bg-gray-100 hover:bg-gray-300 focus-visible:bg-gray-200 active:bg-gray-100\n focus-visible:ring focus-visible:ring-gray-100 focus-visible:active:bg-gray-100\n dark:text-secondary-50 dark:bg-secondary-800 dark:hover:bg-secondary-700 dark:active:bg-secondary-800\n dark:focus-visible:ring dark:focus-visible:ring-secondary-600 dark:focus-visible:active:bg-secondary-700`\n },\n {\n variant: "outline",\n color: "secondary",\n className: `text-secondary-500 focus-visible:text-secondary-400\n border border-gray-300 hover:bg-gray-100 focus-visible:bg-gray-50 active:bg-transparent\n focus-visible:ring focus-visible:ring-gray-200 focus-visible:active:bg-transparent\n dark:text-secondary-50 dark:border-secondary-50 dark:hover:text-secondary-100 dark:active:text-secondary-50 dark:active:border-secondary-800\n dark:focus-visible:ring dark:focus-visible:ring-secondary-50 dark:focus-visible:border-secondary-600 dark:focus-visible:text-secondary-200 \n dark:focus-visible:active:text-secondary-50 dark:focus-visible:active:border-secondary-800`\n },\n {\n variant: "shade",\n color: "secondary",\n className: `text-secondary-500 focus-visible:text-secondary-400\n hover:bg-gray-100 focus-visible:bg-gray-50 active:bg-transparent\n dark:text-secondary-50 dark:hover:bg-secondary-700 dark:hover:text-secondary-100 \n dark:active:bg-transparent dark:active:text-secondary-50\n dark:focus-visible:bg-secondary-600 dark:focus-visible:text-secondary-200 \n dark:focus-visible:active:bg-transparent dark:focus-visible:active:text-secondary-50`\n },\n {\n variant: "text",\n color: "secondary",\n className: `text-secondary-500 focus-visible:text-secondary-400 active:text-secondary-500\n dark:text-secondary-50 dark:hover:text-secondary-100 dark:active:text-secondary-50 dark:focus-visible:text-secondary-200`\n },\n\n // SUCCESS COLOR VARIANTS\n {\n variant: "filled",\n color: "success",\n className: `text-white bg-success-500 hover:bg-success-700 hover:text-white \n focus-visible:text-white focus-visible:bg-success-400 active:bg-success-500 \n disabled:bg-success-200 disabled:text-white focus-visible:ring \n focus-visible:ring-[#A3D6C9] focus-visible:active:bg-success-500\n dark:text-iridium dark:bg-success-400 dark:hover:bg-success-300 dark:active:bg-success-400\n dark:focus-visible:ring dark:focus-visible:ring-[#A3D6C9] dark:focus-visible:active:bg-success-300\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "outline",\n color: "success",\n className: `text-success-500 hover:text-success-700 focus-visible:text-success-400 disabled:text-success-200\n border border-success-500 hover:bg-success-100 hover:border-success-700 active:bg-transparent\n focus-visible:bg-success-50 focus-visible:border-success-500 disabled:border-success-200\n focus-visible:ring focus-visible:ring-[#A3D6C9] focus-visible:active:bg-transparent\n dark:text-success-400 dark:border-success-400 dark:hover:border-success-300 dark:hover:text-success-300 \n dark:active:text-success-400 dark:active:border-success-400\n dark:focus-visible:ring dark:focus-visible:ring-[#A3D6C9] dark:focus-visible:border-success-200 \n dark:focus-visible:text-success-200 dark:focus-visible:active:text-success-400 dark:focus-visible:active:border-success-400\n dark:disabled:border-none dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "shade",\n color: "success",\n className: `text-success-500 hover:text-success-700 focus-visible:text-success-400 disabled:text-success-200\n hover:bg-success-100 focus-visible:bg-success-50 active:bg-transparent disabled:bg-transparent\n dark:text-success-400 dark:hover:bg-success-300 dark:hover:text-iridium dark:active:bg-transparent dark:active:text-success-400\n dark:focus-visible:bg-success-200 dark:focus-visible:text-iridium dark:focus-visible:active:bg-transparent dark:focus-visible:active:text-success-400\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "text",\n color: "success",\n className: `text-success-500 hover:text-success-700 focus-visible:text-success-400 disabled:text-success-200\n active:text-success-500\n dark:text-success-400 dark:hover:text-success-300 dark:active:text-success-400 dark:focus-visible:text-success-200\n dark:disabled:text-secondary-400`\n },\n\n // WARNING COLOR VARIANTS\n {\n variant: "filled",\n color: "warning",\n className: `text-white bg-warning-500 hover:bg-warning-700 hover:text-white \n focus-visible:text-white focus-visible:bg-warning-400 active:bg-warning-500 \n disabled:bg-warning-200 disabled:text-white focus-visible:ring \n focus-visible:ring-[#FBD29C] focus-visible:active:bg-warning-500\n dark:text-iridium dark:bg-warning-400 dark:hover:bg-warning-300 dark:active:bg-warning-400\n dark:focus-visible:ring dark:focus-visible:ring-[#FBD29C] dark:focus-visible:active:bg-warning-300\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "outline",\n color: "warning",\n className: `text-warning-500 hover:text-warning-700 focus-visible:text-warning-400 disabled:text-warning-200\n border border-warning-500 hover:bg-warning-100 hover:border-warning-700\n focus-visible:bg-warning-50 focus-visible:border-warning-50 active:bg-transparent disabled:border-warning-200\n focus-visible:ring focus-visible:ring-[#FBD29C] focus-visible:active:bg-transparent\n dark:text-warning-400 dark:border-warning-400 dark:hover:border-warning-300 dark:hover:text-warning-300 \n dark:active:text-warning-400 dark:active:border-warning-400\n dark:focus-visible:ring dark:focus-visible:ring-[#FBD29C] dark:focus-visible:border-warning-300 \n dark:focus-visible:text-warning-300 dark:focus-visible:active:text-warning-400 dark:focus-visible:active:border-warning-400\n dark:disabled:border-none dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "shade",\n color: "warning",\n className: `text-warning-500 hover:text-warning-700 focus-visible:text-warning-400 disabled:text-warning-200\n hover:bg-warning-100 focus-visible:bg-warning-100 active:bg-transparent disabled:bg-transparent\n dark:text-warning-400 dark:hover:bg-warning-300 dark:hover:text-iridium dark:active:bg-transparent dark:active:text-warning-400\n dark:focus-visible:bg-warning-200 dark:focus-visible:text-iridium dark:focus-visible:active:bg-transparent dark:focus-visible:active:text-warning-400\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "text",\n color: "warning",\n className: `text-warning-500 hover:text-warning-700 focus-visible:text-warning-400 disabled:text-warning-200\n active:text-warning-500\n dark:text-warning-400 dark:hover:text-warning-300 dark:active:text-warning-400 dark:focus-visible:text-warning-200\n dark:disabled:text-secondary-400`\n },\n\n // ERROR COLOR VARIANTS\n {\n variant: "filled",\n color: "error",\n className: `text-white bg-error-500 hover:bg-error-700 hover:text-white \n focus-visible:text-white focus-visible:bg-error-400 active:bg-error-500 \n disabled:bg-error-200 disabled:text-white focus-visible:ring \n focus-visible:ring-[#F1AE9D] focus-visible:active:bg-error-500\n dark:text-iridium dark:bg-error-400 dark:hover:bg-error-300 dark:active:bg-error-400\n dark:focus-visible:ring dark:focus-visible:ring-[#F1AE9D] dark:focus-visible:active:bg-error-300\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "outline",\n color: "error",\n className: `text-error-500 hover:text-error-700 focus-visible:text-error-400 disabled:text-error-200\n border border-error-500 hover:bg-error-100 hover:border-error-700\n focus-visible:bg-error-50 focus-visible:border-error-500 active:bg-transparent disabled:border-error-200\n focus-visible:ring focus-visible:ring-[#F1AE9D] focus-visible:active:bg-transparent\n dark:text-error-400 dark:border-error-400 dark:hover:border-error-300 dark:hover:text-error-300 \n dark:active:text-error-400 dark:active:border-error-400\n dark:focus-visible:ring dark:focus-visible:ring-[#F1AE9D] dark:focus-visible:border-error-300 \n dark:focus-visible:text-error-300 dark:focus-visible:active:text-error-400 dark:focus-visible:active:border-error-400\n dark:disabled:border-none dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "shade",\n color: "error",\n className: `text-error-500 hover:text-error-700 focus-visible:text-error-400 disabled:text-error-200\n hover:bg-error-100 focus-visible:bg-error-50 active:bg-transparent disabled:bg-transparent\n dark:text-error-400 dark:hover:bg-error-300 dark:hover:text-iridium dark:active:bg-transparent dark:active:text-error-400\n dark:focus-visible:bg-error-200 dark:focus-visible:text-iridium dark:focus-visible:active:bg-transparent dark:focus-visible:active:text-error-400\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "text",\n color: "error",\n className: `text-error-500 hover:text-error-700 focus-visible:text-error-400 disabled:text-error-200\n active:text-error-500\n dark:text-error-400 dark:hover:text-error-300 dark:active:text-error-400 dark:focus-visible:text-error-200\n dark:disabled:text-secondary-400`\n },\n\n // GLOBAL DISABLED STATES\n {\n variant: "filled",\n className: `disabled:text-secondary-300 disabled:bg-gray-100\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "outline",\n className: `disabled:text-secondary-300 disabled:border-gray-200\n dark:disabled:border-none dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "shade",\n className: `disabled:text-secondary-300\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "text",\n className: `disabled:text-secondary-200\n dark:disabled:text-secondary-400`\n },\n {\n variant: "text",\n className: "p-0"\n },\n\n // PADDED VARIANTS FOR EACH SIZE WHEN THERE ARE CHILDREN\n {\n variant: [ "filled", "outline", "shade" ],\n size: "xs",\n hasChildren: true,\n className: "px-3 py-2"\n },\n {\n variant: [ "filled", "outline", "shade" ],\n size: "sm",\n hasChildren: true,\n className: "px-5 py-2"\n },\n {\n variant: [ "filled", "outline", "shade" ],\n size: "md",\n hasChildren: true,\n className: "px-6 py-2"\n },\n {\n variant: [ "filled", "outline", "shade" ],\n size: "lg",\n hasChildren: true,\n className: "px-[28px] py-3"\n },\n {\n variant: [ "filled", "outline", "shade" ],\n size: "xl",\n hasChildren: true,\n className: "px-7 py-4"\n }\n ],\n defaultVariants: {\n variant: "filled",\n color: "primary",\n size: "md"\n }\n }\n);'
|
|
1550
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const buttonVariants = tv({\n base: "flex items-center justify-center rounded ease-in-out duration-300 w-fit disabled:cursor-not-allowed outline-none focus:transition-none",\n variants: {\n variant: {\n filled: "",\n outline: "bg-transparent",\n shade: "bg-transparent",\n text: "p-0"\n },\n color: {\n primary: "",\n secondary: "",\n success: "",\n warning: "",\n error: ""\n },\n size: {\n xs: "p-2 text-xs gap-2",\n sm: "p-2.5 text-base gap-2",\n md: "p-2.5 text-md gap-2",\n lg: "p-3.5 text-md gap-2",\n xl: "p-[18px] text-lg gap-3"\n },\n hasChildren: {\n true: ""\n }\n },\n compoundVariants: [\n // PRIMARY COLOR VARIANTS\n {\n variant: "filled",\n color: "primary",\n class: `text-white bg-primary-500 hover:bg-primary-700 hover:text-white \n focus-visible:text-white focus-visible:bg-primary-400 active:bg-primary-500 \n disabled:bg-primary-200 disabled:text-white focus-visible:ring \n focus-visible:ring-[#99CBFA] focus-visible:active:bg-primary-500\n dark:text-iridium dark:bg-primary-400 dark:hover:bg-primary-300 dark:active:bg-primary-400\n dark:focus-visible:ring dark:focus-visible:ring-[#99CBFA] dark:focus-visible:active:bg-primary-300\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "outline",\n color: "primary",\n class: `text-primary-500 hover:text-primary-700 focus-visible:text-primary-400 disabled:text-primary-200\n border border-primary-500 hover:bg-primary-100 hover:border-primary-700\n focus-visible:bg-primary-50 focus-visible:border-primary-500 active:bg-transparent\n disabled:border-primary-200 disabled:hover:bg-secondary-50\n focus-visible:ring focus-visible:ring-[#99CBFA] focus-visible:active:bg-transparent\n dark:text-primary-400 dark:border-primary-400 dark:hover:border-primary-300 dark:hover:text-primary-300 \n dark:active:text-primary-400 dark:active:border-primary-400\n dark:focus-visible:ring dark:focus-visible:ring-[#99CBFA] dark:focus-visible:border-primary-300 \n dark:focus-visible:text-primary-300 dark:focus-visible:active:text-primary-400 dark:focus-visible:active:border-primary-400\n dark:disabled:border-none dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "shade",\n color: "primary",\n class: `text-primary-500 hover:text-primary-700 focus-visible:text-primary-400 disabled:text-primary-200\n hover:bg-primary-100 focus-visible:bg-primary-50 active:bg-transparent disabled:bg-transparent\n dark:text-primary-400 dark:hover:bg-primary-300 dark:hover:text-iridium dark:active:bg-transparent dark:active:text-primary-400\n dark:focus-visible:bg-primary-200 dark:focus-visible:text-iridium dark:focus-visible:active:bg-transparent dark:focus-visible:active:text-primary-400\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "text",\n color: "primary",\n class: `text-primary-500 hover:text-primary-700 focus-visible:text-primary-400 disabled:text-primary-200\n active:text-primary-500\n dark:text-primary-400 dark:hover:text-primary-300 dark:active:text-primary-500 dark:focus-visible:text-primary-200\n dark:disabled:text-secondary-400`\n },\n\n // SECONDARY COLOR VARIANTS\n {\n variant: "filled",\n color: "secondary",\n class: `text-secondary-500 focus-visible:text-secondary-400\n bg-gray-100 hover:bg-gray-300 focus-visible:bg-gray-200 active:bg-gray-100\n focus-visible:ring focus-visible:ring-gray-100 focus-visible:active:bg-gray-100\n dark:text-secondary-50 dark:bg-secondary-800 dark:hover:bg-secondary-700 dark:active:bg-secondary-800\n dark:focus-visible:ring dark:focus-visible:ring-secondary-600 dark:focus-visible:active:bg-secondary-700`\n },\n {\n variant: "outline",\n color: "secondary",\n class: `text-secondary-500 focus-visible:text-secondary-400\n border border-gray-300 hover:bg-gray-100 focus-visible:bg-gray-50 active:bg-transparent\n focus-visible:ring focus-visible:ring-gray-200 focus-visible:active:bg-transparent\n dark:text-secondary-50 dark:border-secondary-50 dark:hover:text-secondary-100 dark:active:text-secondary-50 dark:active:border-secondary-800\n dark:focus-visible:ring dark:focus-visible:ring-secondary-50 dark:focus-visible:border-secondary-600 dark:focus-visible:text-secondary-200 \n dark:focus-visible:active:text-secondary-50 dark:focus-visible:active:border-secondary-800`\n },\n {\n variant: "shade",\n color: "secondary",\n class: `text-secondary-500 focus-visible:text-secondary-400\n hover:bg-gray-100 focus-visible:bg-gray-50 active:bg-transparent\n dark:text-secondary-50 dark:hover:bg-secondary-700 dark:hover:text-secondary-100 \n dark:active:bg-transparent dark:active:text-secondary-50\n dark:focus-visible:bg-secondary-600 dark:focus-visible:text-secondary-200 \n dark:focus-visible:active:bg-transparent dark:focus-visible:active:text-secondary-50`\n },\n {\n variant: "text",\n color: "secondary",\n class: `text-secondary-500 focus-visible:text-secondary-400 active:text-secondary-500\n dark:text-secondary-50 dark:hover:text-secondary-100 dark:active:text-secondary-50 dark:focus-visible:text-secondary-200`\n },\n\n // SUCCESS COLOR VARIANTS\n {\n variant: "filled",\n color: "success",\n class: `text-white bg-success-500 hover:bg-success-700 hover:text-white \n focus-visible:text-white focus-visible:bg-success-400 active:bg-success-500 \n disabled:bg-success-200 disabled:text-white focus-visible:ring \n focus-visible:ring-[#A3D6C9] focus-visible:active:bg-success-500\n dark:text-iridium dark:bg-success-400 dark:hover:bg-success-300 dark:active:bg-success-400\n dark:focus-visible:ring dark:focus-visible:ring-[#A3D6C9] dark:focus-visible:active:bg-success-300\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "outline",\n color: "success",\n class: `text-success-500 hover:text-success-700 focus-visible:text-success-400 disabled:text-success-200\n border border-success-500 hover:bg-success-100 hover:border-success-700 active:bg-transparent\n focus-visible:bg-success-50 focus-visible:border-success-500 disabled:border-success-200\n focus-visible:ring focus-visible:ring-[#A3D6C9] focus-visible:active:bg-transparent\n dark:text-success-400 dark:border-success-400 dark:hover:border-success-300 dark:hover:text-success-300 \n dark:active:text-success-400 dark:active:border-success-400\n dark:focus-visible:ring dark:focus-visible:ring-[#A3D6C9] dark:focus-visible:border-success-200 \n dark:focus-visible:text-success-200 dark:focus-visible:active:text-success-400 dark:focus-visible:active:border-success-400\n dark:disabled:border-none dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "shade",\n color: "success",\n class: `text-success-500 hover:text-success-700 focus-visible:text-success-400 disabled:text-success-200\n hover:bg-success-100 focus-visible:bg-success-50 active:bg-transparent disabled:bg-transparent\n dark:text-success-400 dark:hover:bg-success-300 dark:hover:text-iridium dark:active:bg-transparent dark:active:text-success-400\n dark:focus-visible:bg-success-200 dark:focus-visible:text-iridium dark:focus-visible:active:bg-transparent dark:focus-visible:active:text-success-400\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "text",\n color: "success",\n class: `text-success-500 hover:text-success-700 focus-visible:text-success-400 disabled:text-success-200\n active:text-success-500\n dark:text-success-400 dark:hover:text-success-300 dark:active:text-success-400 dark:focus-visible:text-success-200\n dark:disabled:text-secondary-400`\n },\n\n // WARNING COLOR VARIANTS\n {\n variant: "filled",\n color: "warning",\n class: `text-white bg-warning-500 hover:bg-warning-700 hover:text-white \n focus-visible:text-white focus-visible:bg-warning-400 active:bg-warning-500 \n disabled:bg-warning-200 disabled:text-white focus-visible:ring \n focus-visible:ring-[#FBD29C] focus-visible:active:bg-warning-500\n dark:text-iridium dark:bg-warning-400 dark:hover:bg-warning-300 dark:active:bg-warning-400\n dark:focus-visible:ring dark:focus-visible:ring-[#FBD29C] dark:focus-visible:active:bg-warning-300\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "outline",\n color: "warning",\n class: `text-warning-500 hover:text-warning-700 focus-visible:text-warning-400 disabled:text-warning-200\n border border-warning-500 hover:bg-warning-100 hover:border-warning-700\n focus-visible:bg-warning-50 focus-visible:border-warning-50 active:bg-transparent disabled:border-warning-200\n focus-visible:ring focus-visible:ring-[#FBD29C] focus-visible:active:bg-transparent\n dark:text-warning-400 dark:border-warning-400 dark:hover:border-warning-300 dark:hover:text-warning-300 \n dark:active:text-warning-400 dark:active:border-warning-400\n dark:focus-visible:ring dark:focus-visible:ring-[#FBD29C] dark:focus-visible:border-warning-300 \n dark:focus-visible:text-warning-300 dark:focus-visible:active:text-warning-400 dark:focus-visible:active:border-warning-400\n dark:disabled:border-none dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "shade",\n color: "warning",\n class: `text-warning-500 hover:text-warning-700 focus-visible:text-warning-400 disabled:text-warning-200\n hover:bg-warning-100 focus-visible:bg-warning-100 active:bg-transparent disabled:bg-transparent\n dark:text-warning-400 dark:hover:bg-warning-300 dark:hover:text-iridium dark:active:bg-transparent dark:active:text-warning-400\n dark:focus-visible:bg-warning-200 dark:focus-visible:text-iridium dark:focus-visible:active:bg-transparent dark:focus-visible:active:text-warning-400\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "text",\n color: "warning",\n class: `text-warning-500 hover:text-warning-700 focus-visible:text-warning-400 disabled:text-warning-200\n active:text-warning-500\n dark:text-warning-400 dark:hover:text-warning-300 dark:active:text-warning-400 dark:focus-visible:text-warning-200\n dark:disabled:text-secondary-400`\n },\n\n // ERROR COLOR VARIANTS\n {\n variant: "filled",\n color: "error",\n class: `text-white bg-error-500 hover:bg-error-700 hover:text-white \n focus-visible:text-white focus-visible:bg-error-400 active:bg-error-500 \n disabled:bg-error-200 disabled:text-white focus-visible:ring \n focus-visible:ring-[#F1AE9D] focus-visible:active:bg-error-500\n dark:text-iridium dark:bg-error-400 dark:hover:bg-error-300 dark:active:bg-error-400\n dark:focus-visible:ring dark:focus-visible:ring-[#F1AE9D] dark:focus-visible:active:bg-error-300\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "outline",\n color: "error",\n class: `text-error-500 hover:text-error-700 focus-visible:text-error-400 disabled:text-error-200\n border border-error-500 hover:bg-error-100 hover:border-error-700\n focus-visible:bg-error-50 focus-visible:border-error-500 active:bg-transparent disabled:border-error-200\n focus-visible:ring focus-visible:ring-[#F1AE9D] focus-visible:active:bg-transparent\n dark:text-error-400 dark:border-error-400 dark:hover:border-error-300 dark:hover:text-error-300 \n dark:active:text-error-400 dark:active:border-error-400\n dark:focus-visible:ring dark:focus-visible:ring-[#F1AE9D] dark:focus-visible:border-error-300 \n dark:focus-visible:text-error-300 dark:focus-visible:active:text-error-400 dark:focus-visible:active:border-error-400\n dark:disabled:border-none dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "shade",\n color: "error",\n class: `text-error-500 hover:text-error-700 focus-visible:text-error-400 disabled:text-error-200\n hover:bg-error-100 focus-visible:bg-error-50 active:bg-transparent disabled:bg-transparent\n dark:text-error-400 dark:hover:bg-error-300 dark:hover:text-iridium dark:active:bg-transparent dark:active:text-error-400\n dark:focus-visible:bg-error-200 dark:focus-visible:text-iridium dark:focus-visible:active:bg-transparent dark:focus-visible:active:text-error-400\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "text",\n color: "error",\n class: `text-error-500 hover:text-error-700 focus-visible:text-error-400 disabled:text-error-200\n active:text-error-500\n dark:text-error-400 dark:hover:text-error-300 dark:active:text-error-400 dark:focus-visible:text-error-200\n dark:disabled:text-secondary-400`\n },\n\n // GLOBAL DISABLED STATES\n {\n variant: "filled",\n class: `disabled:text-secondary-300 disabled:bg-gray-100\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "outline",\n class: `disabled:text-secondary-300 disabled:border-gray-200\n dark:disabled:border-none dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "shade",\n class: `disabled:text-secondary-300\n dark:disabled:bg-secondary-600 dark:disabled:text-secondary-400`\n },\n {\n variant: "text",\n class: `disabled:text-secondary-200\n dark:disabled:text-secondary-400`\n },\n {\n variant: "text",\n class: "p-0"\n },\n\n // PADDED VARIANTS FOR EACH SIZE WHEN THERE ARE CHILDREN\n {\n variant: [ "filled", "outline", "shade" ],\n size: "xs",\n hasChildren: true,\n class: "px-3 py-2"\n },\n {\n variant: [ "filled", "outline", "shade" ],\n size: "sm",\n hasChildren: true,\n class: "px-5 py-2"\n },\n {\n variant: [ "filled", "outline", "shade" ],\n size: "md",\n hasChildren: true,\n class: "px-6 py-2"\n },\n {\n variant: [ "filled", "outline", "shade" ],\n size: "lg",\n hasChildren: true,\n class: "px-[28px] py-3"\n },\n {\n variant: [ "filled", "outline", "shade" ],\n size: "xl",\n hasChildren: true,\n class: "px-7 py-4"\n }\n ],\n defaultVariants: {\n variant: "filled",\n color: "primary",\n size: "md"\n }\n});\n'
|
|
1544
1551
|
},
|
|
1545
1552
|
{
|
|
1546
1553
|
"name": "button.tsx",
|
|
1547
|
-
"content": 'import * as React from "react";\nimport {
|
|
1554
|
+
"content": 'import * as React from "react";\nimport type { Placement } from "@floating-ui/react";\n\nimport { Tooltip } from "../tooltip";\nimport { ADPIcon, type TIconType } from "../adpIcon";\nimport { buttonVariants } from "./buttonVariants";\n\nexport const colors = [ "primary", "secondary", "success", "warning", "error" ] as const;\nexport const variants = [ "filled", "outline", "shade", "text" ] as const;\nexport enum ESizes {\n XS = "xs",\n SM = "sm",\n MD = "md",\n LG = "lg",\n XL = "xl"\n}\n\nexport type TColor = ( typeof colors )[number];\nexport type TVariant = ( typeof variants )[number];\nexport type TSize = `${ESizes}`;\n\n/**\n * Button component from Astral UI\n */\nexport interface IButtonProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "css"> {\n /**\n * Size of the button.\n * @default md\n */\n size?: TSize;\n /**\n * Color of the button.\n * @default primary\n */\n color?: TColor;\n /**\n * Variant of the button.\n * @default filled\n */\n variant?: TVariant;\n /**\n * Shows loading animation instead of the button content\n */\n loading?: boolean;\n /**\n * Icon to be displayed before the button\'s content.\n * @description\n * This prop accepts either an IconType (string) or a ReactNode.\n * - If an IconType is provided, it will render an ADPIcon component.\n * - If a ReactNode is provided, it will be rendered as-is.\n */\n prefixIcon?: TIconType | React.ReactNode;\n /**\n * Icon to be displayed after the button\'s content.\n * @description\n * This prop accepts either an IconType (string) or a ReactNode.\n * - If an IconType is provided, it will render an ADPIcon component.\n * - If a ReactNode is provided, it will be rendered as-is.\n */\n suffixIcon?: TIconType | React.ReactNode;\n /**\n * Tooltip to be displayed on hover of the button.\n * @description\n * This prop accepts a ReactNode, if this is passed then the button will render\n * tooltip on hover of the button.\n * This tooltip is only on hover and not on click.\n */\n tooltip?: React.ReactNode;\n /**\n * Placement of the tooltip\n * @default "bottom-end"\n */\n tooltipPlacement?: Placement;\n}\n\n// Map of button size to spinner size\nexport const ButtonSizeToSpinnerMap: Record<\n TSize,\n "xxs" | "xs" | "sm" | "md" | "lg" | "xl"\n> = {\n xs: "xs",\n sm: "xs",\n md: "sm",\n lg: "sm",\n xl: "md"\n};\n\n/**\n * Button component with multiple variants, colors, and sizes.\n *\n * @example\n * ```tsx\n * <Button variant="filled" color="primary" size="md">\n * Click me\n * </Button>\n * ```\n */\nexport const Button = React.forwardRef<HTMLButtonElement, IButtonProps>(\n (\n {\n className,\n variant = "filled",\n color = "primary",\n size = "md",\n children,\n disabled,\n loading = false,\n type = "button",\n prefixIcon,\n suffixIcon,\n tooltip,\n tooltipPlacement,\n ...props\n },\n ref\n ) => {\n // Determine if we should show padded styles based on whether children exist\n const hasChildren = Boolean( children );\n\n const buttonClasses = buttonVariants({\n variant,\n color,\n size,\n hasChildren: hasChildren ? true : undefined,\n class: className\n });\n\n // Create the button element\n const buttonElement = (\n <button\n type={type}\n className={buttonClasses}\n disabled={disabled || loading}\n ref={ref}\n {...props}\n >\n {loading ? (\n <ADPIcon icon="spinner" spin size={ButtonSizeToSpinnerMap[size]} />\n ) : (\n prefixIcon && (\n typeof prefixIcon === "string"\n ? <ADPIcon icon={prefixIcon as TIconType} fixedWidth size={ButtonSizeToSpinnerMap[size]} />\n : prefixIcon\n )\n )}\n {children}\n {suffixIcon && (\n typeof suffixIcon === "string"\n ? <ADPIcon icon={suffixIcon as TIconType} fixedWidth size={ButtonSizeToSpinnerMap[size]} />\n : suffixIcon\n )}\n </button>\n );\n\n // If tooltip is present and not null or empty, wrap the button with Tooltip.\n if ( tooltip !== null && tooltip !== undefined && tooltip !== false && tooltip !== "" ) {\n return (\n <Tooltip\n clickable={false}\n trigger={buttonElement}\n triggerAriaLabel={typeof tooltip === "string" ? tooltip : undefined}\n disabled={disabled}\n placement={tooltipPlacement}\n >\n {tooltip}\n </Tooltip>\n );\n }\n\n // Otherwise, return just the button\n return buttonElement;\n }\n);\n\nButton.displayName = "Button";\n'
|
|
1548
1555
|
},
|
|
1549
1556
|
{
|
|
1550
1557
|
"name": "README.md",
|
|
@@ -1557,8 +1564,9 @@ var REGISTRY = {
|
|
|
1557
1564
|
"name": "card",
|
|
1558
1565
|
"description": "The Card component is a flexible container that groups related content and actions using a compound component pattern. It provides consistent visual structure with optional Header, Body, and Footer subcomponents. **When to use:** - Displaying grouped information (user profiles, products, articles) - Dashboard widgets and statistics - Form containers with headers and action buttons - List items that need clear visual separation - Content previews with actions **Component Architecture:** - Built with React compound component pattern (Card.Header, Card.Body, Card.Footer) - Styled with Tailwind CSS and class-variance-authority (cva) - Uses semantic HTML (`<header>`, `<footer>` elements) - All subcomponents are optional and composable - Integrates with CTAGroup component for header actions",
|
|
1559
1566
|
"dependencies": [
|
|
1560
|
-
"
|
|
1561
|
-
"react"
|
|
1567
|
+
"tailwind-variants",
|
|
1568
|
+
"react",
|
|
1569
|
+
"tailwind-merge"
|
|
1562
1570
|
],
|
|
1563
1571
|
"internalDependencies": [
|
|
1564
1572
|
"ctaGroup",
|
|
@@ -1568,15 +1576,15 @@ var REGISTRY = {
|
|
|
1568
1576
|
"files": [
|
|
1569
1577
|
{
|
|
1570
1578
|
"name": "index.ts",
|
|
1571
|
-
"content": 'export {\n Card,\n CardHeader,\n CardBody,\n CardFooter,\n type ICardHeaderProps\n} from "./card";\n\nexport {
|
|
1579
|
+
"content": 'export {\n Card,\n CardHeader,\n CardBody,\n CardFooter,\n type ICardHeaderProps\n} from "./card";\n\nexport { cardVariants } from "./cardVariants";'
|
|
1572
1580
|
},
|
|
1573
1581
|
{
|
|
1574
1582
|
"name": "cardVariants.ts",
|
|
1575
|
-
"content": 'import {
|
|
1583
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const cardVariants = tv({\n slots: {\n base: "border rounded border-secondary-200 text-secondary-500 dark:bg-secondary-800 dark:text-secondary-50 dark:border-secondary-500",\n header: "flex items-center justify-between bg-gray-100 px-4 py-3 rounded-t min-h-2 dark:bg-secondary-500",\n body: "p-4",\n footer: "mx-4 mb-4"\n }\n});\n'
|
|
1576
1584
|
},
|
|
1577
1585
|
{
|
|
1578
1586
|
"name": "card.tsx",
|
|
1579
|
-
"content": 'import * as React from "react";\nimport {
|
|
1587
|
+
"content": 'import * as React from "react";\n\nimport { cardVariants } from "./cardVariants";\nimport { CTAGroup, type ICTAGroupProps } from "../ctaGroup";\n\nconst { base, header, body, footer } = cardVariants();\n\n/**\n * Card component for displaying content in a container with optional header and footer\n */\nexport type TCardProps = React.HTMLAttributes<HTMLDivElement>;\n\n/**\n * Card component with optional header, body, and footer\n *\n * @example\n * ```tsx\n * <Card>\n * <Card.Header title="Card Title" />\n * <Card.Body>Card content goes here</Card.Body>\n * <Card.Footer>Footer content</Card.Footer>\n * </Card>\n * ```\n */\nconst CardComponent = React.forwardRef<HTMLDivElement, TCardProps>(\n ({ className, children, ...props }, ref ) => {\n return (\n <div\n ref={ref}\n className={base({ class: className })}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nCardComponent.displayName = "Card";\n\n/**\n * Card header props\n */\nexport interface ICardHeaderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {\n /**\n * Title of the card\n */\n title?: React.ReactNode;\n /**\n * Props for the CTAGroup component. Pass an object with the props for the CTAGroup.\n */\n ctaGroupProps?: ICTAGroupProps;\n}\n\n/**\n * Card header component\n *\n * @example\n * ```tsx\n * <Card.Header title="Card Title" />\n * ```\n */\nconst CardHeader = React.forwardRef<HTMLDivElement, ICardHeaderProps>(\n ({ className, title, children, ctaGroupProps, ...props }, ref ) => {\n return (\n <header\n ref={ref}\n className={header({ class: className })}\n data-testid="cardHeader"\n {...props}\n >\n {title && <div className="font-medium">{title}</div>}\n {children}\n {ctaGroupProps && <CTAGroup {...ctaGroupProps} />}\n </header>\n );\n }\n);\n\nCardHeader.displayName = "CardHeader";\n\n/**\n * Card body props\n */\nexport type TCardBodyProps = React.HTMLAttributes<HTMLDivElement>;\n\n/**\n * Card body component\n *\n * @example\n * ```tsx\n * <Card.Body>Card content goes here</Card.Body>\n * ```\n */\nconst CardBody = React.forwardRef<HTMLDivElement, TCardBodyProps>(\n ({ className, children, ...props }, ref ) => {\n if ( !children ) {\n return null;\n }\n\n return (\n <div\n ref={ref}\n className={body({ class: className })}\n data-testid="cardBody"\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nCardBody.displayName = "CardBody";\n\n/**\n * Card footer props\n */\nexport type TCardFooterProps = React.HTMLAttributes<HTMLDivElement>;\n\n/**\n * Card footer component\n *\n * @example\n * ```tsx\n * <Card.Footer>Footer content</Card.Footer>\n * ```\n */\nconst CardFooter = React.forwardRef<HTMLDivElement, TCardFooterProps>(\n ({ className, children, ...props }, ref ) => {\n if ( !children ) {\n return null;\n }\n\n return (\n <footer\n ref={ref}\n className={footer({ class: className })}\n data-testid="cardFooter"\n {...props}\n >\n {children}\n </footer>\n );\n }\n);\n\nCardFooter.displayName = "CardFooter";\n\n// Define the compound component type\ntype TCardCompoundComponent = typeof CardComponent & {\n Header: typeof CardHeader;\n Body: typeof CardBody;\n Footer: typeof CardFooter;\n};\n\n// Create the compound component by casting and attaching subcomponents\nconst Card = CardComponent as TCardCompoundComponent;\nCard.Header = CardHeader;\nCard.Body = CardBody;\nCard.Footer = CardFooter;\n\n// Export the compound component and individual components\nexport { Card, CardHeader, CardBody, CardFooter };\n'
|
|
1580
1588
|
},
|
|
1581
1589
|
{
|
|
1582
1590
|
"name": "README.md",
|
|
@@ -1589,125 +1597,87 @@ var REGISTRY = {
|
|
|
1589
1597
|
"name": "checkbox",
|
|
1590
1598
|
"description": "The Checkbox component provides a binary selection input with label support. **When to use:** - Boolean selections (agree/disagree, enable/disable) - Multi-selection lists - Form agreements and confirmations - Settings toggles **Component Architecture:** - Styled with Tailwind CSS and cva - Accessible with proper ARIA attributes - Indeterminate state support",
|
|
1591
1599
|
"dependencies": [
|
|
1592
|
-
"
|
|
1593
|
-
"react"
|
|
1600
|
+
"tailwind-variants",
|
|
1601
|
+
"react",
|
|
1602
|
+
"tailwind-merge"
|
|
1594
1603
|
],
|
|
1595
1604
|
"internalDependencies": [],
|
|
1596
1605
|
"files": [
|
|
1597
1606
|
{
|
|
1598
1607
|
"name": "index.ts",
|
|
1599
|
-
"content": 'export { Checkbox, type ICheckboxProps } from "./checkbox";\nexport { checkboxVariants
|
|
1608
|
+
"content": 'export { Checkbox, type ICheckboxProps } from "./checkbox";\nexport { checkboxVariants } from "./checkboxVariants";'
|
|
1600
1609
|
},
|
|
1601
1610
|
{
|
|
1602
1611
|
"name": "checkboxVariants.ts",
|
|
1603
|
-
"content": `import {
|
|
1612
|
+
"content": `import { tv } from "tailwind-variants";
|
|
1604
1613
|
|
|
1605
|
-
export const checkboxVariants =
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1614
|
+
export const checkboxVariants = tv({
|
|
1615
|
+
slots: {
|
|
1616
|
+
input: [
|
|
1617
|
+
"appearance-none inline-flex justify-center items-center ease-in-out duration-300",
|
|
1618
|
+
"hover:border-primary-500 rounded-sm outline-none border-2 border-secondary-200 bg-white",
|
|
1619
|
+
"focus:border-primary-500 focus:ring-primary-500 focus:shadow-primary-2px focus:transition-none",
|
|
1620
|
+
"disabled:bg-secondary-50 disabled:border-secondary-200",
|
|
1621
|
+
"dark:bg-iridium dark:border-gray-600 dark:focus:shadow-[#99CBFA]",
|
|
1622
|
+
"dark:hover:border-primary-400 dark:focus:border-primary-400 dark:disabled:border-gray-900",
|
|
1623
|
+
"before:content-[''] before:scale-0 before:bg-white dark:before:bg-iridium checked:before:scale-100"
|
|
1624
|
+
],
|
|
1625
|
+
label: "text-secondary-500 dark:text-secondary-100"
|
|
1626
|
+
},
|
|
1627
|
+
variants: {
|
|
1628
|
+
variant: {
|
|
1629
|
+
filled: {
|
|
1630
|
+
input: [
|
|
1619
1631
|
"checked:bg-primary-500 checked:dark:bg-primary-400 checked:border-none",
|
|
1620
1632
|
"checked:disabled:bg-secondary-100 checked:dark:disabled:bg-gray-900"
|
|
1621
|
-
]
|
|
1622
|
-
|
|
1633
|
+
]
|
|
1634
|
+
},
|
|
1635
|
+
outline: {
|
|
1636
|
+
input: [
|
|
1623
1637
|
"checked:bg-primary-50 checked:border-primary-500",
|
|
1624
1638
|
"checked:disabled:bg-secondary-50 checked:disabled:border-secondary-200 checked:disabled:before:bg-secondary-200",
|
|
1625
1639
|
"checked:dark:bg-iridium checked:dark:border-primary-400",
|
|
1626
1640
|
"checked:dark:disabled:border-gray-900 checked:dark:disabled:before:bg-gray-900",
|
|
1627
1641
|
"before:bg-primary-500 dark:before:bg-primary-400"
|
|
1628
|
-
]
|
|
1629
|
-
|
|
1642
|
+
]
|
|
1643
|
+
},
|
|
1644
|
+
"filled-circle": {
|
|
1645
|
+
input: [
|
|
1630
1646
|
"rounded-full checked:bg-primary-500 checked:dark:bg-primary-400 checked:border-none",
|
|
1631
1647
|
"checked:disabled:bg-secondary-100 checked:dark:disabled:bg-gray-900",
|
|
1632
1648
|
"before:rounded-full before:h-2/5 before:w-2/5 checked:before:scale-100"
|
|
1633
|
-
]
|
|
1634
|
-
|
|
1649
|
+
]
|
|
1650
|
+
},
|
|
1651
|
+
"outline-circle": {
|
|
1652
|
+
input: [
|
|
1635
1653
|
"rounded-full checked:bg-primary-50 checked:border-primary-500",
|
|
1636
1654
|
"checked:disabled:bg-secondary-50 checked:disabled:border-secondary-200 checked:disabled:before:bg-secondary-200",
|
|
1637
1655
|
"checked:dark:bg-iridium checked:dark:border-primary-400",
|
|
1638
1656
|
"checked:dark:disabled:border-gray-900 checked:dark:disabled:before:bg-gray-900",
|
|
1639
1657
|
"before:bg-primary-500 dark:before:bg-primary-400 before:rounded-full before:h-2/5 before:w-2/5 checked:before:scale-100"
|
|
1640
|
-
]
|
|
1641
|
-
},
|
|
1642
|
-
size: {
|
|
1643
|
-
sm: "w-4 h-4 text-base before:h-1.5 before:w-2",
|
|
1644
|
-
md: "w-5 h-5 text-md before:h-2 before:w-2.5",
|
|
1645
|
-
lg: "w-6 h-6 text-lg before:h-2.5 before:w-3"
|
|
1658
|
+
]
|
|
1646
1659
|
}
|
|
1647
1660
|
},
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
}
|
|
1653
|
-
);
|
|
1654
|
-
|
|
1655
|
-
export const checkboxLabelVariants = cva(
|
|
1656
|
-
"text-secondary-500 dark:text-secondary-100",
|
|
1657
|
-
{
|
|
1658
|
-
variants: {
|
|
1659
|
-
size: {
|
|
1660
|
-
sm: "text-base",
|
|
1661
|
-
md: "text-md",
|
|
1662
|
-
lg: "text-lg"
|
|
1663
|
-
},
|
|
1664
|
-
disabled: {
|
|
1665
|
-
true: "text-secondary-200 dark:text-secondary-400"
|
|
1666
|
-
}
|
|
1661
|
+
size: {
|
|
1662
|
+
sm: { input: "w-4 h-4 text-base before:h-1.5 before:w-2", label: "text-base" },
|
|
1663
|
+
md: { input: "w-5 h-5 text-md before:h-2 before:w-2.5", label: "text-md" },
|
|
1664
|
+
lg: { input: "w-6 h-6 text-lg before:h-2.5 before:w-3", label: "text-lg" }
|
|
1667
1665
|
},
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
disabled: false
|
|
1666
|
+
disabled: {
|
|
1667
|
+
true: { label: "text-secondary-200 dark:text-secondary-400" }
|
|
1671
1668
|
}
|
|
1669
|
+
},
|
|
1670
|
+
defaultVariants: {
|
|
1671
|
+
variant: "filled",
|
|
1672
|
+
size: "md",
|
|
1673
|
+
disabled: false
|
|
1672
1674
|
}
|
|
1673
|
-
);
|
|
1674
|
-
|
|
1675
|
-
// The clip-path for the checkmark matches exactly what's in the original css file
|
|
1676
|
-
export const checkmarkClipPath = \`polygon(
|
|
1677
|
-
15% 50%,
|
|
1678
|
-
40.1% 75.4%,
|
|
1679
|
-
41.1% 75.7%,
|
|
1680
|
-
42.1% 75.4%,
|
|
1681
|
-
88% 4%,
|
|
1682
|
-
90% 3.5%,
|
|
1683
|
-
92% 4%,
|
|
1684
|
-
94% 5%,
|
|
1685
|
-
97% 7%,
|
|
1686
|
-
97.5% 7.5%,
|
|
1687
|
-
99% 10%,
|
|
1688
|
-
100% 14%,
|
|
1689
|
-
100% 17%,
|
|
1690
|
-
99% 20%,
|
|
1691
|
-
46% 99%,
|
|
1692
|
-
45% 99.5%,
|
|
1693
|
-
44% 100%,
|
|
1694
|
-
43% 100%,
|
|
1695
|
-
41% 100%,
|
|
1696
|
-
40% 100%,
|
|
1697
|
-
37% 97%,
|
|
1698
|
-
5% 65%,
|
|
1699
|
-
4% 62%,
|
|
1700
|
-
4% 59%,
|
|
1701
|
-
4.2% 57.6%,
|
|
1702
|
-
4.8% 55%,
|
|
1703
|
-
6.5% 52.2%,
|
|
1704
|
-
9% 50.5%,
|
|
1705
|
-
11.9% 49.7%
|
|
1706
|
-
)\`;`
|
|
1675
|
+
});
|
|
1676
|
+
`
|
|
1707
1677
|
},
|
|
1708
1678
|
{
|
|
1709
1679
|
"name": "checkbox.tsx",
|
|
1710
|
-
"content": 'import * as React from "react";\nimport { type ReactElement, type RefObject, forwardRef, useRef, useId } from "react";\nimport { cn } from "
|
|
1680
|
+
"content": 'import * as React from "react";\nimport { type ReactElement, type RefObject, forwardRef, useRef, useId } from "react";\nimport { cn } from "tailwind-variants";\n\nimport { checkboxVariants } from "./checkboxVariants";\n\n/**\n * A standard checkbox component with multiple size and style variants.\n */\nexport interface ICheckboxProps extends Omit<React.HTMLProps<HTMLInputElement>, "label" | "size"> {\n /**\n * Optional label to display next to the checkbox\n */\n label?: React.ReactNode;\n /**\n * Size of the Checkbox\n * @default md\n */\n size?: "sm" | "md" | "lg";\n /**\n * Variant of the Checkbox\n * @default filled\n */\n variant?: "filled" | "outline" | "filled-circle" | "outline-circle";\n /**\n * Pass a ref to the inner input element\n */\n ref?: RefObject<HTMLInputElement>;\n}\n\nexport const checkmarkClipPath = `polygon(\n 15% 50%,\n 40.1% 75.4%,\n 41.1% 75.7%,\n 42.1% 75.4%,\n 88% 4%,\n 90% 3.5%,\n 92% 4%,\n 94% 5%,\n 97% 7%,\n 97.5% 7.5%,\n 99% 10%,\n 100% 14%,\n 100% 17%,\n 99% 20%,\n 46% 99%,\n 45% 99.5%,\n 44% 100%,\n 43% 100%,\n 41% 100%,\n 40% 100%,\n 37% 97%,\n 5% 65%,\n 4% 62%,\n 4% 59%,\n 4.2% 57.6%,\n 4.8% 55%,\n 6.5% 52.2%,\n 9% 50.5%,\n 11.9% 49.7%\n)`;\n\n/**\n * A standard checkbox component with multiple size and style variants.\n */\nexport const Checkbox = forwardRef<HTMLInputElement, ICheckboxProps>(({\n id,\n label,\n size = "md",\n variant = "filled",\n className,\n disabled,\n ...restProps\n}: ICheckboxProps, ref ): ReactElement => {\n const inputId = useRef( useId());\n const { input, label: labelClass } = checkboxVariants({ variant, size, disabled: Boolean( disabled ) });\n\n // Create inline style for the clip-path for the checkmark\n const beforeStyle = {\n clipPath: checkmarkClipPath\n };\n\n return (\n <div className={cn( "inline-flex items-center gap-2", className )}>\n <input\n id={id || inputId.current}\n type="checkbox"\n ref={ref}\n className={input({ class: "before:[clip-path:var(--checkbox-clip-path)]" })}\n style={{ "--checkbox-clip-path": beforeStyle.clipPath } as React.CSSProperties}\n disabled={disabled}\n {...restProps}\n />\n {\n typeof label !== "undefined" && (\n <label\n htmlFor={id || inputId.current}\n className={labelClass()}\n >\n {label}\n </label>\n )\n }\n </div>\n );\n});\n\nCheckbox.displayName = "Checkbox";\n'
|
|
1711
1681
|
},
|
|
1712
1682
|
{
|
|
1713
1683
|
"name": "README.md",
|
|
@@ -1720,8 +1690,9 @@ export const checkmarkClipPath = \`polygon(
|
|
|
1720
1690
|
"name": "checkboxGroup",
|
|
1721
1691
|
"description": "The CheckboxGroup component manages a collection of related checkboxes with shared state. **When to use:** - Multiple choice selections - Preference settings with multiple options - Feature toggles - Filter selections **Component Architecture:** - Manages array of selected values - Styled with Tailwind CSS and cva - Built on Checkbox component - Supports controlled and uncontrolled modes",
|
|
1722
1692
|
"dependencies": [
|
|
1723
|
-
"
|
|
1724
|
-
"react"
|
|
1693
|
+
"tailwind-variants",
|
|
1694
|
+
"react",
|
|
1695
|
+
"tailwind-merge"
|
|
1725
1696
|
],
|
|
1726
1697
|
"internalDependencies": [
|
|
1727
1698
|
"checkbox"
|
|
@@ -1729,15 +1700,15 @@ export const checkmarkClipPath = \`polygon(
|
|
|
1729
1700
|
"files": [
|
|
1730
1701
|
{
|
|
1731
1702
|
"name": "index.ts",
|
|
1732
|
-
"content": 'export { CheckboxGroup, type ICheckboxGroupProps, type TCheckboxGroupSize } from "./checkboxGroup";\nexport { checkboxGroupVariants
|
|
1703
|
+
"content": 'export { CheckboxGroup, type ICheckboxGroupProps, type TCheckboxGroupSize } from "./checkboxGroup";\nexport { checkboxGroupVariants } from "./checkboxGroupVariants";'
|
|
1733
1704
|
},
|
|
1734
1705
|
{
|
|
1735
1706
|
"name": "checkboxGroupVariants.ts",
|
|
1736
|
-
"content": 'import {
|
|
1707
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const checkboxGroupVariants = tv({\n slots: {\n root: "inline-flex flex-col gap-4",\n item: [\n "flex gap-4 items-center rounded border border-secondary-100 outline-none cursor-pointer",\n "hover:border hover:border-primary-500",\n "focus-within:border-primary-500 focus-within:shadow-primary-2px",\n "dark:border-secondary-500",\n "dark:hover:border-primary-400",\n "dark:focus-within:border-primary-400 dark:focus-within:shadow-primary-2px"\n ],\n label: "text-secondary-500 dark:text-secondary-50"\n },\n variants: {\n size: {\n sm: { item: "text-base p-3" },\n md: { item: "text-base p-4" },\n lg: { item: "text-md p-4" }\n },\n checked: {\n true: { item: "bg-primary-50 dark:bg-secondary-600" }\n },\n disabled: {\n true: { item: "pointer-events-none bg-secondary-50 dark:bg-secondary-700 dark:text-secondary-400" }\n }\n },\n defaultVariants: {\n size: "md",\n checked: false,\n disabled: false\n }\n});\n'
|
|
1737
1708
|
},
|
|
1738
1709
|
{
|
|
1739
1710
|
"name": "checkboxGroup.tsx",
|
|
1740
|
-
"content": 'import * as React from "react";\nimport { type ReactElement, type ChangeEvent, useState, useEffect, useRef, useId } from "react";\nimport {
|
|
1711
|
+
"content": 'import * as React from "react";\nimport { type ReactElement, type ChangeEvent, useState, useEffect, useRef, useId } from "react";\nimport { Checkbox, type ICheckboxProps } from "../checkbox";\nimport { checkboxGroupVariants } from "./checkboxGroupVariants";\n\n/**\n * Type for CheckboxGroup size\n */\nexport type TCheckboxGroupSize = "sm" | "md" | "lg";\n\n/**\n * CheckboxGroup component props\n */\nexport interface ICheckboxGroupProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "onChange"> {\n /**\n * Size of the Checkbox group.\n * @default md\n */\n size?: TCheckboxGroupSize;\n /**\n * Pass space separated class names to override the CheckboxGroup styling\n */\n className?: string;\n /**\n * Pass boolean value to disable the component.\n * @default false\n */\n disabled?: boolean;\n /**\n * Selected values\n */\n selectedValues?: unknown[];\n /**\n * An array of objects of checkbox props type\n */\n options?: ICheckboxProps[];\n /**\n * Callback function that is called when the checkbox group selection changes.\n * An array of selected values is passed as an argument to the function.\n */\n onChange?: ( arg: unknown[]) => void;\n /**\n * Placement of the checkbox\n * @default "end"\n */\n checkboxPlacement?: "start" | "end";\n}\n\n/**\n * CheckboxGroup component for displaying a group of related checkboxes\n */\nexport const CheckboxGroup = React.forwardRef<HTMLDivElement, ICheckboxGroupProps>(({\n size = "md",\n options,\n onChange,\n className,\n selectedValues,\n disabled,\n checkboxPlacement = "end",\n ...props\n}, ref ): ReactElement => {\n const [ selected, setSelected ] = useState<unknown[]>( selectedValues ?? []);\n const inputId = useRef( useId());\n\n const onChangeHandler = ( event: ChangeEvent<HTMLInputElement> ): void => {\n const newSelected = event.currentTarget.checked\n ? [ ...selected, event.target.value ]\n : selected.filter(( value ) => value !== event.target.value );\n\n setSelected( newSelected );\n if ( onChange ) {\n onChange( newSelected );\n }\n };\n\n useEffect(() => {\n if ( selectedValues ) {\n setSelected( selectedValues );\n }\n }, [selectedValues]);\n\n const { root } = checkboxGroupVariants({ size });\n\n return (\n <div ref={ref} className={root({ class: className })} {...props}>\n {options?.map(( option, index ) => {\n const { id, name, value, label, className: optionClassName, disabled: optionDisabled, ...restOptionProps } = option;\n const isChecked = selected.includes( value );\n const isDisabled = disabled || optionDisabled;\n const { item, label: labelClass } = checkboxGroupVariants({ size, checked: isChecked, disabled: isDisabled });\n\n const renderCheckbox = (\n <Checkbox\n variant="filled-circle"\n type="checkbox"\n size={size}\n name={name}\n id={id || `${inputId.current}-${index}`}\n value={value}\n onChange={onChangeHandler}\n disabled={isDisabled}\n checked={isChecked}\n {...restOptionProps}\n />\n );\n\n return (\n <label\n key={index}\n htmlFor={id || `${inputId.current}-${index}`}\n className={item({ class: optionClassName })}\n >\n {checkboxPlacement === "start" && renderCheckbox}\n {label && <div className={labelClass()}>{label}</div>}\n {checkboxPlacement === "end" && renderCheckbox}\n </label>\n );\n })}\n </div>\n );\n});\n\nCheckboxGroup.displayName = "CheckboxGroup";\n'
|
|
1741
1712
|
},
|
|
1742
1713
|
{
|
|
1743
1714
|
"name": "README.md",
|
|
@@ -1750,9 +1721,10 @@ export const checkmarkClipPath = \`polygon(
|
|
|
1750
1721
|
"name": "ctaGroup",
|
|
1751
1722
|
"description": "The CTAGroup component provides a container for grouping call-to-action buttons with consistent spacing and layout. **When to use:** - Card headers with actions - Form action buttons - Toolbar buttons - Dialog actions **Component Architecture:** - Styled with Tailwind CSS and cva - Flexible layout options - Supports primary and secondary actions",
|
|
1752
1723
|
"dependencies": [
|
|
1753
|
-
"
|
|
1724
|
+
"tailwind-variants",
|
|
1754
1725
|
"react",
|
|
1755
|
-
"react-dom"
|
|
1726
|
+
"react-dom",
|
|
1727
|
+
"tailwind-merge"
|
|
1756
1728
|
],
|
|
1757
1729
|
"internalDependencies": [
|
|
1758
1730
|
"adpIcon",
|
|
@@ -1762,15 +1734,15 @@ export const checkmarkClipPath = \`polygon(
|
|
|
1762
1734
|
"files": [
|
|
1763
1735
|
{
|
|
1764
1736
|
"name": "index.ts",
|
|
1765
|
-
"content": 'export { CTAGroup, type ICTAGroupProps } from "./ctaGroup";\nexport { ctaGroupVariants
|
|
1737
|
+
"content": 'export { CTAGroup, type ICTAGroupProps } from "./ctaGroup";\nexport { ctaGroupVariants } from "./ctaGroupVariants";'
|
|
1766
1738
|
},
|
|
1767
1739
|
{
|
|
1768
1740
|
"name": "ctaGroupVariants.ts",
|
|
1769
|
-
"content": 'import {
|
|
1741
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const ctaGroupVariants = tv({\n slots: {\n root: "flex gap-1.5 items-center",\n cta: "flex justify-center items-center gap-1 rounded text-gray-800 disabled:text-gray-500 disabled:cursor-not-allowed",\n dropdownItem: "flex items-center justify-start gap-2"\n },\n variants: {\n variant: {\n icon: {\n cta: [\n "p-1",\n "outline-none p-1 bg-transparent font-regular text-base hover:bg-gray-100 focus-visible:bg-gray-50 ",\n "dark:text-secondary-50 dark:hover:bg-secondary-700 dark:hover:text-secondary-100 ",\n "dark:active:text-secondary-50 dark:focus-visible:bg-secondary-600 dark:focus-visible:text-secondary-200 ",\n "dark:focus-visible:active:text-secondary-50"\n ]\n },\n button: {\n cta: [\n "px-2 py-2",\n "border border-gray-400 dark:border-secondary-100",\n "text-base hover:bg-gray-100 focus-visible:bg-gray-50 ",\n "dark:text-secondary-50 dark:hover:bg-secondary-700 dark:hover:text-secondary-100",\n "dark:active:text-secondary-50 dark:focus-visible:bg-secondary-600 dark:focus-visible:text-secondary-200 ",\n "dark:focus-visible:active:text-secondary-50",\n "rounded"\n ]\n }\n }\n }\n});\n'
|
|
1770
1742
|
},
|
|
1771
1743
|
{
|
|
1772
1744
|
"name": "ctaGroup.tsx",
|
|
1773
|
-
"content": 'import * as React from "react";\nimport {
|
|
1745
|
+
"content": 'import * as React from "react";\n\nimport { useWindowSize } from "@hooks";\nimport { ADPIcon } from "../adpIcon";\nimport { Dropdown } from "../dropdown";\nimport { Tooltip } from "../tooltip";\nimport { ctaGroupVariants } from "./ctaGroupVariants";\nimport { mobileBreakPoint } from "@constants";\nexport interface ICTAGroupProps {\n /**\n * List of CTAs which render as icons or as dropdown items in case of mobile view\n */\n ctaList: {\n /** Callback to execute when the CTA is clicked */\n callback: () => void;\n /** Icon to render inside the CTA */\n icon: React.ReactNode;\n /** Label for the CTA. Also shown as tooltip when shrink is false. */\n label: React.ReactNode;\n /** Determines whether the CTA should be disabled */\n disabled?: boolean;\n }[];\n /** Determines whether to show the CTAGroup as a dropdown */\n shrink?: boolean;\n /** Overwrite CTAGroup container styles */\n className?: string;\n /** Reference element for rendering dropdown */\n dropdownPortalTarget?: Element;\n /** Threshold for moving extra CTAs into dropdown */\n threshold?: number;\n /** If true, labels are always shown next to icons */\n showLabels?: boolean;\n /**\n * Variant of the CTAGroup.\n * @default icon\n */\n variant?: "icon" | "button";\n}\n\nexport const CTAGroup = React.forwardRef<HTMLDivElement, ICTAGroupProps>(\n (\n {\n ctaList,\n shrink,\n className,\n dropdownPortalTarget,\n threshold,\n showLabels = false,\n variant = "icon"\n },\n ref\n ) => {\n const [ ctaShrinked, setCtaShrinked ] = React.useState( shrink ?? false );\n const { width } = useWindowSize();\n\n React.useEffect(() => {\n if ( shrink !== undefined ) {\n setCtaShrinked( shrink );\n } else {\n if ( width <= mobileBreakPoint && ctaList.length > 2 ) {\n setCtaShrinked( true );\n } else {\n setCtaShrinked( false );\n }\n }\n }, [ width, shrink, ctaList ]);\n\n const numberOfCTAs = ctaList.length;\n const showExtraDropdown = ( ctaShrinked || numberOfCTAs < 3 )\n ? false\n : threshold\n ? numberOfCTAs > threshold\n : false;\n\n const { root, cta: ctaClass, dropdownItem: dropdownItemClass } = ctaGroupVariants({ variant });\n\n const dropDownJSX = (\n <Dropdown\n portalTarget={dropdownPortalTarget}\n ctaClassName={ctaClass()}\n ctaContent={<ADPIcon size="sm" icon="v-menu-filled" />}\n >\n {( showExtraDropdown\n ? [...ctaList].slice( threshold!, numberOfCTAs )\n : ctaList\n ).map(({ icon, callback, label, disabled }, index ) => (\n <Dropdown.Item\n disabled={disabled}\n key={index}\n onClickHandler={callback}\n className={dropdownItemClass()}\n >\n {icon}\n {label}\n </Dropdown.Item>\n ))}\n </Dropdown>\n );\n\n if ( showExtraDropdown ) {\n return (\n <div\n ref={ref}\n data-testid="crossedThreshold"\n className={root({ class: className })}\n >\n {!ctaShrinked &&\n [...ctaList]\n .slice( 0, threshold! )\n .map(({ icon, callback, disabled, label }, index ) =>\n showLabels ? (\n <button\n key={index}\n className={ctaClass()}\n onClick={callback}\n disabled={disabled}\n >\n {icon}\n {label}\n </button>\n ) : (\n <Tooltip\n key={index}\n clickable={false}\n placement="bottom"\n onTriggerClick={() => {\n if ( !disabled ) {\n callback();\n }\n }}\n trigger={icon}\n triggerAriaLabel={label as string}\n triggerElementClassName={ctaClass()}\n disabled={disabled}\n >\n {label}\n </Tooltip>\n )\n )}\n {dropDownJSX}\n </div>\n );\n }\n\n return ctaShrinked ? (\n dropDownJSX\n ) : (\n <div\n ref={ref}\n data-testid="ctaGroup"\n className={root({ class: className })}\n >\n {ctaList.map(({ icon, callback, disabled, label }, index ) =>\n showLabels ? (\n <button\n key={index}\n className={ctaClass()}\n onClick={callback}\n disabled={disabled}\n >\n {icon}\n {label}\n </button>\n ) : (\n <Tooltip\n key={index}\n clickable={false}\n placement="bottom"\n onTriggerClick={() => {\n if ( !disabled ) {\n callback();\n }\n }}\n trigger={icon}\n triggerAriaLabel={label as string}\n triggerElementClassName={ctaClass()}\n disabled={disabled}\n >\n {label}\n </Tooltip>\n )\n )}\n </div>\n );\n }\n);\n\nCTAGroup.displayName = "CTAGroup";\n'
|
|
1774
1746
|
},
|
|
1775
1747
|
{
|
|
1776
1748
|
"name": "README.md",
|
|
@@ -1786,9 +1758,9 @@ export const checkmarkClipPath = \`polygon(
|
|
|
1786
1758
|
"react",
|
|
1787
1759
|
"@tanstack/react-table",
|
|
1788
1760
|
"@tanstack/react-virtual",
|
|
1789
|
-
"
|
|
1761
|
+
"tailwind-variants",
|
|
1790
1762
|
"react-datepicker",
|
|
1791
|
-
"
|
|
1763
|
+
"tailwind-merge"
|
|
1792
1764
|
],
|
|
1793
1765
|
"internalDependencies": [
|
|
1794
1766
|
"adpIcon",
|
|
@@ -1813,11 +1785,11 @@ export const checkmarkClipPath = \`polygon(
|
|
|
1813
1785
|
},
|
|
1814
1786
|
{
|
|
1815
1787
|
"name": "table.tsx",
|
|
1816
|
-
"content": 'import { type CSSProperties, useRef, useState } from "react";\nimport { flexRender, type Table as TableType } from "@tanstack/react-table";\nimport { useVirtualizer } from "@tanstack/react-virtual";\nimport { cn } from "@utils";\n\nimport { Loader } from "../loader/loader";\nimport { ADPIcon, iconList } from "../adpIcon/adpIcon";\nimport { EmptyState } from "../emptyState/emptyState";\nimport {\n dataTableVariants,\n dataTableHeadVariants,\n dataTableHeaderCellVariants,\n dataTableCellVariants,\n dataTableSelectableVariants,\n dataTableResizerVariants,\n dataTableEmptyStateVariants\n} from "./dataTableVariants";\n\nexport type TTableProps<T> = {\n table: TableType<T>;\n /**\n * Enable column virtualization\n * @default false\n */\n columnVirtualization?: boolean;\n /**\n * Enable row virtualization\n * @default false\n */\n rowVirtualization?: boolean;\n /**\n * Make the columns selectable\n * @default false\n */\n selectable?: boolean;\n /**\n * Loading state of the table\n * @default false\n */\n loading?: boolean;\n /**\n * Enable column resizing\n * @default false\n */\n resizable?: boolean;\n /**\n * Height of the table\n * @default "auto"\n */\n height?: string;\n /**\n * Enable RTL mode\n * @default false\n */\n isRtl?: boolean\n};\n\nexport const Table = <T extends object>({\n table,\n columnVirtualization = false,\n rowVirtualization = false,\n selectable = false,\n resizable = false,\n loading = false,\n height,\n isRtl = false\n}: TTableProps<T> ) => {\n const tableContainerRef = useRef<HTMLDivElement>( null );\n const [ highlightFirstRow, setHighlightFirstRow ] = useState<null | string>();\n const { rows } = table.getRowModel();\n\n const visibleColumns = table.getVisibleLeafColumns();\n const columnVirtualizer = useVirtualizer({\n count: visibleColumns.length,\n estimateSize: ( index ) => visibleColumns[index].getSize(),\n getScrollElement: () => tableContainerRef.current,\n horizontal: true,\n overscan: 50\n });\n\n const rowVirtualizer = useVirtualizer({\n count: rows.length,\n estimateSize: () => 56,\n getScrollElement: () => tableContainerRef.current,\n //measure dynamic row height, except in firefox because it measures table border height incorrectly\n measureElement:\n typeof window !== "undefined" && navigator.userAgent.indexOf( "Firefox" ) === -1\n ? ( element ) => element?.getBoundingClientRect().height\n : undefined,\n overscan: 10\n });\n\n const virtualColumns = columnVirtualization ? columnVirtualizer.getVirtualItems() : [];\n const virtualRows = rowVirtualization ? rowVirtualizer.getVirtualItems() : [];\n\n let virtualPaddingLeft: number | undefined;\n let virtualPaddingRight: number | undefined;\n\n if ( columnVirtualization && virtualColumns?.length ) {\n virtualPaddingLeft = virtualColumns[0]?.start ?? 0;\n virtualPaddingRight = columnVirtualizer.getTotalSize() - ( virtualColumns[virtualColumns.length - 1]?.end ?? 0 );\n }\n\n const getCommonPinningStyles = ( column: any ): CSSProperties => {\n const isPinned = column.getIsPinned();\n\n const styles: CSSProperties = {\n position: isPinned ? "sticky" : "relative",\n zIndex: isPinned ? 1 : 0,\n boxShadow: isPinned ? "0 4px 32px -2px rgba(27, 37, 51, 0.06)" : "none"\n };\n\n // Adjust for RTL mode\n if ( isPinned ) {\n if ( isRtl ) {\n if ( isPinned === "left" ) {\n styles.right = `${column.getStart( "right" )}px`;\n } else if ( isPinned === "right" ) {\n styles.left = `${column.getStart( "left" )}px`;\n }\n } else {\n if ( isPinned === "left" ) {\n styles.left = `${column.getStart( "left" )}px`;\n } else if ( isPinned === "right" ) {\n styles.right = `${column.getStart( "right" )}px`;\n }\n }\n }\n\n return styles;\n };\n\n return (\n <div\n ref={tableContainerRef}\n className="overflow-auto max-h-full"\n style={{ height: rowVirtualization ? "calc(100% - 20px)" : height }}\n >\n <table className={cn( dataTableVariants())} style={{ width: "100%" }}>\n <thead className={cn( dataTableHeadVariants())}>\n {columnVirtualization\n ? (\n <>\n {table?.getHeaderGroups()?.map(( headerGroup: any ) => (\n <tr key={headerGroup.id}>\n {virtualPaddingLeft ? (\n //fake empty column to the left for virtualization scroll padding\n <th style={{ display: "flex", width: virtualPaddingLeft }} />\n ) : null}\n {virtualColumns.map(( vc ) => {\n const header = headerGroup.headers[vc?.index];\n return (\n <th\n key={header.id}\n className={cn(\n dataTableHeaderCellVariants(),\n selectable && dataTableSelectableVariants()\n )}\n style={{\n ...getCommonPinningStyles( header.column ),\n width: header.getSize()\n }}\n >\n <div\n className={cn(\n "flex items-center gap-1",\n header.column.getCanSort() ? "cursor-pointer select-none" : ""\n )}\n onClick={header.column.getToggleSortingHandler()}\n >\n {flexRender( header.column.columnDef.header, header.getContext())}\n {{\n asc: (\n <ADPIcon\n icon={iconList.upArrowLong}\n className="inline-flex ml-1"\n size="xs"\n />\n ),\n desc: (\n <ADPIcon\n icon={iconList.downArrowLong}\n className="inline-flex ml-1"\n size="xs"\n />\n )\n }[header.column.getIsSorted() as string] ?? null}\n </div>\n {resizable && header.column.getCanResize() && (\n <div\n className={cn(\n dataTableResizerVariants({\n direction: isRtl ? "rtl" : "ltr",\n isResizing: header.column.getIsResizing()\n })\n )}\n onMouseDown={header.getResizeHandler()}\n onTouchStart={header.getResizeHandler()}\n />\n )}\n </th>\n );\n })}\n {virtualPaddingRight ? (\n //fake empty column to the right for virtualization scroll padding\n <th style={{ display: "flex", width: virtualPaddingRight }} />\n ) : null}\n </tr>\n ))}\n </>\n )\n : (\n <>\n {table?.getHeaderGroups()?.map(( headerGroup: any ) => (\n <tr key={headerGroup.id}>\n {headerGroup.headers.map(( header: any ) => {\n return (\n <th\n key={header.id}\n className={cn(\n dataTableHeaderCellVariants(),\n selectable && dataTableSelectableVariants()\n )}\n style={{\n ...getCommonPinningStyles( header.column ),\n width: header.getSize()\n }}\n >\n <div\n className={cn(\n "flex items-center gap-1",\n header.column.getCanSort() ? "cursor-pointer select-none" : ""\n )}\n onClick={header.column.getToggleSortingHandler()}\n >\n {flexRender( header.column.columnDef.header, header.getContext())}\n {{\n asc: (\n <ADPIcon\n icon={iconList.upArrowLong}\n className="inline-flex ml-1"\n size="xs"\n />\n ),\n desc: (\n <ADPIcon\n icon={iconList.downArrowLong}\n className="inline-flex ml-1"\n size="xs"\n />\n )\n }[header.column.getIsSorted() as string] ?? null}\n </div>\n {resizable && header.column.getCanResize() && (\n <div\n className={cn(\n dataTableResizerVariants({\n direction: isRtl ? "rtl" : "ltr",\n isResizing: header.column.getIsResizing()\n })\n )}\n onMouseDown={header.getResizeHandler()}\n onTouchStart={header.getResizeHandler()}\n />\n )}\n </th>\n );\n })}\n </tr>\n ))}\n </>\n )}\n </thead>\n\n <tbody>\n {loading ? (\n <tr className="h-full">\n <td colSpan={table.getAllColumns().length} className="text-center">\n <Loader />\n </td>\n </tr>\n ) : rows.length === 0 ? (\n <tr>\n <td colSpan={table.getAllColumns().length}>\n <div className={cn( dataTableEmptyStateVariants())}>\n <EmptyState imageVariant="analytics">\n <EmptyState.Content className="text-md text-secondary-500 font-semibold dark:text-secondary-50">\n There are no records available\n </EmptyState.Content>\n </EmptyState>\n </div>\n </td>\n </tr>\n ) : rowVirtualization ? (\n <>\n {/* Spacer row at the top for virtualization */}\n {virtualRows.length > 0 && (\n <tr style={{ height: `${virtualRows[0]?.start || 0}px` }}>\n <td colSpan={table.getAllColumns().length} />\n </tr>\n )}\n\n {/* Virtualized rows */}\n {virtualRows.map(( virtualRow: any ) => {\n const row = rows[virtualRow.index];\n return (\n <tr\n key={row.id}\n data-index={virtualRow.index}\n ref={( node ) => rowVirtualizer.measureElement( node )}\n >\n {columnVirtualization ? (\n <>\n {virtualPaddingLeft ? (\n <td style={{ display: "flex", width: virtualPaddingLeft }} />\n ) : null}\n\n {virtualColumns.map(( vc ) => {\n const cell = row.getVisibleCells()[vc.index];\n return (\n <td\n key={cell.id}\n className={cn(\n dataTableCellVariants(),\n selectable && dataTableSelectableVariants(),\n highlightFirstRow === row.id ? "bg-gray-50 dark:bg-secondary-600" : "bg-white dark:bg-iridium"\n )}\n style={{\n ...getCommonPinningStyles( cell.column ),\n width: cell.column.getSize(),\n wordBreak: "break-word"\n }}\n onMouseEnter={() => setHighlightFirstRow( row.id )}\n onMouseLeave={() => setHighlightFirstRow( null )}\n >\n {flexRender( cell.column.columnDef.cell, cell.getContext())}\n </td>\n );\n })}\n\n {virtualPaddingRight ? (\n <td style={{ display: "flex", width: virtualPaddingRight }} />\n ) : null}\n </>\n ) : (\n row.getVisibleCells().map(( cell: any ) => (\n <td\n key={cell.id}\n className={cn(\n dataTableCellVariants(),\n selectable && dataTableSelectableVariants(),\n highlightFirstRow === row.id ? "bg-gray-50 dark:bg-secondary-600" : "bg-white dark:bg-iridium"\n )}\n style={{\n ...getCommonPinningStyles( cell.column ),\n width: cell.column.getSize(),\n wordBreak: "break-word"\n }}\n onMouseEnter={() => setHighlightFirstRow( row.id )}\n onMouseLeave={() => setHighlightFirstRow( null )}\n >\n {flexRender( cell.column.columnDef.cell, cell.getContext())}\n </td>\n ))\n )}\n </tr>\n );\n })}\n\n {/* Spacer row at the bottom for virtualization */}\n <tr\n style={{\n height: `${\n rowVirtualizer.getTotalSize() -\n ( virtualRows[virtualRows.length - 1]?.end || 0 )\n }px`\n }}\n >\n <td colSpan={table.getAllColumns().length} />\n </tr>\n </>\n ) : (\n // Non-virtualized rows\n table.getRowModel().rows.map(( row ) => (\n <tr key={row.id}>\n {row.getVisibleCells().map(( cell ) => (\n <td\n key={cell.id}\n className={cn(\n dataTableCellVariants(),\n selectable && dataTableSelectableVariants(),\n highlightFirstRow === row.id ? "bg-gray-50 dark:bg-secondary-600" : "bg-white dark:bg-iridium"\n )}\n style={{\n ...getCommonPinningStyles( cell.column ),\n width: cell.column.getSize(),\n wordBreak: "break-word"\n }}\n onMouseEnter={() => setHighlightFirstRow( row.id )}\n onMouseLeave={() => setHighlightFirstRow( null )}\n >\n {flexRender( cell.column.columnDef.cell, cell.getContext())}\n </td>\n ))}\n </tr>\n ))\n )}\n </tbody>\n </table>\n </div>\n );\n};\n\nexport default Table;\n'
|
|
1788
|
+
"content": 'import { type CSSProperties, useRef, useState } from "react";\nimport { flexRender, type Table as TableType } from "@tanstack/react-table";\nimport { useVirtualizer } from "@tanstack/react-virtual";\nimport { cn } from "tailwind-variants";\n\nimport { Loader } from "../loader/loader";\nimport { ADPIcon, iconList } from "../adpIcon/adpIcon";\nimport { EmptyState } from "../emptyState/emptyState";\nimport { dataTableVariants } from "./dataTableVariants";\n\nexport type TTableProps<T> = {\n table: TableType<T>;\n /**\n * Enable column virtualization\n * @default false\n */\n columnVirtualization?: boolean;\n /**\n * Enable row virtualization\n * @default false\n */\n rowVirtualization?: boolean;\n /**\n * Make the columns selectable\n * @default false\n */\n selectable?: boolean;\n /**\n * Loading state of the table\n * @default false\n */\n loading?: boolean;\n /**\n * Enable column resizing\n * @default false\n */\n resizable?: boolean;\n /**\n * Height of the table\n * @default "auto"\n */\n height?: string;\n /**\n * Enable RTL mode\n * @default false\n */\n isRtl?: boolean\n};\n\nexport const Table = <T extends object>({\n table,\n columnVirtualization = false,\n rowVirtualization = false,\n selectable = false,\n resizable = false,\n loading = false,\n height,\n isRtl = false\n}: TTableProps<T> ) => {\n const tableContainerRef = useRef<HTMLDivElement>( null );\n const [ highlightFirstRow, setHighlightFirstRow ] = useState<null | string>();\n const { rows } = table.getRowModel();\n\n const visibleColumns = table.getVisibleLeafColumns();\n const columnVirtualizer = useVirtualizer({\n count: visibleColumns.length,\n estimateSize: ( index ) => visibleColumns[index].getSize(),\n getScrollElement: () => tableContainerRef.current,\n horizontal: true,\n overscan: 50\n });\n\n const rowVirtualizer = useVirtualizer({\n count: rows.length,\n estimateSize: () => 56,\n getScrollElement: () => tableContainerRef.current,\n //measure dynamic row height, except in firefox because it measures table border height incorrectly\n measureElement:\n typeof window !== "undefined" && navigator.userAgent.indexOf( "Firefox" ) === -1\n ? ( element ) => element?.getBoundingClientRect().height\n : undefined,\n overscan: 10\n });\n\n const virtualColumns = columnVirtualization ? columnVirtualizer.getVirtualItems() : [];\n const virtualRows = rowVirtualization ? rowVirtualizer.getVirtualItems() : [];\n\n let virtualPaddingLeft: number | undefined;\n let virtualPaddingRight: number | undefined;\n\n if ( columnVirtualization && virtualColumns?.length ) {\n virtualPaddingLeft = virtualColumns[0]?.start ?? 0;\n virtualPaddingRight = columnVirtualizer.getTotalSize() - ( virtualColumns[virtualColumns.length - 1]?.end ?? 0 );\n }\n\n const getCommonPinningStyles = ( column: any ): CSSProperties => {\n const isPinned = column.getIsPinned();\n\n const styles: CSSProperties = {\n position: isPinned ? "sticky" : "relative",\n zIndex: isPinned ? 1 : 0,\n boxShadow: isPinned ? "0 4px 32px -2px rgba(27, 37, 51, 0.06)" : "none"\n };\n\n // Adjust for RTL mode\n if ( isPinned ) {\n if ( isRtl ) {\n if ( isPinned === "left" ) {\n styles.right = `${column.getStart( "right" )}px`;\n } else if ( isPinned === "right" ) {\n styles.left = `${column.getStart( "left" )}px`;\n }\n } else {\n if ( isPinned === "left" ) {\n styles.left = `${column.getStart( "left" )}px`;\n } else if ( isPinned === "right" ) {\n styles.right = `${column.getStart( "right" )}px`;\n }\n }\n }\n\n return styles;\n };\n\n const {\n table: tableClass,\n head,\n headerCell,\n selectable: selectableClass,\n cell,\n emptyState: emptyStateClass\n } = dataTableVariants();\n\n const direction = isRtl ? "rtl" : "ltr";\n\n return (\n <div\n ref={tableContainerRef}\n className="overflow-auto max-h-full"\n style={{ height: rowVirtualization ? "calc(100% - 20px)" : height }}\n >\n <table className={tableClass()} style={{ width: "100%" }}>\n <thead className={head()}>\n {columnVirtualization\n ? (\n <>\n {table?.getHeaderGroups()?.map(( headerGroup: any ) => (\n <tr key={headerGroup.id}>\n {virtualPaddingLeft ? (\n //fake empty column to the left for virtualization scroll padding\n <th style={{ display: "flex", width: virtualPaddingLeft }} />\n ) : null}\n {virtualColumns.map(( vc ) => {\n const header = headerGroup.headers[vc?.index];\n return (\n <th\n key={header.id}\n className={cn(\n headerCell(),\n selectable && selectableClass()\n )}\n style={{\n ...getCommonPinningStyles( header.column ),\n width: header.getSize()\n }}\n >\n <div\n className={cn(\n "flex items-center gap-1",\n header.column.getCanSort() ? "cursor-pointer select-none" : ""\n )}\n onClick={header.column.getToggleSortingHandler()}\n >\n {flexRender( header.column.columnDef.header, header.getContext())}\n {{\n asc: (\n <ADPIcon\n icon={iconList.upArrowLong}\n className="inline-flex ml-1"\n size="xs"\n />\n ),\n desc: (\n <ADPIcon\n icon={iconList.downArrowLong}\n className="inline-flex ml-1"\n size="xs"\n />\n )\n }[header.column.getIsSorted() as string] ?? null}\n </div>\n {resizable && header.column.getCanResize() && (\n <div\n className={dataTableVariants({\n direction,\n isResizing: header.column.getIsResizing()\n }).resizer()}\n onMouseDown={header.getResizeHandler()}\n onTouchStart={header.getResizeHandler()}\n />\n )}\n </th>\n );\n })}\n {virtualPaddingRight ? (\n //fake empty column to the right for virtualization scroll padding\n <th style={{ display: "flex", width: virtualPaddingRight }} />\n ) : null}\n </tr>\n ))}\n </>\n )\n : (\n <>\n {table?.getHeaderGroups()?.map(( headerGroup: any ) => (\n <tr key={headerGroup.id}>\n {headerGroup.headers.map(( header: any ) => {\n return (\n <th\n key={header.id}\n className={cn(\n headerCell(),\n selectable && selectableClass()\n )}\n style={{\n ...getCommonPinningStyles( header.column ),\n width: header.getSize()\n }}\n >\n <div\n className={cn(\n "flex items-center gap-1",\n header.column.getCanSort() ? "cursor-pointer select-none" : ""\n )}\n onClick={header.column.getToggleSortingHandler()}\n >\n {flexRender( header.column.columnDef.header, header.getContext())}\n {{\n asc: (\n <ADPIcon\n icon={iconList.upArrowLong}\n className="inline-flex ml-1"\n size="xs"\n />\n ),\n desc: (\n <ADPIcon\n icon={iconList.downArrowLong}\n className="inline-flex ml-1"\n size="xs"\n />\n )\n }[header.column.getIsSorted() as string] ?? null}\n </div>\n {resizable && header.column.getCanResize() && (\n <div\n className={dataTableVariants({\n direction,\n isResizing: header.column.getIsResizing()\n }).resizer()}\n onMouseDown={header.getResizeHandler()}\n onTouchStart={header.getResizeHandler()}\n />\n )}\n </th>\n );\n })}\n </tr>\n ))}\n </>\n )}\n </thead>\n\n <tbody>\n {loading ? (\n <tr className="h-full">\n <td colSpan={table.getAllColumns().length} className="text-center">\n <Loader />\n </td>\n </tr>\n ) : rows.length === 0 ? (\n <tr>\n <td colSpan={table.getAllColumns().length}>\n <div className={emptyStateClass()}>\n <EmptyState imageVariant="analytics">\n <EmptyState.Content className="text-md text-secondary-500 font-semibold dark:text-secondary-50">\n There are no records available\n </EmptyState.Content>\n </EmptyState>\n </div>\n </td>\n </tr>\n ) : rowVirtualization ? (\n <>\n {/* Spacer row at the top for virtualization */}\n {virtualRows.length > 0 && (\n <tr style={{ height: `${virtualRows[0]?.start || 0}px` }}>\n <td colSpan={table.getAllColumns().length} />\n </tr>\n )}\n\n {/* Virtualized rows */}\n {virtualRows.map(( virtualRow: any ) => {\n const row = rows[virtualRow.index];\n return (\n <tr\n key={row.id}\n data-index={virtualRow.index}\n ref={( node ) => rowVirtualizer.measureElement( node )}\n >\n {columnVirtualization ? (\n <>\n {virtualPaddingLeft ? (\n <td style={{ display: "flex", width: virtualPaddingLeft }} />\n ) : null}\n\n {virtualColumns.map(( vc ) => {\n const tableCell = row.getVisibleCells()[vc.index];\n return (\n <td\n key={tableCell.id}\n className={cn(\n cell(),\n selectable && selectableClass(),\n highlightFirstRow === row.id ? "bg-gray-50 dark:bg-secondary-600" : "bg-white dark:bg-iridium"\n )}\n style={{\n ...getCommonPinningStyles( tableCell.column ),\n width: tableCell.column.getSize(),\n wordBreak: "break-word"\n }}\n onMouseEnter={() => setHighlightFirstRow( row.id )}\n onMouseLeave={() => setHighlightFirstRow( null )}\n >\n {flexRender( tableCell.column.columnDef.cell, tableCell.getContext())}\n </td>\n );\n })}\n\n {virtualPaddingRight ? (\n <td style={{ display: "flex", width: virtualPaddingRight }} />\n ) : null}\n </>\n ) : (\n row.getVisibleCells().map(( tableCell: any ) => (\n <td\n key={tableCell.id}\n className={cn(\n cell(),\n selectable && selectableClass(),\n highlightFirstRow === row.id ? "bg-gray-50 dark:bg-secondary-600" : "bg-white dark:bg-iridium"\n )}\n style={{\n ...getCommonPinningStyles( tableCell.column ),\n width: tableCell.column.getSize(),\n wordBreak: "break-word"\n }}\n onMouseEnter={() => setHighlightFirstRow( row.id )}\n onMouseLeave={() => setHighlightFirstRow( null )}\n >\n {flexRender( tableCell.column.columnDef.cell, tableCell.getContext())}\n </td>\n ))\n )}\n </tr>\n );\n })}\n\n {/* Spacer row at the bottom for virtualization */}\n <tr\n style={{\n height: `${\n rowVirtualizer.getTotalSize() -\n ( virtualRows[virtualRows.length - 1]?.end || 0 )\n }px`\n }}\n >\n <td colSpan={table.getAllColumns().length} />\n </tr>\n </>\n ) : (\n // Non-virtualized rows\n table.getRowModel().rows.map(( row ) => (\n <tr key={row.id}>\n {row.getVisibleCells().map(( tableCell ) => (\n <td\n key={tableCell.id}\n className={cn(\n cell(),\n selectable && selectableClass(),\n highlightFirstRow === row.id ? "bg-gray-50 dark:bg-secondary-600" : "bg-white dark:bg-iridium"\n )}\n style={{\n ...getCommonPinningStyles( tableCell.column ),\n width: tableCell.column.getSize(),\n wordBreak: "break-word"\n }}\n onMouseEnter={() => setHighlightFirstRow( row.id )}\n onMouseLeave={() => setHighlightFirstRow( null )}\n >\n {flexRender( tableCell.column.columnDef.cell, tableCell.getContext())}\n </td>\n ))}\n </tr>\n ))\n )}\n </tbody>\n </table>\n </div>\n );\n};\n\nexport default Table;\n'
|
|
1817
1789
|
},
|
|
1818
1790
|
{
|
|
1819
1791
|
"name": "selectedFilterList.tsx",
|
|
1820
|
-
"content": 'import { useMemo } from "react";\nimport {
|
|
1792
|
+
"content": 'import { useMemo } from "react";\nimport { ADPIcon, iconList } from "../adpIcon/adpIcon";\nimport { Badge } from "../badge/badge";\nimport { Button } from "../button/button";\nimport { dataTableVariants } from "./dataTableVariants";\nimport type { TFilterItem, TAdvancedFilterItem } from "./types";\nimport { type TIconType } from "../adpIcon/svgObjects";\n\ninterface IFilterOption {\n label: string;\n value: string;\n}\n\ninterface ISelectedFilters {\n [key: string]: IFilterOption[] | IFilterOption;\n}\n\ninterface ISelectedFilterListProps {\n advancedFilters: TAdvancedFilterItem[];\n selectedFilters: ISelectedFilters;\n basicFilters: TFilterItem[];\n handleRemoveFilter: ( filterId: string, index?: number ) => void;\n handleClearAllSelectedFilters: () => void;\n}\n\nexport const SelectedFilterList = ({\n advancedFilters,\n selectedFilters,\n basicFilters,\n handleRemoveFilter,\n handleClearAllSelectedFilters\n}: ISelectedFilterListProps ) => {\n const icons = useMemo(() => {\n const iconMap: { [key: string]: TIconType | undefined } = {};\n\n Object.keys( selectedFilters ).forEach(( key ) => {\n const icon =\n advancedFilters?.find(( el ) => el?.id === key )?.icon ||\n basicFilters?.find(( el ) => el?.id === key )?.icon;\n iconMap[key] = icon;\n });\n\n return iconMap;\n }, [ advancedFilters, basicFilters, selectedFilters ]);\n\n const { selectedFilterContainer, selectedFilter } = dataTableVariants();\n\n if ( Object.keys( selectedFilters ).length === 0 ) {\n return null;\n }\n\n return (\n <div className={selectedFilterContainer()}>\n {Object.keys( selectedFilters ).map(( key ) => {\n const value = selectedFilters[key];\n const icon = icons[key];\n const filterName = advancedFilters?.find(( el ) => el?.id === key )?.title ||\n basicFilters?.find(( el ) => el?.id === key )?.title;\n\n if ( Array.isArray( value )) {\n return value.map(( item, index ) => (\n <div key={`${key}-${index}`}>\n <Badge\n key={index}\n size="sm"\n color="secondary"\n variant="outline"\n prefixIcon={icon ? <ADPIcon size="xxs" icon={icon} /> : undefined}\n label={item.label}\n suffixIcon={<ADPIcon icon={iconList.cross} size="xs" onClick={() => handleRemoveFilter( key, index )} />}\n className={selectedFilter()}\n title={`${filterName}: ${item.label}`}\n />\n </div>\n ));\n } else if ( typeof value === "object" ) {\n return (\n <div key={key}>\n <Badge\n size="sm"\n color="secondary"\n variant="outline"\n prefixIcon={icon ? <ADPIcon icon={icon} size="xxs" /> : undefined}\n label={value.label}\n suffixIcon={<ADPIcon icon={iconList.cross} size="xs" onClick={() => handleRemoveFilter( key, 0 )} />}\n className={selectedFilter()}\n title={`${filterName}: ${value.label}`}\n />\n </div>\n );\n }\n return null;\n })}\n {Object.keys( selectedFilters )?.length > 0 && (\n <Button onClick={handleClearAllSelectedFilters} variant="text" size="xs" className="font-medium">\n Clear all\n </Button>\n )}\n </div>\n );\n};\n\nexport default SelectedFilterList;\n'
|
|
1821
1793
|
},
|
|
1822
1794
|
{
|
|
1823
1795
|
"name": "pageSizeModal.tsx",
|
|
@@ -1825,19 +1797,19 @@ export const checkmarkClipPath = \`polygon(
|
|
|
1825
1797
|
},
|
|
1826
1798
|
{
|
|
1827
1799
|
"name": "index.ts",
|
|
1828
|
-
"content": '// Export the components\nexport { DataTable, type IDataTableProps } from "./dataTable";\nexport { type TColumn, type TFilterItem, type TAdvancedFilterItem, type TFilters } from "./types";\nexport { AdvancedFilters } from "./advancedFilters";\nexport { DataTableColumnSettings } from "./DataTableColumnSettings";\nexport { PageSizeModal } from "./pageSizeModal";\n\n// Export the variants\nexport {
|
|
1800
|
+
"content": '// Export the components\nexport { DataTable, type IDataTableProps } from "./dataTable";\nexport { type TColumn, type TFilterItem, type TAdvancedFilterItem, type TFilters } from "./types";\nexport { AdvancedFilters } from "./advancedFilters";\nexport { DataTableColumnSettings } from "./DataTableColumnSettings";\nexport { PageSizeModal } from "./pageSizeModal";\n\n// Export the variants\nexport { dataTableVariants } from "./dataTableVariants";'
|
|
1829
1801
|
},
|
|
1830
1802
|
{
|
|
1831
1803
|
"name": "dataTableVariants.ts",
|
|
1832
|
-
"content": 'import {
|
|
1804
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const dataTableVariants = tv({\n slots: {\n container: "relative overflow-hidden p-1 w-full",\n table: "w-full h-full table-auto overflow-scroll border-collapse",\n cell: [\n "text-sm text-secondary-500 min-h-12 px-6 py-4 min-w-40 border-b border-gray-100 first:bg-white",\n "dark:bg-transparent dark:text-secondary-50 dark:first:bg-iridium dark:border-secondary-500"\n ],\n headerCell: [\n "text-xs font-medium text-secondary-500 text-start h-8 px-6 py-2.5 min-w-40 border-b border-gray-100",\n "capitalize bg-gray-100 sticky top-0 dark:!bg-secondary-500 dark:text-secondary-50 dark:border-secondary-500"\n ],\n head: "sticky top-0 z-10",\n selectable: "first:w-[56px] first:pr-3 first:min-w-[44px] first:pl-6",\n pagination: "px-6 py-3.5 border border-secondary-100 border-t-0 rounded-b dark:border-secondary-500",\n filters: "flex flex-wrap items-center gap-4 mb-4",\n filterSearch: "w-full md:max-w-[22rem]",\n filterSelect: "max-w-96 min-w-52",\n advancedFilterButton: "min-w-fit font-medium text-primary-500 dark:text-primary-400 h-9",\n resizer: [\n "opacity-0 absolute top-1/2 rounded-sm -translate-y-1/2 h-3/5 w-0.5",\n "bg-secondary-300 cursor-col-resize select-none touch-none hover:opacity-100"\n ],\n containerInner: "overflow-auto relative rounded-t border border-gray-100 z-0 dark:border-secondary-500",\n emptyState: "h-screen w-full inline-flex gap-4 flex-col items-center justify-center",\n selectedFilter: "text-gray-800 dark:bg-secondary-600 dark:border dark:border-secondary-500",\n selectedFilterContainer: "flex gap-3 mb-4 flex-wrap",\n selectedFilterLabel: "text-secondary-400 text-xs dark:text-secondary-200",\n recordsInfo: "text-secondary-400 text-sm mb-4 dark:text-secondary-200",\n columnSettings: [\n "relative py-1 px-1 rounded hover:bg-gray-100 text-gray-700 cursor-pointer overflow-hidden",\n "dark:text-secondary-200 dark:hover:bg-secondary-700"\n ]\n },\n variants: {\n direction: {\n ltr: { resizer: "right-0" },\n rtl: { resizer: "left-0" }\n },\n isResizing: {\n true: { resizer: "bg-secondary-500 opacity-100" }\n }\n },\n defaultVariants: {\n direction: "ltr"\n }\n});\n'
|
|
1833
1805
|
},
|
|
1834
1806
|
{
|
|
1835
1807
|
"name": "dataTable.tsx",
|
|
1836
|
-
"content": 'import * as React from "react";\nimport { type ReactNode, useCallback, useEffect, useRef, useState } from "react";\nimport {\n type CellContext,\n createColumnHelper,\n getCoreRowModel,\n getFilteredRowModel,\n getSortedRowModel,\n useReactTable,\n type SortingState,\n type ColumnHelper,\n type ColumnPinningState,\n type Table as TReactTable,\n type OnChangeFn,\n type RowSelectionState\n} from "@tanstack/react-table";\n\nimport { Button } from "../button/button";\nimport { TextField } from "../textField/textField";\nimport { Select } from "../select/select";\nimport { ADPIcon, iconList } from "../adpIcon/adpIcon";\nimport { Pagination } from "../pagination/pagination";\nimport { Checkbox } from "../checkbox/checkbox";\nimport { cn, debounce, findParentAttribute } from "@utils";\nimport {\n dataTableContainerVariants,\n dataTableFiltersVariants,\n dataTableFilterSearchVariants,\n dataTableFilterSelectVariants,\n dataTableRecordsInfoVariants,\n dataTableContainerInnerVariants,\n dataTablePaginationVariants\n} from "./dataTableVariants";\nimport { PageSizeModal } from "./pageSizeModal";\nimport { DataTableColumnSettings } from "./DataTableColumnSettings";\nimport { AdvancedFilters } from "./advancedFilters";\nimport SelectedFilterList from "./selectedFilterList";\nimport { Table } from "./table";\nimport type { TColumn, TFilterItem, TAdvancedFilterItem, TFilters } from "./types";\n\n// Utility function to derive configured columns\nconst deriveConfiguredColumns = (\n rawColumns: TColumn[],\n selectable: boolean,\n columnHelper: ColumnHelper<any>\n) => {\n const derivedColumns = [\n ...( selectable\n ? [\n columnHelper.accessor( "select", {\n id: "select",\n header: ({ table }) => (\n <Checkbox\n checked={table.getIsAllRowsSelected()}\n // Use data-* attribute for indeterminate state\n data-indeterminate={table.getIsSomeRowsSelected().toString()}\n onChange={table.getToggleAllPageRowsSelectedHandler()}\n />\n ),\n cell: ({ row }) => (\n <Checkbox\n checked={row.getIsSelected()}\n disabled={!row.getCanSelect()}\n // Use data-* attribute for indeterminate state\n data-indeterminate={row.getIsSomeSelected().toString()}\n onChange={row.getToggleSelectedHandler()}\n />\n ),\n enableResizing: false,\n enableSorting: false,\n size: 56\n })\n ]\n : []),\n ...( rawColumns\n ?.sort(( a: any, b: any ) => Number( b?.fixed ?? false ) - Number( a?.fixed ?? false ))\n ?.filter( el => el?.selected === undefined || el?.selected !== false )?.map(( column: TColumn ) => {\n const { cell, key, header, enableResizing, enableSorting, sortingFn, size } = column;\n return columnHelper.accessor( key, {\n header: header,\n cell: ( info: CellContext<any, any> ) => {\n return <>{typeof cell === "function" ? cell( info ) : info.getValue()}</>;\n },\n enableResizing: enableResizing ?? true,\n enableSorting: enableSorting ?? true,\n ...( sortingFn ? { sortingFn } : {}),\n size: size ? size : 400\n });\n }) ?? [])\n ];\n\n return derivedColumns;\n};\n\nconst DEBOUNCE_TIME = 500;\nconst HEIGHT = "500px";\n// Default page size options (maintained for backward compatibility but currently unused)\n\nconst PAGE_SIZE_OPTIONS = [ 25, 50, 100, 200, 500, 1000 ];\n\ninterface ISearchProps {\n placeholder?: string;\n hidden?: boolean;\n onChange?: ( e: React.ChangeEvent<HTMLInputElement> ) => void;\n value?: string;\n /**\n * Props to control the onSearch function debounceTime\n */\n debounceTime?: number;\n /**\n * Attach the debounced search function\n */\n onSearch?: ( value: string ) => void;\n}\n\ninterface IPaginationProps {\n onPageSizeChange?: ( args: number ) => void;\n onPageChange?: ( args: number ) => void;\n totalRecords?: number;\n offset?: number;\n pageSize?: number;\n}\n\nexport interface IDataTableProps<T> {\n /**\n * Defines the column structure for the Data Table, including column headers,\n * accessors, and any additional configurations.\n */\n columns: TColumn[];\n /**\n * The array of data objects that will populate the rows of the Data Table.\n */\n data: T[];\n /**\n * Optional: Provide custom class names as a space-separated\n * string to override the default Data Table styling.\n */\n className?: string;\n /**\n * Configuration for applying advanced filters to the Data Table.\n * Allows complex filtering logic and customization.\n */\n advancedFilters?: TAdvancedFilterItem[];\n /**\n * Configuration for applying basic filters to the Data Table.\n * Enables simple filtering by key-value pairs.\n * Showing maximum 3 basic filters in the table.\n */\n basicFilters?: TFilterItem[];\n /**\n * Allows to set filters by key-value pairs.\n */\n filters?: TFilters;\n /**\n * A callback function used to update the list of applied filters.\n */\n setFilters?: ( filters: TFilters ) => void;\n /**\n * Boolean value to display a loading state in the Data Table,\n * typically while loading or processing data.\n * @default false\n */\n loading?: boolean;\n /**\n * A callback function triggered to refresh the Data Table,\n * typically re-fetching or updating the data.\n */\n onRefresh?: () => void;\n /**\n * To adjust the width of the columns\n * @default false\n */\n resizable?: boolean;\n /**\n * To adding virtualization for column in the data table\n * @default true\n */\n columnVirtualization?: boolean;\n /**\n * To adding virtualization for row in the data table\n * @default true\n */\n rowVirtualization?: boolean;\n /**\n * To pin the first columns of the data table\n * @default true\n */\n pinnedFirstColumn?: boolean;\n /**\n * To make the row selectable by showing checkboxes\n * @default false\n */\n selectable?: boolean;\n /**\n * Callback function triggered on row selection.\n * @param selectedRows - Index List of the selected rows\n */\n onRowSelection?: ( selectedRows: object ) => void;\n /**\n * Allows to set sort by using key-value pair.\n */\n sorting?: SortingState;\n /**\n * Callback function triggered on row sorting.\n * @param sortState - Index List of the selected rows\n */\n setSorting?: OnChangeFn<SortingState> | undefined;\n /**\n * Allows to set pagination props.\n */\n paginationProps?: IPaginationProps;\n /**\n * To Adjust the height of the table.\n * @default 100%\n */\n height?: string;\n /**\n * To set the page size option\n * @default PAGE_SIZE_OPTIONS\n */\n pageSizeOptions?: number[];\n /**\n * To add the custom CTAs\n */\n customCTAs?: ReactNode;\n /**\n * Callback function triggered on change of Column Preferences.\n * @param columns - columns array with updated order and selected key value\n */\n setColumns: ( columns: TColumn[]) => void;\n /**\n * Allows to set search textfield props.\n */\n searchProps?: ISearchProps;\n}\n\n/**\n * DataTable is a powerful component that displays tabular data with advanced features\n * like filtering, sorting, pagination, and row selection.\n */\nexport const DataTable = <T extends object>({\n advancedFilters,\n basicFilters,\n className,\n columns,\n data = [],\n filters,\n loading = false,\n paginationProps,\n resizable = false,\n selectable = false,\n /** pin first column on left */\n pinnedFirstColumn = true,\n /** enable row virtualization */\n rowVirtualization = true,\n /** enable column virtualization */\n columnVirtualization = true,\n sorting,\n height = HEIGHT,\n customCTAs,\n searchProps,\n pageSizeOptions = PAGE_SIZE_OPTIONS,\n setFilters,\n setSorting,\n onRefresh,\n onRowSelection,\n setColumns\n}: IDataTableProps<T> ): ReactNode => {\n const columnHelper: ColumnHelper<T> = createColumnHelper<T>();\n const [ configuredColumns, setConfiguredColumns ] = useState(\n deriveConfiguredColumns(\n columns,\n selectable,\n columnHelper\n )\n );\n const [ globalFilter, setGlobalFilter ] = useState<string>( "" );\n const [ rowSelection, setRowSelection ] = useState<RowSelectionState>({});\n const [ selectedFilters, setSelectedFilters ] = useState( filters || {});\n const [ columnPinning, setColumnPinning ] = useState<ColumnPinningState>({\n left: pinnedFirstColumn ? [ "select", ( configuredColumns?.[1] as any )?.accessorKey ?? "" ] : []\n });\n\n const { totalRecords, offset, pageSize, onPageChange, onPageSizeChange } = paginationProps ?? {};\n const [ showRecordsChangeModal, setShowRecordsChangeModal ] = useState( false );\n const tableContainerRef = useRef<HTMLDivElement>( null );\n const [ isRtl, setIsRtl ] = useState( false );\n const [ showSettingsDropdown, setShowSettingsDropdown ] = useState( false );\n\n const totalPages = totalRecords && pageSize && Math.ceil( totalRecords / pageSize );\n\n const table: TReactTable<T> = useReactTable({\n data,\n columns: configuredColumns,\n state: {\n rowSelection,\n globalFilter,\n sorting,\n columnPinning\n },\n globalFilterFn: "includesString",\n columnResizeMode: "onChange",\n columnResizeDirection: "ltr",\n enableRowSelection: selectable,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: getFilteredRowModel() ?? undefined,\n onRowSelectionChange: setRowSelection ?? undefined,\n onGlobalFilterChange: setGlobalFilter ?? undefined,\n onSortingChange: setSorting ?? undefined,\n getSortedRowModel: sorting ? getSortedRowModel() : undefined,\n onColumnPinningChange: setColumnPinning ?? undefined\n });\n\n // For handling basic filter\n const handleBasicFilterChange = ( e: any, id: string ) => {\n setSelectedFilters(( prev: any ) => ({\n ...prev,\n [id]: e\n }));\n basicFilters?.find(( el: any ) => el.id === id )?.onChange?.();\n };\n\n // Clear advanced filters -\n const handleClearFilter = () => {\n const newSelectedFilters = Object.keys( selectedFilters ).reduce(( acc: any, key ) => {\n if ( basicFilters?.some(( filter: any ) => filter.id === key )) {\n acc[key] = selectedFilters[key];\n }\n return acc;\n }, {});\n\n setSelectedFilters( newSelectedFilters );\n advancedFilters?.forEach(( filter ) => {\n if ( filter?.setSelectedValue ) {\n filter?.setSelectedValue( null );\n }\n });\n };\n\n // Remove filter from selected filter list -\n const handleRemoveFilter = ( key: string, index?: number ) => {\n setSelectedFilters(( prevFilters: any ) => {\n const updatedFilters = { ...prevFilters };\n const value = updatedFilters[key];\n\n if ( Array.isArray( value ) && typeof index === "number" ) {\n updatedFilters[key] = value.filter(( _, i ) => i !== index );\n if ( updatedFilters[key].length === 0 ) {\n delete updatedFilters[key];\n }\n } else {\n delete updatedFilters[key];\n }\n return updatedFilters;\n });\n\n advancedFilters?.forEach(( filter ) => {\n if ( filter?.id === key && filter?.setSelectedValue ) {\n if ( Array.isArray( selectedFilters[key]) && typeof index === "number" ) {\n const arr = [...( selectedFilters[key] as any[])]; arr.splice( index, 1 );\n filter.setSelectedValue( arr.length ? arr : null );\n } else {\n filter.setSelectedValue( null );\n }\n }\n });\n };\n\n const handleClearAllSelectedFilters = () => {\n setSelectedFilters({});\n advancedFilters?.forEach(( filter ) => {\n if ( filter?.setSelectedValue ) {\n filter?.setSelectedValue( null );\n }\n });\n };\n\n const toggleRecordsModal = () => {\n setShowRecordsChangeModal( !showRecordsChangeModal );\n };\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const onSearchInput = useCallback( debounce(( value ) => {\n searchProps?.onSearch?.( value );\n }, searchProps?.debounceTime ?? DEBOUNCE_TIME ), []);\n\n const onSearchTextChange = ( e: React.ChangeEvent<HTMLInputElement> ) => {\n if ( searchProps?.onChange && typeof searchProps?.onChange === "function" ) {\n searchProps?.onChange( e );\n }\n\n if ( typeof searchProps?.onSearch === "undefined" ) {\n setGlobalFilter( e.target.value );\n } else {\n onSearchInput( e.target.value );\n }\n };\n\n // Filter Bar of the Data Table\n const FilterBar = () => {\n const { value = "", placeholder = "Search in this page", hidden } = searchProps || {};\n return (\n <div className={cn( dataTableFiltersVariants())}>\n {!hidden && (\n <TextField\n placeholder={placeholder}\n prefixIcon={iconList.search}\n className={cn( dataTableFilterSearchVariants())}\n onChange={onSearchTextChange}\n value={value ? value : globalFilter}\n suffixIcon={\n globalFilter || value ? (\n <ADPIcon\n className="cursor-pointer"\n icon={iconList.cross}\n onClick={() => {\n if ( searchProps?.onSearch ) {\n searchProps.onSearch( "" );\n } else {\n setGlobalFilter( "" );\n }\n }}\n />\n ) : undefined\n }\n />\n )}\n {basicFilters?.slice( 0, 3 )?.map(( props: TFilterItem ) => {\n return (\n <Select\n {...props}\n options={props?.options}\n data-testid={`table-filter-${props.id}`}\n key={props.id}\n className={cn( dataTableFilterSelectVariants())}\n onChange={( e ) => handleBasicFilterChange( e, props.id ?? "" )}\n value={selectedFilters[props.id ?? ""] || null}\n prefixIcon={props.icon ? <ADPIcon className="text-gray-800" icon={props.icon} /> : undefined}\n placeholder={\n <div className="flex gap-2 items-center">\n <span className="text-secondary-500 dark:text-secondary-400 font-medium text-xs">\n {props?.placeholder || props?.title}\n </span>\n </div>\n }\n />\n );\n })}\n {advancedFilters && advancedFilters.length > 0 && (\n <AdvancedFilters\n advancedFilters={advancedFilters}\n basicFilters={basicFilters}\n selectedFilters={selectedFilters}\n setSelectedFilters={setSelectedFilters}\n handleClearFilter={handleClearFilter}\n />\n )}\n <div className="flex items-center gap-4 ms-auto">\n {typeof onRefresh === "function" && (\n <Button\n prefixIcon={iconList.reload}\n size="xs"\n variant="outline"\n className="border-primary-500"\n onClick={onRefresh}\n />\n )}\n {columns && (\n <div className="relative">\n <Button\n prefixIcon={iconList.settings}\n size="xs"\n variant="outline"\n className="border-primary-500"\n onClick={() => setShowSettingsDropdown( !showSettingsDropdown )}\n />\n <DataTableColumnSettings\n columns={columns}\n setColumns={setColumns}\n isOpen={showSettingsDropdown}\n onClose={() => setShowSettingsDropdown( false )}\n />\n </div>\n )}\n </div>\n {customCTAs}\n </div>\n );\n };\n\n useEffect(() => {\n setConfiguredColumns( deriveConfiguredColumns( columns, selectable, columnHelper ));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [columns]);\n\n // Return the selected rows\n useEffect(() => {\n if ( rowSelection ) {\n onRowSelection?.( rowSelection );\n }\n }, [ onRowSelection, rowSelection ]);\n\n useEffect(() => {\n setFilters?.( selectedFilters );\n }, [ selectedFilters, setFilters ]);\n\n useEffect(() => {\n setColumnPinning({\n left: pinnedFirstColumn ? [ "select", ( configuredColumns?.[selectable ? 1 : 0] as any )?.accessorKey ?? "" ] : []\n });\n }, [ configuredColumns, selectable, pinnedFirstColumn ]);\n\n useEffect(() => {\n const tableContainer = tableContainerRef.current;\n if ( tableContainer ) {\n const dir = findParentAttribute( tableContainer, "dir" );\n setIsRtl( dir === "rtl" );\n }\n }, [tableContainerRef]);\n\n return (\n <div\n data-testid="table"\n className={cn( dataTableContainerVariants(), className )}\n ref={tableContainerRef}\n >\n {FilterBar()}\n <SelectedFilterList\n advancedFilters={advancedFilters ?? []}\n basicFilters={basicFilters ?? []}\n selectedFilters={selectedFilters}\n handleRemoveFilter={handleRemoveFilter}\n handleClearAllSelectedFilters={handleClearAllSelectedFilters}\n />\n <div className={cn( dataTableRecordsInfoVariants())}>\n {totalRecords && offset && pageSize && !loading ? (\n <>\n <span className="text-secondary-400 dark:text-secondary-200">\n Showing {offset} - {Math.min( offset - 1 + pageSize, totalRecords )} of {totalRecords} results.{" "}\n </span>\n <Button size="sm" variant="text" onClick={toggleRecordsModal} className="inline">\n See more\n </Button>\n </>\n ) : <></>}\n </div>\n <div\n className={cn( dataTableContainerInnerVariants())}\n style={{ height }}\n >\n <Table<T>\n table={table}\n columnVirtualization={columnVirtualization}\n rowVirtualization={rowVirtualization}\n selectable={selectable}\n loading={loading}\n resizable={resizable}\n height={height ?? HEIGHT}\n isRtl={isRtl}\n />\n </div>\n {Boolean( totalPages ) && <div className={cn( dataTablePaginationVariants())}>\n <Pagination\n layout="detailed"\n variant="outline"\n totalPages={totalPages}\n onPageChange={onPageChange}\n activePageNo={offset && Math.ceil( offset / ( pageSize ?? 25 ))}\n />\n </div>\n }\n\n {/* Page Size Modal */}\n <PageSizeModal\n show={showRecordsChangeModal}\n toggleVisibility={toggleRecordsModal}\n pageSize={pageSize || 25}\n pageSizeOptions={pageSizeOptions}\n onPageSizeChange={onPageSizeChange}\n onPageChange={onPageChange}\n />\n </div>\n );\n};\n'
|
|
1808
|
+
"content": 'import * as React from "react";\nimport { type ReactNode, useCallback, useEffect, useRef, useState } from "react";\nimport {\n type CellContext,\n createColumnHelper,\n getCoreRowModel,\n getFilteredRowModel,\n getSortedRowModel,\n useReactTable,\n type SortingState,\n type ColumnHelper,\n type ColumnPinningState,\n type Table as TReactTable,\n type OnChangeFn,\n type RowSelectionState\n} from "@tanstack/react-table";\n\nimport { Button } from "../button/button";\nimport { TextField } from "../textField/textField";\nimport { Select } from "../select/select";\nimport { ADPIcon, iconList } from "../adpIcon/adpIcon";\nimport { Pagination } from "../pagination/pagination";\nimport { Checkbox } from "../checkbox/checkbox";\nimport { debounce, findParentAttribute } from "@utils";\nimport { dataTableVariants } from "./dataTableVariants";\nimport { PageSizeModal } from "./pageSizeModal";\nimport { DataTableColumnSettings } from "./DataTableColumnSettings";\nimport { AdvancedFilters } from "./advancedFilters";\nimport SelectedFilterList from "./selectedFilterList";\nimport { Table } from "./table";\nimport type { TColumn, TFilterItem, TAdvancedFilterItem, TFilters } from "./types";\n\n// Utility function to derive configured columns\nconst deriveConfiguredColumns = (\n rawColumns: TColumn[],\n selectable: boolean,\n columnHelper: ColumnHelper<any>\n) => {\n const derivedColumns = [\n ...( selectable\n ? [\n columnHelper.accessor( "select", {\n id: "select",\n header: ({ table }) => (\n <Checkbox\n checked={table.getIsAllRowsSelected()}\n // Use data-* attribute for indeterminate state\n data-indeterminate={table.getIsSomeRowsSelected().toString()}\n onChange={table.getToggleAllPageRowsSelectedHandler()}\n />\n ),\n cell: ({ row }) => (\n <Checkbox\n checked={row.getIsSelected()}\n disabled={!row.getCanSelect()}\n // Use data-* attribute for indeterminate state\n data-indeterminate={row.getIsSomeSelected().toString()}\n onChange={row.getToggleSelectedHandler()}\n />\n ),\n enableResizing: false,\n enableSorting: false,\n size: 56\n })\n ]\n : []),\n ...( rawColumns\n ?.sort(( a: any, b: any ) => Number( b?.fixed ?? false ) - Number( a?.fixed ?? false ))\n ?.filter( el => el?.selected === undefined || el?.selected !== false )?.map(( column: TColumn ) => {\n const { cell, key, header, enableResizing, enableSorting, sortingFn, size } = column;\n return columnHelper.accessor( key, {\n header: header,\n cell: ( info: CellContext<any, any> ) => {\n return <>{typeof cell === "function" ? cell( info ) : info.getValue()}</>;\n },\n enableResizing: enableResizing ?? true,\n enableSorting: enableSorting ?? true,\n ...( sortingFn ? { sortingFn } : {}),\n size: size ? size : 400\n });\n }) ?? [])\n ];\n\n return derivedColumns;\n};\n\nconst DEBOUNCE_TIME = 500;\nconst HEIGHT = "500px";\n// Default page size options (maintained for backward compatibility but currently unused)\n\nconst PAGE_SIZE_OPTIONS = [ 25, 50, 100, 200, 500, 1000 ];\n\ninterface ISearchProps {\n placeholder?: string;\n hidden?: boolean;\n onChange?: ( e: React.ChangeEvent<HTMLInputElement> ) => void;\n value?: string;\n /**\n * Props to control the onSearch function debounceTime\n */\n debounceTime?: number;\n /**\n * Attach the debounced search function\n */\n onSearch?: ( value: string ) => void;\n}\n\ninterface IPaginationProps {\n onPageSizeChange?: ( args: number ) => void;\n onPageChange?: ( args: number ) => void;\n totalRecords?: number;\n offset?: number;\n pageSize?: number;\n}\n\nexport interface IDataTableProps<T> {\n /**\n * Defines the column structure for the Data Table, including column headers,\n * accessors, and any additional configurations.\n */\n columns: TColumn[];\n /**\n * The array of data objects that will populate the rows of the Data Table.\n */\n data: T[];\n /**\n * Optional: Provide custom class names as a space-separated\n * string to override the default Data Table styling.\n */\n className?: string;\n /**\n * Configuration for applying advanced filters to the Data Table.\n * Allows complex filtering logic and customization.\n */\n advancedFilters?: TAdvancedFilterItem[];\n /**\n * Configuration for applying basic filters to the Data Table.\n * Enables simple filtering by key-value pairs.\n * Showing maximum 3 basic filters in the table.\n */\n basicFilters?: TFilterItem[];\n /**\n * Allows to set filters by key-value pairs.\n */\n filters?: TFilters;\n /**\n * A callback function used to update the list of applied filters.\n */\n setFilters?: ( filters: TFilters ) => void;\n /**\n * Boolean value to display a loading state in the Data Table,\n * typically while loading or processing data.\n * @default false\n */\n loading?: boolean;\n /**\n * A callback function triggered to refresh the Data Table,\n * typically re-fetching or updating the data.\n */\n onRefresh?: () => void;\n /**\n * To adjust the width of the columns\n * @default false\n */\n resizable?: boolean;\n /**\n * To adding virtualization for column in the data table\n * @default true\n */\n columnVirtualization?: boolean;\n /**\n * To adding virtualization for row in the data table\n * @default true\n */\n rowVirtualization?: boolean;\n /**\n * To pin the first columns of the data table\n * @default true\n */\n pinnedFirstColumn?: boolean;\n /**\n * To make the row selectable by showing checkboxes\n * @default false\n */\n selectable?: boolean;\n /**\n * Callback function triggered on row selection.\n * @param selectedRows - Index List of the selected rows\n */\n onRowSelection?: ( selectedRows: object ) => void;\n /**\n * Allows to set sort by using key-value pair.\n */\n sorting?: SortingState;\n /**\n * Callback function triggered on row sorting.\n * @param sortState - Index List of the selected rows\n */\n setSorting?: OnChangeFn<SortingState> | undefined;\n /**\n * Allows to set pagination props.\n */\n paginationProps?: IPaginationProps;\n /**\n * To Adjust the height of the table.\n * @default 100%\n */\n height?: string;\n /**\n * To set the page size option\n * @default PAGE_SIZE_OPTIONS\n */\n pageSizeOptions?: number[];\n /**\n * To add the custom CTAs\n */\n customCTAs?: ReactNode;\n /**\n * Callback function triggered on change of Column Preferences.\n * @param columns - columns array with updated order and selected key value\n */\n setColumns: ( columns: TColumn[]) => void;\n /**\n * Allows to set search textfield props.\n */\n searchProps?: ISearchProps;\n}\n\n/**\n * DataTable is a powerful component that displays tabular data with advanced features\n * like filtering, sorting, pagination, and row selection.\n */\nexport const DataTable = <T extends object>({\n advancedFilters,\n basicFilters,\n className,\n columns,\n data = [],\n filters,\n loading = false,\n paginationProps,\n resizable = false,\n selectable = false,\n /** pin first column on left */\n pinnedFirstColumn = true,\n /** enable row virtualization */\n rowVirtualization = true,\n /** enable column virtualization */\n columnVirtualization = true,\n sorting,\n height = HEIGHT,\n customCTAs,\n searchProps,\n pageSizeOptions = PAGE_SIZE_OPTIONS,\n setFilters,\n setSorting,\n onRefresh,\n onRowSelection,\n setColumns\n}: IDataTableProps<T> ): ReactNode => {\n const columnHelper: ColumnHelper<T> = createColumnHelper<T>();\n const [ configuredColumns, setConfiguredColumns ] = useState(\n deriveConfiguredColumns(\n columns,\n selectable,\n columnHelper\n )\n );\n const [ globalFilter, setGlobalFilter ] = useState<string>( "" );\n const [ rowSelection, setRowSelection ] = useState<RowSelectionState>({});\n const [ selectedFilters, setSelectedFilters ] = useState( filters || {});\n const [ columnPinning, setColumnPinning ] = useState<ColumnPinningState>({\n left: pinnedFirstColumn ? [ "select", ( configuredColumns?.[1] as any )?.accessorKey ?? "" ] : []\n });\n\n const { totalRecords, offset, pageSize, onPageChange, onPageSizeChange } = paginationProps ?? {};\n const [ showRecordsChangeModal, setShowRecordsChangeModal ] = useState( false );\n const tableContainerRef = useRef<HTMLDivElement>( null );\n const [ isRtl, setIsRtl ] = useState( false );\n const [ showSettingsDropdown, setShowSettingsDropdown ] = useState( false );\n\n const totalPages = totalRecords && pageSize && Math.ceil( totalRecords / pageSize );\n\n const table: TReactTable<T> = useReactTable({\n data,\n columns: configuredColumns,\n state: {\n rowSelection,\n globalFilter,\n sorting,\n columnPinning\n },\n globalFilterFn: "includesString",\n columnResizeMode: "onChange",\n columnResizeDirection: "ltr",\n enableRowSelection: selectable,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: getFilteredRowModel() ?? undefined,\n onRowSelectionChange: setRowSelection ?? undefined,\n onGlobalFilterChange: setGlobalFilter ?? undefined,\n onSortingChange: setSorting ?? undefined,\n getSortedRowModel: sorting ? getSortedRowModel() : undefined,\n onColumnPinningChange: setColumnPinning ?? undefined\n });\n\n // For handling basic filter\n const handleBasicFilterChange = ( e: any, id: string ) => {\n setSelectedFilters(( prev: any ) => ({\n ...prev,\n [id]: e\n }));\n basicFilters?.find(( el: any ) => el.id === id )?.onChange?.();\n };\n\n // Clear advanced filters -\n const handleClearFilter = () => {\n const newSelectedFilters = Object.keys( selectedFilters ).reduce(( acc: any, key ) => {\n if ( basicFilters?.some(( filter: any ) => filter.id === key )) {\n acc[key] = selectedFilters[key];\n }\n return acc;\n }, {});\n\n setSelectedFilters( newSelectedFilters );\n advancedFilters?.forEach(( filter ) => {\n if ( filter?.setSelectedValue ) {\n filter?.setSelectedValue( null );\n }\n });\n };\n\n // Remove filter from selected filter list -\n const handleRemoveFilter = ( key: string, index?: number ) => {\n setSelectedFilters(( prevFilters: any ) => {\n const updatedFilters = { ...prevFilters };\n const value = updatedFilters[key];\n\n if ( Array.isArray( value ) && typeof index === "number" ) {\n updatedFilters[key] = value.filter(( _, i ) => i !== index );\n if ( updatedFilters[key].length === 0 ) {\n delete updatedFilters[key];\n }\n } else {\n delete updatedFilters[key];\n }\n return updatedFilters;\n });\n\n advancedFilters?.forEach(( filter ) => {\n if ( filter?.id === key && filter?.setSelectedValue ) {\n if ( Array.isArray( selectedFilters[key]) && typeof index === "number" ) {\n const arr = [...( selectedFilters[key] as any[])]; arr.splice( index, 1 );\n filter.setSelectedValue( arr.length ? arr : null );\n } else {\n filter.setSelectedValue( null );\n }\n }\n });\n };\n\n const handleClearAllSelectedFilters = () => {\n setSelectedFilters({});\n advancedFilters?.forEach(( filter ) => {\n if ( filter?.setSelectedValue ) {\n filter?.setSelectedValue( null );\n }\n });\n };\n\n const toggleRecordsModal = () => {\n setShowRecordsChangeModal( !showRecordsChangeModal );\n };\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const onSearchInput = useCallback( debounce(( value ) => {\n searchProps?.onSearch?.( value );\n }, searchProps?.debounceTime ?? DEBOUNCE_TIME ), []);\n\n const onSearchTextChange = ( e: React.ChangeEvent<HTMLInputElement> ) => {\n if ( searchProps?.onChange && typeof searchProps?.onChange === "function" ) {\n searchProps?.onChange( e );\n }\n\n if ( typeof searchProps?.onSearch === "undefined" ) {\n setGlobalFilter( e.target.value );\n } else {\n onSearchInput( e.target.value );\n }\n };\n\n const {\n container,\n filters: filtersClass,\n filterSearch,\n filterSelect,\n recordsInfo,\n containerInner,\n pagination: paginationClass\n } = dataTableVariants();\n\n // Filter Bar of the Data Table\n const FilterBar = () => {\n const { value = "", placeholder = "Search in this page", hidden } = searchProps || {};\n return (\n <div className={filtersClass()}>\n {!hidden && (\n <TextField\n placeholder={placeholder}\n prefixIcon={iconList.search}\n className={filterSearch()}\n onChange={onSearchTextChange}\n value={value ? value : globalFilter}\n suffixIcon={\n globalFilter || value ? (\n <ADPIcon\n className="cursor-pointer"\n icon={iconList.cross}\n onClick={() => {\n if ( searchProps?.onSearch ) {\n searchProps.onSearch( "" );\n } else {\n setGlobalFilter( "" );\n }\n }}\n />\n ) : undefined\n }\n />\n )}\n {basicFilters?.slice( 0, 3 )?.map(( props: TFilterItem ) => {\n return (\n <Select\n {...props}\n options={props?.options}\n data-testid={`table-filter-${props.id}`}\n key={props.id}\n className={filterSelect()}\n onChange={( e ) => handleBasicFilterChange( e, props.id ?? "" )}\n value={selectedFilters[props.id ?? ""] || null}\n prefixIcon={props.icon ? <ADPIcon className="text-gray-800" icon={props.icon} /> : undefined}\n placeholder={\n <div className="flex gap-2 items-center">\n <span className="text-secondary-500 dark:text-secondary-400 font-medium text-xs">\n {props?.placeholder || props?.title}\n </span>\n </div>\n }\n />\n );\n })}\n {advancedFilters && advancedFilters.length > 0 && (\n <AdvancedFilters\n advancedFilters={advancedFilters}\n basicFilters={basicFilters}\n selectedFilters={selectedFilters}\n setSelectedFilters={setSelectedFilters}\n handleClearFilter={handleClearFilter}\n />\n )}\n <div className="flex items-center gap-4 ms-auto">\n {typeof onRefresh === "function" && (\n <Button\n prefixIcon={iconList.reload}\n size="xs"\n variant="outline"\n className="border-primary-500"\n onClick={onRefresh}\n />\n )}\n {columns && (\n <div className="relative">\n <Button\n prefixIcon={iconList.settings}\n size="xs"\n variant="outline"\n className="border-primary-500"\n onClick={() => setShowSettingsDropdown( !showSettingsDropdown )}\n />\n <DataTableColumnSettings\n columns={columns}\n setColumns={setColumns}\n isOpen={showSettingsDropdown}\n onClose={() => setShowSettingsDropdown( false )}\n />\n </div>\n )}\n </div>\n {customCTAs}\n </div>\n );\n };\n\n useEffect(() => {\n setConfiguredColumns( deriveConfiguredColumns( columns, selectable, columnHelper ));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [columns]);\n\n // Return the selected rows\n useEffect(() => {\n if ( rowSelection ) {\n onRowSelection?.( rowSelection );\n }\n }, [ onRowSelection, rowSelection ]);\n\n useEffect(() => {\n setFilters?.( selectedFilters );\n }, [ selectedFilters, setFilters ]);\n\n useEffect(() => {\n setColumnPinning({\n left: pinnedFirstColumn ? [ "select", ( configuredColumns?.[selectable ? 1 : 0] as any )?.accessorKey ?? "" ] : []\n });\n }, [ configuredColumns, selectable, pinnedFirstColumn ]);\n\n useEffect(() => {\n const tableContainer = tableContainerRef.current;\n if ( tableContainer ) {\n const dir = findParentAttribute( tableContainer, "dir" );\n setIsRtl( dir === "rtl" );\n }\n }, [tableContainerRef]);\n\n return (\n <div\n data-testid="table"\n className={container({ class: className })}\n ref={tableContainerRef}\n >\n {FilterBar()}\n <SelectedFilterList\n advancedFilters={advancedFilters ?? []}\n basicFilters={basicFilters ?? []}\n selectedFilters={selectedFilters}\n handleRemoveFilter={handleRemoveFilter}\n handleClearAllSelectedFilters={handleClearAllSelectedFilters}\n />\n <div className={recordsInfo()}>\n {totalRecords && offset && pageSize && !loading ? (\n <>\n <span className="text-secondary-400 dark:text-secondary-200">\n Showing {offset} - {Math.min( offset - 1 + pageSize, totalRecords )} of {totalRecords} results.{" "}\n </span>\n <Button size="sm" variant="text" onClick={toggleRecordsModal} className="inline">\n See more\n </Button>\n </>\n ) : <></>}\n </div>\n <div\n className={containerInner()}\n style={{ height }}\n >\n <Table<T>\n table={table}\n columnVirtualization={columnVirtualization}\n rowVirtualization={rowVirtualization}\n selectable={selectable}\n loading={loading}\n resizable={resizable}\n height={height ?? HEIGHT}\n isRtl={isRtl}\n />\n </div>\n {Boolean( totalPages ) && <div className={paginationClass()}>\n <Pagination\n layout="detailed"\n variant="outline"\n totalPages={totalPages}\n onPageChange={onPageChange}\n activePageNo={offset && Math.ceil( offset / ( pageSize ?? 25 ))}\n />\n </div>\n }\n\n {/* Page Size Modal */}\n <PageSizeModal\n show={showRecordsChangeModal}\n toggleVisibility={toggleRecordsModal}\n pageSize={pageSize || 25}\n pageSizeOptions={pageSizeOptions}\n onPageSizeChange={onPageSizeChange}\n onPageChange={onPageChange}\n />\n </div>\n );\n};\n'
|
|
1837
1809
|
},
|
|
1838
1810
|
{
|
|
1839
1811
|
"name": "advancedFilters.tsx",
|
|
1840
|
-
"content": 'import { useEffect, useMemo, useState } from "react";\
|
|
1812
|
+
"content": 'import { useEffect, useMemo, useState } from "react";\n\nimport { ADPIcon, iconList } from "../adpIcon/adpIcon";\nimport { Button } from "../button/button";\nimport { Checkbox } from "../checkbox/checkbox";\nimport { Radio } from "../radio/radio";\nimport { Panel } from "../panel/panel";\nimport { Accordion } from "../accordion/accordion";\nimport { dataTableVariants } from "./dataTableVariants";\n\nimport type { TAdvancedFilterItem, TFilterItem } from "./types";\n\ninterface IAdvancedFilterProps {\n advancedFilters?: TAdvancedFilterItem[];\n basicFilters?: TFilterItem[];\n selectedFilters: Record<string, any>;\n setSelectedFilters: ( filters: Record<string, any> ) => void;\n handleClearFilter: () => void;\n}\n\nexport const AdvancedFilters = ({\n advancedFilters,\n basicFilters,\n selectedFilters,\n setSelectedFilters,\n handleClearFilter\n}: IAdvancedFilterProps ) => {\n const [ selectedAdvancedFilters, setSelectedAdvancedFilters ] = useState( selectedFilters );\n const [ showPanel, setShowPanel ] = useState<boolean>( false );\n\n // For handling advance filter-\n const handleAdvanceFilterChange = ( e: any, id: any ) => {\n if ( e.target.type === "checkbox" ) {\n const newFilterValue = { value: e.target.value, label: e.target.name };\n setSelectedAdvancedFilters(( prev:any ) => {\n const updatedVal = { ...prev };\n const currentSelectedValues = updatedVal[id] || [];\n\n if ( e.target.checked ) {\n updatedVal[id] = [ ...currentSelectedValues, newFilterValue ];\n } else {\n updatedVal[id] = currentSelectedValues.filter(\n ( item: any ) => item.label !== e.target.name\n );\n if ( updatedVal[id].length === 0 ) {\n delete updatedVal[id];\n }\n }\n return updatedVal;\n });\n } else if ( e.target.type === "radio" ) {\n setSelectedAdvancedFilters(( prev:any ) => ({\n ...prev,\n [id]: { value: e.target.value, label: e.target.name }\n }));\n }\n advancedFilters?.find(( el: any ) => el.id === id )?.onChange?.();\n };\n\n const customSelectedFilter = useMemo(() => {\n return advancedFilters?.filter(( filter ) => "selectedValue" in filter );\n }, [advancedFilters]);\n\n const handleApplyFilter = () => {\n let updatedFilters = {\n ...selectedFilters,\n ...selectedAdvancedFilters\n };\n advancedFilters?.forEach(( filter ) => {\n if ( filter?.selectedValue ) {\n updatedFilters = {\n ...updatedFilters,\n ...filter.selectedValue\n };\n }\n });\n setSelectedFilters( updatedFilters );\n setShowPanel( false );\n };\n\n const basicFilterIds = basicFilters?.map(( filter: any ) => filter.id );\n const advanceFilterSelectedLength = useMemo(() => {\n return Object.keys( selectedAdvancedFilters ).filter(( key ) => !basicFilterIds?.includes( key )).length;\n }, [ basicFilterIds, selectedAdvancedFilters ]);\n\n useEffect(() => {\n setSelectedAdvancedFilters( selectedFilters );\n }, [selectedFilters]);\n\n useEffect(() => {\n let updatedFilters = {\n ...selectedAdvancedFilters\n };\n customSelectedFilter?.forEach(( filter ) => {\n if ( filter?.selectedValue ) {\n updatedFilters = {\n ...updatedFilters,\n ...filter.selectedValue\n };\n } else {\n Object.keys( updatedFilters ).forEach(( key ) => {\n if ( key === filter.id ) {\n delete updatedFilters[key];\n }\n });\n }\n });\n setSelectedAdvancedFilters( updatedFilters );\n // Adding selectedAdvancedFilters to the dependency array causes an infinite re-render\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [customSelectedFilter]);\n\n if ( !advancedFilters?.length ) {\n return null;\n }\n\n const { advancedFilterButton } = dataTableVariants();\n\n return (\n <>\n <Button\n onClick={() => setShowPanel( true )}\n prefixIcon={iconList.filter}\n size="xs"\n variant="text"\n color="primary"\n data-testid="advanced-filter-btn"\n className={advancedFilterButton({ class: "min-w-fit" })}\n >\n Advanced Filters\n </Button>\n <Panel showPanel={showPanel} className="table-filter" data-testid={"table-filter-panel"}>\n <Panel.Header\n showCloseButton\n header="Filters"\n subHeader="Apply filters to narrow down your search results"\n onCloseHandler={() => setShowPanel( false )}\n />\n <Panel.Body className="bg-frostBlue dark:bg-iridium">\n {advancedFilters?.map(( props: any ) => {\n const selectedCount = selectedAdvancedFilters[props.id]?.length;\n return <Accordion key={props.id} className="mb-6" expand data-testid={`table-filter-${props.id}`}>\n <Accordion.Header className="">\n <div className="text-left">\n <p className="flex items-center text-sm leading-5 font-semibold text-secondary-500">\n <ADPIcon icon={props.icon} size="xs" className="!inline-block text-gray-800 me-1" />\n {props.title} {(( props.isMulti || props.selectedValue ) && ( selectedCount !== undefined && selectedCount > 0 )) &&\n <span className="text-success-700 dark:text-success-400 font-medium ms-4">{`${selectedCount} selected`}</span>}\n </p>\n <span className="text-xs leading-[130%] text-secondary-300">{props.subTitle}</span>\n </div>\n </Accordion.Header>\n <Accordion.Body className={props.render ? "!bg-transparent !p-0.5" : undefined}>\n <div className="flex flex-col gap-4">\n {props.isMulti && props?.options?.length > 0\n ? props.options.map(( option: any ) => (\n <Checkbox\n key={option.value}\n label={option.label}\n name={option.label}\n value={option.value}\n size="sm"\n onChange={( e ) => handleAdvanceFilterChange( e, props.id )}\n checked={selectedAdvancedFilters[props.id]?.findIndex(\n ( item: any ) => item.value === option.value\n ) > -1}\n />\n ))\n : !props.isMulti && props?.options?.length > 0\n ? props.options.map(( option: any ) => (\n <Radio\n key={option.value}\n label={option.label}\n name={option.label}\n value={option.value}\n onChange={( e ) => handleAdvanceFilterChange( e, props.id )}\n checked={selectedAdvancedFilters[props.id]?.value === option.value}\n />\n )) : typeof ( props?.render ) === "function" ? props?.render() : props?.render}\n </div>\n </Accordion.Body>\n </Accordion>;\n })}\n </Panel.Body>\n <Panel.Footer className="min-h-0 py-4">\n <div className="flex justify-end gap-4">\n <Button variant="outline" color="secondary" onClick ={() => {\n handleClearFilter();\n setShowPanel( false );\n }}>Clear Filters</Button>\n <Button\n onClick ={() => {\n handleApplyFilter();\n setShowPanel( false );\n }}\n disabled={advanceFilterSelectedLength === 0}\n >\n Apply {\n advanceFilterSelectedLength !== 0 && advanceFilterSelectedLength\n } Filters\n </Button>\n </div>\n </Panel.Footer>\n </Panel>\n </>\n );\n};\n'
|
|
1841
1813
|
},
|
|
1842
1814
|
{
|
|
1843
1815
|
"name": "README.md",
|
|
@@ -1845,7 +1817,7 @@ export const checkmarkClipPath = \`polygon(
|
|
|
1845
1817
|
},
|
|
1846
1818
|
{
|
|
1847
1819
|
"name": "DataTableColumnSettings.tsx",
|
|
1848
|
-
"content": 'import * as React from "react";\nimport { useEffect, useRef, useState } from "react";\nimport { cn } from "
|
|
1820
|
+
"content": 'import * as React from "react";\nimport { useEffect, useRef, useState } from "react";\nimport { cn } from "tailwind-variants";\n\nimport { ADPIcon, iconList } from "../adpIcon/adpIcon";\nimport { Button } from "../button/button";\nimport { Toggle } from "../toggle/toggle";\n\nimport type { TColumn } from "./types";\n\ntype TItemRef = { index: number };\n\nconst prepareData = ( columns: TColumn[], hideColumn = true ) => {\n return columns\n ?.filter(( field: TColumn ) => field?.id !== "select" && field?.hideColPreference !== hideColumn )\n ?.map(( item: TColumn ) => {\n const { header, selected, ...rest } = item;\n return {\n header: typeof header === "function" ? header()?.props?.children : header,\n selected: selected === undefined || selected !== false,\n ...rest\n };\n })?.sort(( a: any, b: any ) => Number( b?.fixed ?? false ) - Number( a?.fixed ?? false ));\n};\n\nexport const DataTableColumnSettings = ({\n columns,\n setColumns,\n isOpen,\n onClose\n}: {\n columns: TColumn[];\n setColumns: ( columns: TColumn[]) => void;\n isOpen: boolean;\n onClose: () => void;\n}) => {\n const [ settingItem, setSettingsItem ] = useState<TColumn[]>( prepareData( columns ));\n const dragItemRef = useRef<TItemRef>({ index: -1 });\n const dragOverItemRef = useRef<TItemRef>({ index: -1 });\n\n useEffect(() => {\n setSettingsItem( prepareData( columns ));\n }, [ columns, setSettingsItem ]);\n\n const handleDrop = ( event: React.DragEvent<HTMLElement> ) => {\n event.stopPropagation();\n const listCopy = structuredClone( settingItem );\n if ( listCopy[dragOverItemRef.current.index]?.fixed ) {\n return;\n }\n const requiredItem = listCopy[dragItemRef.current.index];\n listCopy.splice( dragOverItemRef.current.index, 0, requiredItem );\n const dragLocationIndex = dragItemRef.current.index >= dragOverItemRef.current.index\n ? dragItemRef.current.index + 1\n : dragItemRef.current.index;\n listCopy.splice( dragLocationIndex, 1 );\n setSettingsItem( listCopy );\n };\n\n const toggleItem = ( index: number ) => {\n const listCopy = settingItem.slice();\n listCopy[index].selected = !listCopy[index]?.selected;\n setSettingsItem( listCopy );\n };\n\n // Handle setting submission\n const handleSettingsSubmit = () => {\n setColumns([\n ...settingItem,\n ...prepareData(\n columns?.filter(( e ) => e.hideColPreference ),\n false\n )\n ]);\n onClose();\n };\n\n const closeSettings = () => {\n // Reset to initial state\n setSettingsItem( prepareData( columns ));\n onClose();\n };\n\n return (\n <div className={cn(\n "absolute top-9 w-[484px] shadow-md rounded px-4 pt-4 pb-6 bg-white z-20",\n "right-[-100vw] ease-in-out duration-500",\n { "right-0": isOpen },\n "dark:border dark:border-secondary-400 dark:bg-iridium"\n )}>\n {/* Header */}\n <div className="flex items-start justify-between mb-6">\n <div>\n <h1 className="text-md text-secondary-500 font-medium dark:text-secondary-50">\n Table Column Preferences\n </h1>\n <p className="text-xs text-secondary-400 dark:text-secondary-300">\n Customize the visibility and display order of columns\n </p>\n </div>\n <button\n onClick={closeSettings}\n className="text-secondary-500 hover:text-secondary-700 dark:text-secondary-400 dark:hover:text-secondary-200 focus:outline-none"\n >\n <ADPIcon size="md" icon={iconList.cross} />\n </button>\n </div>\n\n {/* Items List */}\n <div\n onDragEnter={( event ) => event.preventDefault()}\n onDragOver={( event ) => event.preventDefault()}\n onDrop={handleDrop}\n className="flex flex-col mb-6 max-h-[400px] overflow-y-auto gap-4"\n >\n {[ ...settingItem, { id: "-1", key: "", header: "" }]?.map(( column: TColumn, index: number ) => {\n return (\n <div\n key={index}\n draggable={column?.selected && !column.fixed}\n onDragStart={( event ) => {\n event.stopPropagation();\n dragItemRef.current.index = index;\n }}\n onDragEnter={( event ) => {\n event.stopPropagation();\n dragOverItemRef.current.index = index;\n }}\n onDragOver={( event ) => {\n if ( column.id !== "-1" ) {\n event.currentTarget.classList?.add( "border-2", "border-dashed", "border-primary-500" );\n }\n event.preventDefault();\n }}\n onDragLeave={( event ) => {\n event.currentTarget.classList?.remove( "border-2", "border-dashed", "border-primary-500" );\n }}\n onDrop={( event ) => {\n event.currentTarget.classList?.remove( "border-2", "border-dashed", "border-primary-500" );\n }}\n className={cn(\n "px-4 py-2.5 border border-secondary-100 rounded flex items-center justify-between dark:bg-secondary-800 dark:border-secondary-500",\n column.id === "-1"\n ? "h-0 border-none p-0 pb-4"\n : column.fixed\n ? "cursor-not-allowed"\n : "cursor-grab"\n )}\n >\n {column.id !== "-1" && (\n <>\n <Toggle\n size="sm"\n toggledIcon={column?.fixed ? iconList.lock : undefined}\n toggled={column?.selected}\n label={<span className="ml-2">{column?.header}</span>}\n disabled={column?.fixed}\n onChange={() => {\n if ( !column?.fixed ) {\n toggleItem( index );\n }\n }}\n />\n {!column.fixed && (\n <ADPIcon\n size="md"\n icon={iconList.drag}\n className="cursor-grab text-secondary-400 dark:text-secondary-500"\n />\n )}\n </>\n )}\n </div>\n );\n })}\n </div>\n\n {/* CTA Buttons */}\n <div className="flex justify-end gap-4">\n <Button size="sm" variant="outline" className="w-[99px]" onClick={closeSettings}>\n Cancel\n </Button>\n <Button size="sm" variant="filled" color="primary" className="w-[99px]" onClick={handleSettingsSubmit}>\n Save\n </Button>\n </div>\n </div>\n );\n};\n'
|
|
1849
1821
|
}
|
|
1850
1822
|
],
|
|
1851
1823
|
"directories": []
|
|
@@ -1854,9 +1826,10 @@ export const checkmarkClipPath = \`polygon(
|
|
|
1854
1826
|
"name": "datePicker",
|
|
1855
1827
|
"description": "The DatePicker component provides an interface for selecting dates with a calendar popup. Built on react-day-picker for robust date selection functionality. **When to use:** - Date input fields in forms - Date range selection - Scheduling and booking interfaces - Filtering by date **Component Architecture:** - Built with react-day-picker - Styled with Tailwind CSS - Supports single date and date range selection - Keyboard navigation and accessibility",
|
|
1856
1828
|
"dependencies": [
|
|
1857
|
-
"class-variance-authority",
|
|
1858
1829
|
"react",
|
|
1859
|
-
"react-datepicker"
|
|
1830
|
+
"react-datepicker",
|
|
1831
|
+
"tailwind-variants",
|
|
1832
|
+
"tailwind-merge"
|
|
1860
1833
|
],
|
|
1861
1834
|
"internalDependencies": [
|
|
1862
1835
|
"textField"
|
|
@@ -1864,15 +1837,11 @@ export const checkmarkClipPath = \`polygon(
|
|
|
1864
1837
|
"files": [
|
|
1865
1838
|
{
|
|
1866
1839
|
"name": "index.ts",
|
|
1867
|
-
"content": '// Export component\nexport { DatePicker } from "./datePicker";\n// eslint-disable-next-line no-duplicate-imports\nexport type { TDatePickerCombinedProps } from "./datePicker"
|
|
1868
|
-
},
|
|
1869
|
-
{
|
|
1870
|
-
"name": "datePickerVariants.ts",
|
|
1871
|
-
"content": 'import { cva } from "class-variance-authority";\n\nexport const datePickerVariants = cva(\n "relative inline-block w-full",\n {\n variants: {\n variant: {\n default: ""\n },\n size: {\n sm: "text-sm",\n md: "text-base",\n lg: "text-lg"\n }\n },\n defaultVariants: {\n variant: "default",\n size: "md"\n }\n }\n);\n\nexport const datePickerCalendarVariants = cva(\n "w-fit p-6 shadow-xs rounded relative bg-white z-[2020] dark:bg-secondary-800",\n {\n variants: {\n variant: {\n default: ""\n }\n },\n defaultVariants: {\n variant: "default"\n }\n }\n);\n\nexport const calendarStyles = {\n // Main calendar container\n calendar: "border-none bg-transparent",\n\n // Header styling\n header: "border-none bg-transparent rounded-none mb-2",\n timeHeader: "pt-1 mb-4",\n currentMonth: "text-secondary-500 dark:text-secondary-50",\n\n // Navigation\n navigationIcon: "before:border-gray-800 before:border-t before:border-r rtl:rotate-180 rtl:-top-2.5",\n navigationPrevious: "rtl:left-auto rtl:right-[0px]",\n navigationNext: "rtl:right-auto rtl:left-[0px]",\n navigationNextWithTime: "rtl:!left-[122px] rtl:!right-auto",\n\n // Month view\n month: "m-0",\n monthWrapper: "flex gap-1 justify-evenly",\n monthContainer: "me-1 last:mr-0 rtl:float-right",\n monthText:\n "m-0 px-3 py-2 text-base rounded text-secondary-300 hover:bg-primary-50 hover:text-secondary-500 " +\n "ease-in-out duration-200 dark:text-secondary-200 dark:hover:bg-secondary-600",\n monthTextKeyboardSelected: "bg-primary-50 text-secondary-500 dark:bg-secondary-600 dark:text-secondary-50",\n monthTextSelected:\n "bg-primary-500 text-white hover:bg-primary-800 hover:text-white hover:rounded " +\n "dark:hover:bg-primary-300 dark:hover:text-iridium dark:bg-primary-400 dark:text-iridium",\n\n // Week view\n week: "flex gap-1 mb-1 last:mb-0",\n\n // Day styling\n day:\n "m-0 w-[46px] h-8 flex items-center justify-center rounded text-base text-secondary-500 " +\n "hover:bg-primary-50 hover:rounded dark:text-secondary-50 dark:hover:bg-secondary-600 dark:hover:text-secondary-200",\n dayNames: "flex gap-1 mt-6",\n dayName: "w-[46px] m-0 text-base text-secondary-300 h-8 flex items-center justify-center dark:text-secondary-200",\n daySelected:\n "text-white bg-primary-500 hover:bg-primary-800 hover:text-white hover:rounded " +\n "dark:text-iridium dark:bg-primary-400 dark:hover:bg-primary-300 dark:hover:text-iridium",\n dayInRange: "bg-primary-50 dark:bg-secondary-600 dark:text-secondary-200",\n dayRangeStartEnd:\n "bg-primary-500 !text-white hover:!bg-primary-800 hover:rounded " +\n "dark:!bg-primary-400 dark:!text-iridium dark:hover:!bg-primary-300",\n dayOutsideMonth: "pointer-events-none bg-transparent text-secondary-100 dark:text-secondary-400",\n dayDisabled:\n "bg-secondary-50 text-secondary-200 dark:text-secondary-400 dark:bg-secondary-600 dark:hover:text-secondary-400",\n\n // Year view\n year: "m-0",\n yearWrapper: "max-w-[222px] mx-auto justify-center gap-1",\n yearText:\n "m-0 py-2 px-3 text-secondary-300 text-md rounded hover:bg-primary-50 hover:text-secondary-500 " +\n "dark:text-secondary-200 dark:hover:bg-secondary-600 dark:hover:text-secondary-200",\n yearTextSelected:\n "bg-primary-500 text-white hover:bg-primary-800 hover:text-white hover:rounded ease-in-out duration-200 " +\n "dark:text-iridium dark:hover:text-iridium dark:bg-primary-400 dark:hover:bg-primary-300",\n yearTextKeyboardSelected: "bg-primary-50 text-secondary-500 dark:bg-secondary-600 dark:text-secondary-50",\n yearHeader: "mb-6 text-secondary-500 text-md dark:text-secondary-50",\n\n // Time selector\n time: "text-secondary-500 dark:bg-transparent",\n timeList: "!h-64",\n timeContainer: "border-gray-200 w-fit px-3 pb-3 rtl:border-l-0 rtl:border-r-[1px]",\n timeListItem:\n "!h-8 text-base text-secondary-500 rounded flex justify-center items-center hover:!bg-primary-50 " +\n "dark:hover:!bg-secondary-600 dark:text-secondary-50 dark:hover:text-secondary-200",\n timeListItemSelected: "!bg-primary-500 hover:!bg-primary-700 dark:!bg-primary-400 dark:!text-iridium dark:hover:!bg-primary-300"\n};'
|
|
1840
|
+
"content": '// Export component\nexport { DatePicker } from "./datePicker";\n// eslint-disable-next-line no-duplicate-imports\nexport type { TDatePickerCombinedProps } from "./datePicker";'
|
|
1872
1841
|
},
|
|
1873
1842
|
{
|
|
1874
1843
|
"name": "datePicker.tsx",
|
|
1875
|
-
"content": 'import * as React from "react";\nimport { forwardRef } from "react";\nimport DatePickerComp, { CalendarContainer, type DatePickerProps } from "react-datepicker";\nimport { cn } from "@utils";\nimport "react-datepicker/dist/react-datepicker.css";\n\nimport { TextField, type ITextFieldProps } from "../textField";\n\nimport "./datePicker.styles.css";\n\nexport type TDatePickerCombinedProps = ( DatePickerProps & {\n inline?: false;\n textFieldProps?: Omit<ITextFieldProps, "ref" | "onBlur" | "onFocus" | "disabled">;\n}) | ( DatePickerProps & {\n inline?: true;\n textFieldProps?: never;\n});\n\nfunction isValidDate( testDate?: Date | null ) {\n if ( testDate ) {\n try {\n if ( !( new Date( testDate ) instanceof Date ) || new Date( testDate ).toString() === "Invalid Date" ) {\n return false;\n }\n } catch {\n return false;\n }\n }\n return true;\n}\n\n/**\n * DatePicker component that wraps react-datepicker with custom styling and text field integration\n *\n * @example\n * ```tsx\n * // Basic usage\n * <DatePicker\n * selected={date}\n * onChange={(date) => setDate(date)}\n * />\n *\n * // With time selection\n * <DatePicker\n * selected={date}\n * onChange={(date) => setDate(date)}\n * showTimeSelect\n * />\n *\n * // Inline calendar\n * <DatePicker\n * selected={date}\n * onChange={(date) => setDate(date)}\n * inline\n * />\n * ```\n */\nexport const DatePicker = forwardRef<HTMLDivElement, TDatePickerCombinedProps>(({\n inline = false,\n selected,\n showTimeSelect = false,\n textFieldProps,\n ...restProps\n}, ref ): React.ReactElement | null => {\n\n const CustomContainer = ({ className, children }: { className?: string; children?: React.ReactNode }) => {\n return (\n <div\n ref={ref}\n className={cn(\n "datepicker-container",\n !inline && "w-fit p-6 shadow-xs rounded relative dark:bg-secondary-800"\n )}\n >\n {/* @ts-expect-error - Ignore children type mismatch with CalendarContainer */}\n <CalendarContainer className={className}>{children}</CalendarContainer>\n </div>\n );\n };\n\n const DatepickerCustomInput = forwardRef<HTMLInputElement, any>(( props, inputRef ) => (\n <TextField\n {...textFieldProps}\n {...props}\n readOnly\n ref={inputRef}\n className={cn( "w-full", textFieldProps?.className )}\n data-testid="datepicker-input"\n />\n ));\n\n DatepickerCustomInput.displayName = "DatepickerCustomInput";\n\n const validDate = isValidDate( selected );\n\n if ( !validDate ) {\n console.error( "DatePicker: Invalid date format" );\n return null;\n }\n\n return (\n <DatePickerComp\n {...restProps}\n selected={selected}\n calendarContainer={CustomContainer}\n inline={inline}\n showTimeSelect={showTimeSelect}\n customInput={<DatepickerCustomInput />}\n popperPlacement="bottom-start"\n showPopperArrow={false}\n wrapperClassName="w-full"\n className={cn(\n // Calendar base styling\n "[&.react-datepicker]:border-none [&.react-datepicker]:bg-transparent",\n\n // Header styling\n "[&_.react-datepicker__header]:border-none [&_.react-datepicker__header]:bg-transparent " +\n "[&_.react-datepicker__header]:rounded-none [&_.react-datepicker__header]:mb-2",\n "[&_.react-datepicker__header--time]:pt-1 [&_.react-datepicker__header--time]:mb-4",\n\n // Current month styling\n "[&_.react-datepicker__current-month]:text-secondary-500 [&_.react-datepicker__current-month]:dark:text-secondary-50",\n\n // Navigation\n "[&_.react-datepicker__navigation-icon]:before:border-gray-800 [&_.react-datepicker__navigation-icon]:before:border-t " +\n "[&_.react-datepicker__navigation-icon]:before:border-r",\n "[&_.react-datepicker__navigation-icon]:rtl:rotate-180 [&_.react-datepicker__navigation-icon]:rtl:-top-2.5",\n "[&_.react-datepicker__navigation--previous]:rtl:left-auto [&_.react-datepicker__navigation--previous]:rtl:right-[0px]",\n "[&_.react-datepicker__navigation--next]:rtl:right-auto [&_.react-datepicker__navigation--next]:rtl:left-[0px]",\n "[&_.react-datepicker__navigation--next--with-time]:rtl:!left-[122px] " +\n "[&_.react-datepicker__navigation--next--with-time]:rtl:!right-auto",\n\n // Month styling\n "[&_.react-datepicker__month]:m-0",\n "[&_.react-datepicker__month-wrapper]:flex [&_.react-datepicker__month-wrapper]:gap-1 " +\n "[&_.react-datepicker__month-wrapper]:justify-evenly",\n "[&_.react-datepicker__month-container]:me-1 [&_.react-datepicker__month-container]:last:mr-0 " +\n "[&_.react-datepicker__month-container]:rtl:float-right",\n\n // Month text - part 1\n "[&_.react-datepicker__month-text]:m-0 [&_.react-datepicker__month-text]:px-3 " +\n "[&_.react-datepicker__month-text]:py-2 [&_.react-datepicker__month-text]:text-base " +\n "[&_.react-datepicker__month-text]:rounded [&_.react-datepicker__month-text]:text-secondary-300",\n\n // Month text - part 2\n "[&_.react-datepicker__month-text]:hover:bg-primary-50 [&_.react-datepicker__month-text]:hover:text-secondary-500 " +\n "[&_.react-datepicker__month-text]:ease-in-out [&_.react-datepicker__month-text]:duration-200",\n\n // Month text - dark mode\n "[&_.react-datepicker__month-text]:dark:text-secondary-200 [&_.react-datepicker__month-text]:dark:hover:bg-secondary-600",\n\n // Month text - keyboard selected\n "[&_.react-datepicker__month-text--keyboard-selected]:bg-primary-50 " +\n "[&_.react-datepicker__month-text--keyboard-selected]:text-secondary-500",\n\n "[&_.react-datepicker__month-text--keyboard-selected]:dark:bg-secondary-600 " +\n "[&_.react-datepicker__month-text--keyboard-selected]:dark:text-secondary-50",\n\n // Month text - selected - part 1\n "[&_.react-datepicker__month-text--selected]:bg-primary-500 [&_.react-datepicker__month-text--selected]:text-white",\n\n // Month text - selected - part 2\n "[&_.react-datepicker__month-text--selected]:hover:bg-primary-800 " +\n "[&_.react-datepicker__month-text--selected]:hover:text-white " +\n "[&_.react-datepicker__month-text--selected]:hover:rounded",\n\n // Month text - selected - dark mode\n "[&_.react-datepicker__month-text--selected]:dark:hover:bg-primary-300 " +\n "[&_.react-datepicker__month-text--selected]:dark:hover:text-iridium " +\n "[&_.react-datepicker__month-text--selected]:dark:bg-primary-400 " +\n "[&_.react-datepicker__month-text--selected]:dark:text-iridium",\n\n // Week styling\n "[&_.react-datepicker__week]:flex [&_.react-datepicker__week]:gap-1 " +\n "[&_.react-datepicker__week]:mb-1 [&_.react-datepicker__week]:last:mb-0",\n\n // Day styling - part 1\n "[&_.react-datepicker__day]:m-0 [&_.react-datepicker__day]:w-[46px] " +\n "[&_.react-datepicker__day]:h-8 [&_.react-datepicker__day]:flex " +\n "[&_.react-datepicker__day]:items-center [&_.react-datepicker__day]:justify-center",\n\n // Day styling - part 2\n "[&_.react-datepicker__day]:rounded [&_.react-datepicker__day]:text-base " +\n "[&_.react-datepicker__day]:text-secondary-500",\n\n // Day styling - hover\n "[&_.react-datepicker__day]:hover:bg-primary-50 [&_.react-datepicker__day]:hover:rounded",\n\n // Day styling - dark mode\n "[&_.react-datepicker__day]:dark:text-secondary-50 " +\n "[&_.react-datepicker__day]:dark:hover:bg-secondary-600 " +\n "[&_.react-datepicker__day]:dark:hover:text-secondary-200",\n\n // Day names\n "[&_.react-datepicker__day-names]:flex [&_.react-datepicker__day-names]:gap-1 " +\n "[&_.react-datepicker__day-names]:mt-6",\n\n // Day name - part 1\n "[&_.react-datepicker__day-name]:w-[46px] [&_.react-datepicker__day-name]:m-0 " +\n "[&_.react-datepicker__day-name]:text-base [&_.react-datepicker__day-name]:text-secondary-300",\n\n // Day name - part 2\n "[&_.react-datepicker__day-name]:h-8 [&_.react-datepicker__day-name]:flex " +\n "[&_.react-datepicker__day-name]:items-center [&_.react-datepicker__day-name]:justify-center",\n\n // Day name - dark mode\n "[&_.react-datepicker__day-name]:dark:text-secondary-200",\n\n // Selected day - part 1\n "[&_.react-datepicker__day--selected]:text-white [&_.react-datepicker__day--selected]:bg-primary-500",\n\n // Selected day - hover\n "[&_.react-datepicker__day--selected]:hover:bg-primary-800 " +\n "[&_.react-datepicker__day--selected]:hover:text-white " +\n "[&_.react-datepicker__day--selected]:hover:rounded",\n\n // Selected day - dark mode\n "[&_.react-datepicker__day--selected]:dark:text-iridium " +\n "[&_.react-datepicker__day--selected]:dark:bg-primary-400 " +\n "[&_.react-datepicker__day--selected]:dark:hover:bg-primary-300 " +\n "[&_.react-datepicker__day--selected]:dark:hover:text-iridium",\n\n // Day in range - part 1\n "[&_.react-datepicker__day--in-range]:bg-primary-50",\n\n // Day in range - dark mode\n "[&_.react-datepicker__day--in-range]:dark:bg-secondary-600 " +\n "[&_.react-datepicker__day--in-range]:dark:text-secondary-200",\n\n // Day in range - today - part 1\n "[&_.react-datepicker__day--in-range.react-datepicker__day--today]:text-secondary-500 " +\n "[&_.react-datepicker__day--in-range.react-datepicker__day--today]:font-medium",\n\n // Day in range - today - part 2\n "[&_.react-datepicker__day--in-range.react-datepicker__day--today]:hover:bg-primary-50",\n\n // Day in range - today - dark mode\n "[&_.react-datepicker__day--in-range.react-datepicker__day--today]:dark:text-secondary-200 " +\n "[&_.react-datepicker__day--in-range.react-datepicker__day--today]:dark:bg-secondary-600",\n\n // Range start and end - part 1\n "[&_.react-datepicker__day--range-start]:bg-primary-500 " +\n "[&_.react-datepicker__day--range-start]:!text-white",\n\n // Range start and end - part 2\n "[&_.react-datepicker__day--range-end]:bg-primary-500 " +\n "[&_.react-datepicker__day--range-end]:!text-white",\n\n // Range start and end - hover - part 1\n "[&_.react-datepicker__day--range-start]:hover:!bg-primary-800 " +\n "[&_.react-datepicker__day--range-start]:hover:rounded",\n\n // Range start and end - hover - part 2\n "[&_.react-datepicker__day--range-end]:hover:!bg-primary-800 " +\n "[&_.react-datepicker__day--range-end]:hover:rounded",\n\n // Range start and end - dark mode - part 1\n "[&_.react-datepicker__day--range-start]:dark:!bg-primary-400 " +\n "[&_.react-datepicker__day--range-start]:dark:!text-iridium " +\n "[&_.react-datepicker__day--range-start]:dark:hover:!bg-primary-300",\n\n // Range start and end - dark mode - part 2\n "[&_.react-datepicker__day--range-end]:dark:!bg-primary-400 " +\n "[&_.react-datepicker__day--range-end]:dark:!text-iridium " +\n "[&_.react-datepicker__day--range-end]:dark:hover:!bg-primary-300",\n\n // Outside month - part 1\n "[&_.react-datepicker__day--outside-month]:pointer-events-none " +\n "[&_.react-datepicker__day--outside-month]:bg-transparent",\n\n // Outside month - part 2\n "[&_.react-datepicker__day--outside-month]:text-secondary-100",\n\n // Outside month - dark mode\n "[&_.react-datepicker__day--outside-month]:dark:text-secondary-400",\n\n // Disabled day - part 1\n "[&_.react-datepicker__day--disabled]:bg-secondary-50 " +\n "[&_.react-datepicker__day--disabled]:text-secondary-200",\n\n // Disabled day - dark mode\n "[&_.react-datepicker__day--disabled]:dark:text-secondary-400 " +\n "[&_.react-datepicker__day--disabled]:dark:bg-secondary-600 " +\n "[&_.react-datepicker__day--disabled]:dark:hover:text-secondary-400",\n\n // Year styling\n "[&_.react-datepicker__year]:m-0",\n\n // Year wrapper\n "[&_.react-datepicker__year-wrapper]:max-w-[222px] [&_.react-datepicker__year-wrapper]:mx-auto " +\n "[&_.react-datepicker__year-wrapper]:justify-center [&_.react-datepicker__year-wrapper]:gap-1",\n\n // Year text - part 1\n "[&_.react-datepicker__year-text]:m-0 [&_.react-datepicker__year-text]:py-2 " +\n "[&_.react-datepicker__year-text]:px-3 [&_.react-datepicker__year-text]:text-secondary-300",\n\n // Year text - part 2\n "[&_.react-datepicker__year-text]:text-md [&_.react-datepicker__year-text]:rounded",\n\n // Year text - hover\n "[&_.react-datepicker__year-text]:hover:bg-primary-50 " +\n "[&_.react-datepicker__year-text]:hover:text-secondary-500",\n\n // Year text - dark mode\n "[&_.react-datepicker__year-text]:dark:text-secondary-200 " +\n "[&_.react-datepicker__year-text]:dark:hover:bg-secondary-600 " +\n "[&_.react-datepicker__year-text]:dark:hover:text-secondary-200",\n\n // Selected year - part 1\n "[&_.react-datepicker__year-text--selected]:bg-primary-500 " +\n "[&_.react-datepicker__year-text--selected]:text-white",\n\n // Selected year - hover - part 1\n "[&_.react-datepicker__year-text--selected]:hover:bg-primary-800 " +\n "[&_.react-datepicker__year-text--selected]:hover:text-white",\n\n // Selected year - hover - part 2\n "[&_.react-datepicker__year-text--selected]:hover:rounded " +\n "[&_.react-datepicker__year-text--selected]:ease-in-out " +\n "[&_.react-datepicker__year-text--selected]:duration-200",\n\n // Selected year - dark mode - part 1\n "[&_.react-datepicker__year-text--selected]:dark:text-iridium " +\n "[&_.react-datepicker__year-text--selected]:dark:hover:text-iridium",\n\n // Selected year - dark mode - part 2\n "[&_.react-datepicker__year-text--selected]:dark:bg-primary-400 " +\n "[&_.react-datepicker__year-text--selected]:dark:hover:bg-primary-300",\n\n // Keyboard selected year - part 1\n "[&_.react-datepicker__year-text--keyboard-selected]:bg-primary-50 " +\n "[&_.react-datepicker__year-text--keyboard-selected]:text-secondary-500",\n\n // Keyboard selected year - dark mode\n "[&_.react-datepicker__year-text--keyboard-selected]:dark:bg-secondary-600 " +\n "[&_.react-datepicker__year-text--keyboard-selected]:dark:text-secondary-50",\n\n // Year header\n "[&_.react-datepicker-year-header]:mb-6 [&_.react-datepicker-year-header]:text-secondary-500 " +\n "[&_.react-datepicker-year-header]:text-md",\n\n // Year header - dark mode\n "[&_.react-datepicker-year-header]:dark:text-secondary-50",\n\n // Time container - part 1\n "[&_.react-datepicker__time]:text-secondary-500",\n "[&_.react-datepicker__time]:dark:bg-transparent",\n "[&_.react-datepicker__time-list]:!h-64",\n\n // Time container - part 2\n "[&_.react-datepicker__time-container]:border-gray-200 [&_.react-datepicker__time-container]:w-fit " +\n "[&_.react-datepicker__time-container]:px-3 [&_.react-datepicker__time-container]:pb-3",\n\n // Time container - RTL\n "[&_.react-datepicker__time-container]:rtl:border-l-0 " +\n "[&_.react-datepicker__time-container]:rtl:border-r-[1px]",\n\n // Time list item - part 1\n "[&_.react-datepicker__time-list-item]:!h-8 [&_.react-datepicker__time-list-item]:text-base " +\n "[&_.react-datepicker__time-list-item]:text-secondary-500 [&_.react-datepicker__time-list-item]:rounded",\n\n // Time list item - part 2\n "[&_.react-datepicker__time-list-item]:flex [&_.react-datepicker__time-list-item]:justify-center " +\n "[&_.react-datepicker__time-list-item]:items-center",\n\n // Time list item - hover\n "[&_.react-datepicker__time-list-item]:hover:!bg-primary-50",\n\n // Time list item - dark mode\n "[&_.react-datepicker__time-list-item]:dark:hover:!bg-secondary-600 " +\n "[&_.react-datepicker__time-list-item]:dark:text-secondary-50 " +\n "[&_.react-datepicker__time-list-item]:dark:hover:text-secondary-200",\n\n // Selected time - part 1\n "[&_.react-datepicker__time-list-item--selected]:!bg-primary-500",\n "[&_.react-datepicker__time-list-item--selected]:hover:!bg-primary-700",\n\n // Selected time - dark mode\n "[&_.react-datepicker__time-list-item--selected]:dark:!bg-primary-400 " +\n "[&_.react-datepicker__time-list-item--selected]:dark:!text-iridium " +\n "[&_.react-datepicker__time-list-item--selected]:dark:hover:!bg-primary-300",\n\n // Time header\n "[&_.react-datepicker-time__header]:text-md [&_.react-datepicker-time__header]:text-secondary-500",\n "[&_.react-datepicker-time__header]:dark:text-secondary-50",\n\n // Navigation with time\n "[&.react-datepicker_.react-datepicker__navigation--next--with-time:not(.react-datepicker__navigation--next--with-today-button)]:right-[116px]",\n\n // Z-index\n "[&.react-datepicker-popper]:z-[2020]"\n )}\n />\n );\n});\n\nDatePicker.displayName = "DatePicker";\n'
|
|
1844
|
+
"content": 'import * as React from "react";\nimport { forwardRef } from "react";\nimport DatePickerComp, { CalendarContainer, type DatePickerProps } from "react-datepicker";\nimport { cn } from "tailwind-variants";\n\nimport { TextField, type ITextFieldProps } from "../textField";\n\nimport "react-datepicker/dist/react-datepicker.css";\nimport "./datePicker.styles.css";\n\nexport type TDatePickerCombinedProps = ( DatePickerProps & {\n inline?: false;\n textFieldProps?: Omit<ITextFieldProps, "ref" | "onBlur" | "onFocus" | "disabled">;\n}) | ( DatePickerProps & {\n inline?: true;\n textFieldProps?: never;\n});\n\nfunction isValidDate( testDate?: Date | null ) {\n if ( testDate ) {\n try {\n if ( !( new Date( testDate ) instanceof Date ) || new Date( testDate ).toString() === "Invalid Date" ) {\n return false;\n }\n } catch {\n return false;\n }\n }\n return true;\n}\n\n/**\n * DatePicker component that wraps react-datepicker with custom styling and text field integration\n *\n * @example\n * ```tsx\n * // Basic usage\n * <DatePicker\n * selected={date}\n * onChange={(date) => setDate(date)}\n * />\n *\n * // With time selection\n * <DatePicker\n * selected={date}\n * onChange={(date) => setDate(date)}\n * showTimeSelect\n * />\n *\n * // Inline calendar\n * <DatePicker\n * selected={date}\n * onChange={(date) => setDate(date)}\n * inline\n * />\n * ```\n */\nexport const DatePicker = forwardRef<HTMLDivElement, TDatePickerCombinedProps>(({\n inline = false,\n selected,\n showTimeSelect = false,\n textFieldProps,\n ...restProps\n}, ref ): React.ReactElement | null => {\n\n const CustomContainer = ({ className, children }: { className?: string; children?: React.ReactNode }) => {\n return (\n <div\n ref={ref}\n className={cn(\n "datepicker-container",\n !inline && "w-fit p-6 shadow-xs rounded relative dark:bg-secondary-800"\n )}\n >\n {/* @ts-expect-error - Ignore children type mismatch with CalendarContainer */}\n <CalendarContainer className={className}>{children}</CalendarContainer>\n </div>\n );\n };\n\n const DatepickerCustomInput = forwardRef<HTMLInputElement, any>(( props, inputRef ) => (\n <TextField\n {...textFieldProps}\n {...props}\n readOnly\n ref={inputRef}\n className={cn( "w-full", textFieldProps?.className )}\n data-testid="datepicker-input"\n />\n ));\n\n DatepickerCustomInput.displayName = "DatepickerCustomInput";\n\n const validDate = isValidDate( selected );\n\n if ( !validDate ) {\n console.error( "DatePicker: Invalid date format" );\n return null;\n }\n\n return (\n <DatePickerComp\n {...restProps}\n selected={selected}\n calendarContainer={CustomContainer}\n inline={inline}\n showTimeSelect={showTimeSelect}\n customInput={<DatepickerCustomInput />}\n popperPlacement="bottom-start"\n showPopperArrow={false}\n wrapperClassName="w-full"\n />\n );\n});\n\nDatePicker.displayName = "DatePicker";\n'
|
|
1876
1845
|
},
|
|
1877
1846
|
{
|
|
1878
1847
|
"name": "datePicker.styles.css",
|
|
@@ -1889,22 +1858,23 @@ export const checkmarkClipPath = \`polygon(
|
|
|
1889
1858
|
"name": "distributionSlider",
|
|
1890
1859
|
"description": "The DistributionSlider component is an interactive form control that allows users to adjust proportional values across multiple segments through an intuitive drag-and-drop interface. It maintains a constant total value while redistributing proportions between adjacent segments. **When to use:** - Distributing percentages or values that must sum to a fixed total (e.g., difficulty levels, skill distributions) - Allowing users to visually adjust proportions between multiple categories - Creating interactive allocation interfaces (budgets, time allocation, resource distribution) - Form inputs that require proportional value adjustments - Settings panels where users configure distribution percentages **Component Architecture:** - Built with React and TypeScript - Styled with Tailwind CSS and class-variance-authority (cva) - Supports drag-and-drop interactions with mouse events - Full keyboard navigation support with arrow keys - Accessible with ARIA attributes and screen reader support",
|
|
1891
1860
|
"dependencies": [
|
|
1892
|
-
"
|
|
1893
|
-
"react"
|
|
1861
|
+
"tailwind-variants",
|
|
1862
|
+
"react",
|
|
1863
|
+
"tailwind-merge"
|
|
1894
1864
|
],
|
|
1895
1865
|
"internalDependencies": [],
|
|
1896
1866
|
"files": [
|
|
1897
1867
|
{
|
|
1898
1868
|
"name": "index.ts",
|
|
1899
|
-
"content": 'export {\n DistributionSlider,\n DEFAULT_SEGMENT_COLORS,\n type IDistributionSliderProps,\n type IDistributionSegment\n} from "./distributionSlider";\n\nexport {
|
|
1869
|
+
"content": 'export {\n DistributionSlider,\n DEFAULT_SEGMENT_COLORS,\n type IDistributionSliderProps,\n type IDistributionSegment\n} from "./distributionSlider";\n\nexport { distributionSliderVariants } from "./distributionSliderVariants";\n\n'
|
|
1900
1870
|
},
|
|
1901
1871
|
{
|
|
1902
1872
|
"name": "distributionSliderVariants.ts",
|
|
1903
|
-
"content": 'import {
|
|
1873
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const distributionSliderVariants = tv({\n slots: {\n container: "flex flex-col gap-3",\n label: "text-base font-medium text-secondary-400 dark:text-secondary-300",\n sliderWrapper: "flex flex-col gap-2",\n segmentLabels: "flex justify-between gap-2 max-md:flex-col max-md:gap-1",\n segmentLabel: "flex flex-col items-center gap-1 text-xs max-md:flex-row max-md:justify-between max-md:items-center",\n segmentName: "text-secondary-400 font-medium dark:text-secondary-300",\n segmentValue: "text-secondary-500 font-semibold dark:text-secondary-200",\n sliderTrack: [\n "relative bg-secondary-100 rounded-md cursor-pointer overflow-hidden",\n "select-none transition-all duration-150 ease-out",\n "dark:bg-secondary-800"\n ],\n segmentBackground: "absolute top-0 h-full transition-all duration-150 ease-out opacity-80 hover:opacity-90",\n segmentDivider: [\n "absolute top-0 w-2 h-full bg-white cursor-col-resize shadow-md",\n "border border-secondary-300 rounded-sm transition-all duration-150 ease-in-out",\n "-translate-x-1/2 z-10 hover:bg-secondary-50 hover:border-secondary-400 hover:shadow-lg",\n "focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"\n ],\n helperText: "text-xs font-normal"\n },\n variants: {\n size: {\n md: {\n segmentLabel: "text-xs",\n segmentName: "text-xs",\n segmentValue: "text-sm",\n sliderTrack: "h-6"\n },\n lg: {\n segmentLabel: "text-sm",\n segmentName: "text-sm",\n segmentValue: "text-base",\n sliderTrack: "h-7 rounded-xl"\n }\n },\n disabled: {\n true: {\n sliderTrack: "cursor-not-allowed opacity-50 dark:bg-secondary-800 dark:opacity-50",\n segmentDivider: "cursor-not-allowed"\n },\n false: {}\n },\n error: {\n true: {\n sliderTrack: "ring-2 ring-error-500 dark:ring-error-400",\n helperText: "text-error-500 dark:text-error-400"\n },\n false: {\n helperText: "text-secondary-400 dark:text-secondary-200"\n }\n },\n dragging: {\n true: {\n segmentDivider: "bg-primary-50 border-primary-400 shadow-xl dark:bg-primary-300 dark:border-primary-400"\n },\n false: {}\n },\n focused: {\n true: {\n segmentDivider: "bg-primary-100 border-primary-500 shadow-lg dark:bg-primary-200 dark:border-primary-400"\n },\n false: {}\n }\n },\n defaultVariants: {\n size: "md",\n disabled: false,\n error: false,\n dragging: false,\n focused: false\n }\n});\n'
|
|
1904
1874
|
},
|
|
1905
1875
|
{
|
|
1906
1876
|
"name": "distributionSlider.tsx",
|
|
1907
|
-
"content": '// Libraries\nimport * as React from "react";\nimport { type ReactNode, forwardRef, useState, useEffect, useRef, useCallback } from "react";\n\n// Methods / Hooks / Constants / Styles\nimport { cn } from "@utils";\nimport {\n distributionContainerVariants,\n distributionLabelVariants,\n sliderWrapperVariants,\n segmentLabelsVariants,\n segmentLabelVariants,\n segmentNameVariants,\n segmentValueVariants,\n sliderTrackVariants,\n segmentBackgroundVariants,\n segmentDividerVariants,\n distributionHelperTextVariants\n} from "./distributionSliderVariants";\n\nexport interface IDistributionSegment {\n name: string;\n value: number;\n color?: string;\n}\n\nexport interface IDistributionSliderProps {\n /**\n * Label for the distribution slider\n */\n label?: string | ReactNode;\n /**\n * Array of segments with names and values\n */\n segments: IDistributionSegment[];\n /**\n * Callback function called when segments change\n */\n onChange?: ( segments: IDistributionSegment[]) => void;\n /**\n * Mode of the distribution slider\n * - \'percentage\': Values sum to 100, displayed as percentages (e.g., 33%)\n * - \'absolute\': Values sum to targetTotal, displayed as raw numbers (e.g., 40)\n * @default \'percentage\'\n */\n mode?: "percentage" | "absolute";\n /**\n * Target total value for absolute mode (ignored in percentage mode)\n * @default 100\n */\n targetTotal?: number;\n /**\n * Prefix to display before values in absolute mode\n * Ignored in percentage mode\n */\n valuePrefix?: ReactNode;\n /**\n * Additional information or guidance displayed below the slider\n */\n helperText?: ReactNode;\n /**\n * Pass a boolean value to indicate if the component is in an error state\n */\n error?: boolean;\n /**\n * Whether the component is disabled\n */\n disabled?: boolean;\n /**\n * Pass space separated class names to override the DistributionSlider styles\n */\n className?: string;\n /**\n * Size of the slider\n * @default \'md\'\n */\n size?: "md" | "lg";\n /**\n * Step size for keyboard navigation (percentage of target total)\n * @default 1\n */\n keyboardStep?: number;\n}\n\n// Default colors for segments using project\'s color system\nexport const DEFAULT_SEGMENT_COLORS = [\n "rgb(6 118 215)", // primary-500 (blue)\n "rgb(61 180 115)", // success-500 (green)\n "rgb(245 154 32)", // warning-500 (yellow)\n "rgb(231 73 58)", // error-500 (red)\n "rgb(89 100 121)", // secondary-400 (gray)\n "rgb(152 162 179)" // gray-500 (light gray)\n] as const;\n\n/**\n * DistributionSlider component for adjusting proportional values across multiple segments.\n * Supports drag-and-drop interaction and keyboard navigation to redistribute values while maintaining total.\n *\n * @example\n * // Basic usage with level distribution\n * <DistributionSlider\n * segments={[\n * {name: \'Easy\', value: 33},\n * {name: \'Medium\', value: 34},\n * {name: \'Hard\', value: 33}\n * ]}\n * onChange={setSegments}\n * />\n */\nexport const DistributionSlider = forwardRef<HTMLDivElement, IDistributionSliderProps>(\n function DistributionSlider(\n {\n label,\n segments,\n onChange,\n mode = "percentage",\n targetTotal = 100,\n valuePrefix,\n helperText,\n error = false,\n disabled = false,\n className,\n size = "md",\n keyboardStep = 1,\n ...props\n }: IDistributionSliderProps,\n ref\n ) {\n // In percentage mode, always use 100 as the target total\n const effectiveTargetTotal = mode === "percentage" ? 100 : targetTotal;\n const sliderRef = useRef<HTMLDivElement>( null );\n const [ isDragging, setIsDragging ] = useState( false );\n const [ dragIndex, setDragIndex ] = useState<number | null>( null );\n const [ dragStartX, setDragStartX ] = useState( 0 );\n const [ initialSegments, setInitialSegments ] = useState<IDistributionSegment[]>([]);\n const [ focusedDividerIndex, setFocusedDividerIndex ] = useState<number | null>( null );\n\n // Calculate segment positions as percentages\n const getSegmentPositions = useCallback((): number[] => {\n let cumulative = 0;\n const positions = [0];\n\n segments.forEach( seg => {\n cumulative += seg.value;\n positions.push(( cumulative / effectiveTargetTotal ) * 100 );\n });\n\n return positions;\n }, [ segments, effectiveTargetTotal ]);\n\n // Handle mouse down on segment divider\n const handleMouseDown = useCallback(( e: React.MouseEvent, index: number ) => {\n if ( disabled ) {\n return;\n }\n\n e.preventDefault();\n setIsDragging( true );\n setDragIndex( index );\n setDragStartX( e.clientX );\n setInitialSegments([...segments]);\n }, [ disabled, segments ]);\n\n // Handle mouse move during drag\n const handleMouseMove = useCallback(( e: MouseEvent ) => {\n if ( !isDragging || dragIndex === null || !sliderRef.current ) {\n return;\n }\n\n const rect = sliderRef.current.getBoundingClientRect();\n const deltaX = e.clientX - dragStartX;\n const deltaPercent = ( deltaX / rect.width ) * 100;\n const deltaValue = ( deltaPercent / 100 ) * effectiveTargetTotal;\n\n // Calculate new values\n const newSegments = [...initialSegments];\n const leftSegment = newSegments[dragIndex];\n const rightSegment = newSegments[dragIndex + 1];\n\n if ( !leftSegment || !rightSegment ) {\n return;\n }\n\n // Apply constraints\n const minValue = 0; // Minimum 0% per segment\n const newLeftValue = Math.max( minValue, Math.min( leftSegment.value + rightSegment.value - minValue, leftSegment.value + deltaValue ));\n const newRightValue = ( leftSegment.value + rightSegment.value ) - newLeftValue;\n\n newSegments[dragIndex] = { ...leftSegment, value: newLeftValue };\n newSegments[dragIndex + 1] = { ...rightSegment, value: newRightValue };\n\n onChange?.( newSegments );\n }, [ isDragging, dragIndex, dragStartX, initialSegments, effectiveTargetTotal, onChange ]);\n\n // Handle mouse up\n const handleMouseUp = useCallback(() => {\n setIsDragging( false );\n setDragIndex( null );\n }, []);\n\n // Handle keyboard navigation\n const handleKeyDown = useCallback(( e: React.KeyboardEvent, index: number ) => {\n if ( disabled ) {\n return;\n }\n\n const { key } = e;\n\n if ( key === "ArrowLeft" || key === "ArrowRight" ) {\n e.preventDefault();\n\n const direction = key === "ArrowLeft" ? -1 : 1;\n const stepValue = ( keyboardStep / 100 ) * effectiveTargetTotal;\n const deltaValue = direction * stepValue;\n\n // Calculate new values\n const newSegments = [...segments];\n const leftSegment = newSegments[index];\n const rightSegment = newSegments[index + 1];\n\n if ( !leftSegment || !rightSegment ) {\n return;\n }\n\n // Apply constraints\n const minValue = 0; // Minimum 0% per segment\n const newLeftValue = Math.max( minValue, Math.min( leftSegment.value + rightSegment.value - minValue, leftSegment.value + deltaValue ));\n const newRightValue = ( leftSegment.value + rightSegment.value ) - newLeftValue;\n\n newSegments[index] = { ...leftSegment, value: newLeftValue };\n newSegments[index + 1] = { ...rightSegment, value: newRightValue };\n\n onChange?.( newSegments );\n }\n }, [ disabled, keyboardStep, effectiveTargetTotal, segments, onChange ]);\n\n // Handle focus events\n const handleFocus = useCallback(( index: number ) => {\n if ( !disabled ) {\n setFocusedDividerIndex( index );\n }\n }, [disabled]);\n\n const handleBlur = useCallback(() => {\n setFocusedDividerIndex( null );\n }, []);\n\n // Set up global mouse events\n useEffect(() => {\n if ( isDragging ) {\n document.addEventListener( "mousemove", handleMouseMove );\n document.addEventListener( "mouseup", handleMouseUp );\n\n return () => {\n document.removeEventListener( "mousemove", handleMouseMove );\n document.removeEventListener( "mouseup", handleMouseUp );\n };\n }\n }, [ isDragging, handleMouseMove, handleMouseUp ]);\n\n const positions = getSegmentPositions();\n\n const formatValue = ( value: number ): ReactNode => {\n const rounded = Math.round( value );\n if ( mode === "percentage" ) {\n // In percentage mode, display as percentage of 100\n const percentage = Math.round(( value / effectiveTargetTotal ) * 100 );\n return `${percentage}%`;\n }\n // In absolute mode, display the raw value with optional prefix\n if ( valuePrefix ) {\n return (\n <>\n {valuePrefix}\n {rounded}\n </>\n );\n }\n return rounded.toString();\n };\n\n return (\n <div ref={ref} className={cn( distributionContainerVariants(), className )} {...props}>\n {label && (\n <label className={distributionLabelVariants()}>\n {label}\n </label>\n )}\n\n <div className={sliderWrapperVariants()}>\n {/* Segment labels */}\n <div className={segmentLabelsVariants()}>\n {segments.map(( segment ) => (\n <div key={segment.name} className={segmentLabelVariants({ size })}>\n <span className={segmentNameVariants({ size })}>{segment.name}</span>\n <span className={segmentValueVariants({ size })}>{formatValue( segment.value )}</span>\n </div>\n ))}\n </div>\n\n {/* Slider track */}\n <div\n ref={sliderRef}\n className={sliderTrackVariants({\n size,\n disabled,\n error\n })}\n >\n {/* Segment backgrounds */}\n {segments.map(( segment, index ) => (\n <div\n key={`${segment.name}-bg`}\n className={segmentBackgroundVariants()}\n style={{\n left: `${positions[index]}%`,\n width: `${positions[index + 1] - positions[index]}%`,\n backgroundColor: segment.color || DEFAULT_SEGMENT_COLORS[index % DEFAULT_SEGMENT_COLORS.length]\n }}\n />\n ))}\n\n {/* Segment dividers (draggable and keyboard accessible) */}\n {positions.slice( 1, -1 ).map(( position, index ) => (\n <div\n key={`divider-${index}`}\n className={segmentDividerVariants({\n dragging: isDragging && dragIndex === index,\n focused: focusedDividerIndex === index,\n disabled\n })}\n style={{ left: `${position}%` }}\n tabIndex={disabled ? -1 : 0}\n role="slider"\n aria-label={`Adjust distribution between ${segments[index].name} and ${segments[index + 1].name}`}\n aria-valuemin={0}\n aria-valuemax={segments[index].value + segments[index + 1].value}\n aria-valuenow={Math.round( segments[index].value )}\n aria-valuetext={\n `${segments[index].name}: ${formatValue( segments[index].value )}, ${segments[index + 1].name}: ${formatValue( segments[index + 1].value )}`\n }\n onMouseDown={( e ) => handleMouseDown( e, index )}\n onKeyDown={( e ) => handleKeyDown( e, index )}\n onFocus={() => handleFocus( index )}\n onBlur={handleBlur}\n />\n ))}\n </div>\n </div>\n\n {helperText && (\n <div className={distributionHelperTextVariants({ error })}>\n {helperText}\n </div>\n )}\n </div>\n );\n }\n);\n\nDistributionSlider.displayName = "DistributionSlider";\n\n'
|
|
1877
|
+
"content": '// Libraries\nimport * as React from "react";\nimport { type ReactNode, forwardRef, useState, useEffect, useRef, useCallback, useMemo } from "react";\n\n// Methods / Hooks / Constants / Styles\nimport { distributionSliderVariants } from "./distributionSliderVariants";\n\nexport interface IDistributionSegment {\n name: string;\n value: number;\n color?: string;\n}\n\nexport interface IDistributionSliderProps {\n /**\n * Label for the distribution slider\n */\n label?: string | ReactNode;\n /**\n * Array of segments with names and values\n */\n segments: IDistributionSegment[];\n /**\n * Callback function called when segments change\n */\n onChange?: ( segments: IDistributionSegment[]) => void;\n /**\n * Mode of the distribution slider\n * - \'percentage\': Values sum to 100, displayed as percentages (e.g., 33%)\n * - \'absolute\': Values sum to targetTotal, displayed as raw numbers (e.g., 40)\n * @default \'percentage\'\n */\n mode?: "percentage" | "absolute";\n /**\n * Target total value for absolute mode (ignored in percentage mode)\n * @default 100\n */\n targetTotal?: number;\n /**\n * Prefix to display before values in absolute mode\n * Ignored in percentage mode\n */\n valuePrefix?: ReactNode;\n /**\n * Additional information or guidance displayed below the slider\n */\n helperText?: ReactNode;\n /**\n * Pass a boolean value to indicate if the component is in an error state\n */\n error?: boolean;\n /**\n * Whether the component is disabled\n */\n disabled?: boolean;\n /**\n * Pass space separated class names to override the DistributionSlider styles\n */\n className?: string;\n /**\n * Size of the slider\n * @default \'md\'\n */\n size?: "md" | "lg";\n /**\n * Step size for keyboard navigation (percentage of target total)\n * @default 1\n */\n keyboardStep?: number;\n}\n\n// Default colors for segments using project\'s color system\nexport const DEFAULT_SEGMENT_COLORS = [\n "rgb(6 118 215)", // primary-500 (blue)\n "rgb(61 180 115)", // success-500 (green)\n "rgb(245 154 32)", // warning-500 (yellow)\n "rgb(231 73 58)", // error-500 (red)\n "rgb(89 100 121)", // secondary-400 (gray)\n "rgb(152 162 179)" // gray-500 (light gray)\n] as const;\n\n/**\n * DistributionSlider component for adjusting proportional values across multiple segments.\n * Supports drag-and-drop interaction and keyboard navigation to redistribute values while maintaining total.\n *\n * @example\n * // Basic usage with level distribution\n * <DistributionSlider\n * segments={[\n * {name: \'Easy\', value: 33},\n * {name: \'Medium\', value: 34},\n * {name: \'Hard\', value: 33}\n * ]}\n * onChange={setSegments}\n * />\n */\nexport const DistributionSlider = forwardRef<HTMLDivElement, IDistributionSliderProps>(\n function DistributionSlider(\n {\n label,\n segments,\n onChange,\n mode = "percentage",\n targetTotal = 100,\n valuePrefix,\n helperText,\n error = false,\n disabled = false,\n className,\n size = "md",\n keyboardStep = 1,\n ...props\n }: IDistributionSliderProps,\n ref\n ) {\n // In percentage mode, always use 100 as the target total\n const effectiveTargetTotal = mode === "percentage" ? 100 : targetTotal;\n const sliderRef = useRef<HTMLDivElement>( null );\n const [ isDragging, setIsDragging ] = useState( false );\n const [ dragIndex, setDragIndex ] = useState<number | null>( null );\n const [ dragStartX, setDragStartX ] = useState( 0 );\n const [ initialSegments, setInitialSegments ] = useState<IDistributionSegment[]>([]);\n const [ focusedDividerIndex, setFocusedDividerIndex ] = useState<number | null>( null );\n\n // Calculate segment positions as percentages\n const getSegmentPositions = useCallback((): number[] => {\n let cumulative = 0;\n const positions = [0];\n\n segments.forEach( seg => {\n cumulative += seg.value;\n positions.push(( cumulative / effectiveTargetTotal ) * 100 );\n });\n\n return positions;\n }, [ segments, effectiveTargetTotal ]);\n\n // Handle mouse down on segment divider\n const handleMouseDown = useCallback(( e: React.MouseEvent, index: number ) => {\n if ( disabled ) {\n return;\n }\n\n e.preventDefault();\n setIsDragging( true );\n setDragIndex( index );\n setDragStartX( e.clientX );\n setInitialSegments([...segments]);\n }, [ disabled, segments ]);\n\n // Handle mouse move during drag\n const handleMouseMove = useCallback(( e: MouseEvent ) => {\n if ( !isDragging || dragIndex === null || !sliderRef.current ) {\n return;\n }\n\n const rect = sliderRef.current.getBoundingClientRect();\n const deltaX = e.clientX - dragStartX;\n const deltaPercent = ( deltaX / rect.width ) * 100;\n const deltaValue = ( deltaPercent / 100 ) * effectiveTargetTotal;\n\n // Calculate new values\n const newSegments = [...initialSegments];\n const leftSegment = newSegments[dragIndex];\n const rightSegment = newSegments[dragIndex + 1];\n\n if ( !leftSegment || !rightSegment ) {\n return;\n }\n\n // Apply constraints\n const minValue = 0; // Minimum 0% per segment\n const newLeftValue = Math.max( minValue, Math.min( leftSegment.value + rightSegment.value - minValue, leftSegment.value + deltaValue ));\n const newRightValue = ( leftSegment.value + rightSegment.value ) - newLeftValue;\n\n newSegments[dragIndex] = { ...leftSegment, value: newLeftValue };\n newSegments[dragIndex + 1] = { ...rightSegment, value: newRightValue };\n\n onChange?.( newSegments );\n }, [ isDragging, dragIndex, dragStartX, initialSegments, effectiveTargetTotal, onChange ]);\n\n // Handle mouse up\n const handleMouseUp = useCallback(() => {\n setIsDragging( false );\n setDragIndex( null );\n }, []);\n\n // Handle keyboard navigation\n const handleKeyDown = useCallback(( e: React.KeyboardEvent, index: number ) => {\n if ( disabled ) {\n return;\n }\n\n const { key } = e;\n\n if ( key === "ArrowLeft" || key === "ArrowRight" ) {\n e.preventDefault();\n\n const direction = key === "ArrowLeft" ? -1 : 1;\n const stepValue = ( keyboardStep / 100 ) * effectiveTargetTotal;\n const deltaValue = direction * stepValue;\n\n // Calculate new values\n const newSegments = [...segments];\n const leftSegment = newSegments[index];\n const rightSegment = newSegments[index + 1];\n\n if ( !leftSegment || !rightSegment ) {\n return;\n }\n\n // Apply constraints\n const minValue = 0; // Minimum 0% per segment\n const newLeftValue = Math.max( minValue, Math.min( leftSegment.value + rightSegment.value - minValue, leftSegment.value + deltaValue ));\n const newRightValue = ( leftSegment.value + rightSegment.value ) - newLeftValue;\n\n newSegments[index] = { ...leftSegment, value: newLeftValue };\n newSegments[index + 1] = { ...rightSegment, value: newRightValue };\n\n onChange?.( newSegments );\n }\n }, [ disabled, keyboardStep, effectiveTargetTotal, segments, onChange ]);\n\n // Handle focus events\n const handleFocus = useCallback(( index: number ) => {\n if ( !disabled ) {\n setFocusedDividerIndex( index );\n }\n }, [disabled]);\n\n const handleBlur = useCallback(() => {\n setFocusedDividerIndex( null );\n }, []);\n\n // Set up global mouse events\n useEffect(() => {\n if ( isDragging ) {\n document.addEventListener( "mousemove", handleMouseMove );\n document.addEventListener( "mouseup", handleMouseUp );\n\n return () => {\n document.removeEventListener( "mousemove", handleMouseMove );\n document.removeEventListener( "mouseup", handleMouseUp );\n };\n }\n }, [ isDragging, handleMouseMove, handleMouseUp ]);\n\n const positions = getSegmentPositions();\n\n const formatValue = ( value: number ): ReactNode => {\n const rounded = Math.round( value );\n if ( mode === "percentage" ) {\n // In percentage mode, display as percentage of 100\n const percentage = Math.round(( value / effectiveTargetTotal ) * 100 );\n return `${percentage}%`;\n }\n // In absolute mode, display the raw value with optional prefix\n if ( valuePrefix ) {\n return (\n <>\n {valuePrefix}\n {rounded}\n </>\n );\n }\n return rounded.toString();\n };\n\n const {\n container,\n label: labelClass,\n sliderWrapper,\n segmentLabels,\n segmentLabel,\n segmentName,\n segmentValue,\n sliderTrack,\n segmentBackground,\n helperText: helperTextClass\n } = useMemo(\n () => distributionSliderVariants({ size, disabled, error }),\n [ size, disabled, error ]\n );\n\n return (\n <div ref={ref} className={container({ class: className })} {...props}>\n {label && (\n <label className={labelClass()}>\n {label}\n </label>\n )}\n\n <div className={sliderWrapper()}>\n {/* Segment labels */}\n <div className={segmentLabels()}>\n {segments.map(( segment ) => (\n <div key={segment.name} className={segmentLabel()}>\n <span className={segmentName()}>{segment.name}</span>\n <span className={segmentValue()}>{formatValue( segment.value )}</span>\n </div>\n ))}\n </div>\n\n {/* Slider track */}\n <div\n ref={sliderRef}\n className={sliderTrack()}\n >\n {/* Segment backgrounds */}\n {segments.map(( segment, index ) => (\n <div\n key={`${segment.name}-bg`}\n className={segmentBackground()}\n style={{\n left: `${positions[index]}%`,\n width: `${positions[index + 1] - positions[index]}%`,\n backgroundColor: segment.color || DEFAULT_SEGMENT_COLORS[index % DEFAULT_SEGMENT_COLORS.length]\n }}\n />\n ))}\n\n {/* Segment dividers (draggable and keyboard accessible) */}\n {positions.slice( 1, -1 ).map(( position, index ) => {\n const { segmentDivider } = distributionSliderVariants({\n dragging: isDragging && dragIndex === index,\n focused: focusedDividerIndex === index,\n disabled\n });\n return (\n <div\n key={`divider-${index}`}\n className={segmentDivider()}\n style={{ left: `${position}%` }}\n tabIndex={disabled ? -1 : 0}\n role="slider"\n aria-label={`Adjust distribution between ${segments[index].name} and ${segments[index + 1].name}`}\n aria-valuemin={0}\n aria-valuemax={segments[index].value + segments[index + 1].value}\n aria-valuenow={Math.round( segments[index].value )}\n aria-valuetext={\n `${segments[index].name}: ${formatValue( segments[index].value )}, ${segments[index + 1].name}: ${formatValue( segments[index + 1].value )}`\n }\n onMouseDown={( e ) => handleMouseDown( e, index )}\n onKeyDown={( e ) => handleKeyDown( e, index )}\n onFocus={() => handleFocus( index )}\n onBlur={handleBlur}\n />\n );\n })}\n </div>\n </div>\n\n {helperText && (\n <div className={helperTextClass()}>\n {helperText}\n </div>\n )}\n </div>\n );\n }\n);\n\nDistributionSlider.displayName = "DistributionSlider";\n\n'
|
|
1908
1878
|
},
|
|
1909
1879
|
{
|
|
1910
1880
|
"name": "README.md",
|
|
@@ -1917,30 +1887,30 @@ export const checkmarkClipPath = \`polygon(
|
|
|
1917
1887
|
"name": "dropdown",
|
|
1918
1888
|
"description": "The Dropdown component provides a toggleable menu that displays a list of actions or options. It uses compound component pattern with Dropdown.Item subcomponent and @floating-ui for intelligent positioning. **When to use:** - Action menus and context menus - User profile menus - Settings and preferences menus - Navigation submenus - List of options or commands **Component Architecture:** - Built with React compound component pattern (Dropdown.Item) - Positioned using @floating-ui for automatic placement - Styled with Tailwind CSS and class-variance-authority (cva) - Keyboard navigation support (Arrow keys, ESC, Enter) - Automatic focus management",
|
|
1919
1889
|
"dependencies": [
|
|
1920
|
-
"
|
|
1890
|
+
"tailwind-variants",
|
|
1921
1891
|
"react",
|
|
1922
1892
|
"react-dom",
|
|
1923
|
-
"@floating-ui/react"
|
|
1893
|
+
"@floating-ui/react",
|
|
1894
|
+
"tailwind-merge"
|
|
1924
1895
|
],
|
|
1925
1896
|
"internalDependencies": [],
|
|
1926
1897
|
"files": [
|
|
1927
1898
|
{
|
|
1928
1899
|
"name": "index.ts",
|
|
1929
|
-
"content": 'import { Dropdown as DropdownComponent, type IDropdownProps } from "./dropdown";\nimport { DropdownItem, type IDropdownItemProps } from "./dropdownItem";\nimport { DropdownToggle, type IDropdownToggleProps } from "./dropdownToggle";\nimport { dropdownVariants
|
|
1900
|
+
"content": 'import { Dropdown as DropdownComponent, type IDropdownProps } from "./dropdown";\nimport { DropdownItem, type IDropdownItemProps } from "./dropdownItem";\nimport { DropdownToggle, type IDropdownToggleProps } from "./dropdownToggle";\nimport { dropdownVariants } from "./dropdownVariants";\n\n// Create a composite Dropdown with Item as a property\ntype TDropdownCompoundComponent = typeof DropdownComponent & {\n Item: typeof DropdownItem;\n};\n\nconst Dropdown = DropdownComponent as TDropdownCompoundComponent;\nDropdown.Item = DropdownItem;\n\nexport {\n Dropdown,\n DropdownItem,\n DropdownToggle,\n dropdownVariants\n};\n\nexport type {\n IDropdownProps,\n IDropdownItemProps,\n IDropdownToggleProps\n};'
|
|
1930
1901
|
},
|
|
1931
1902
|
{
|
|
1932
1903
|
"name": "dropdownVariants.ts",
|
|
1933
|
-
"content": 'import {
|
|
1904
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const dropdownVariants = tv({\n slots: {\n menu: "bg-white rounded border border-secondary-50 shadow-lg min-w-[300px] z-[2020] dark:bg-secondary-800 dark:border-secondary-800",\n header: "border-b border-secondary-100 dark:border-secondary-400",\n item: "px-4 text-secondary-500 cursor-pointer relative ease-in-out duration-200 dark:text-secondary-200 hover:bg-secondary-50" +\n " dark:hover:bg-secondary-500 dark:hover:text-secondary-100 dark:focus:text-secondary-300 focus:outline-none"\n },\n variants: {\n size: {\n md: { item: "py-2.5 text-base" },\n lg: { item: "py-3 text-md" }\n },\n headerType: {\n text: { header: "text-xs text-secondary-300 uppercase px-4 py-2.5 dark:text-secondary-300" },\n custom: { header: "p-4" }\n },\n disabled: {\n true: {\n item: "cursor-not-allowed bg-secondary-100/30 text-secondary-400 dark:bg-secondary-500" +\n " hover:bg-secondary-100/30 dark:hover:bg-secondary-500"\n },\n false: {}\n },\n position: {\n first: { item: "rounded-t-sm" },\n last: { item: "rounded-b-sm" },\n middle: {}\n }\n },\n defaultVariants: {\n size: "md",\n headerType: "text",\n disabled: false,\n position: "middle"\n }\n});\n'
|
|
1934
1905
|
},
|
|
1935
1906
|
{
|
|
1936
1907
|
"name": "dropdownToggle.tsx",
|
|
1937
|
-
"content": 'import * as React from "react";\nimport { cn } from "
|
|
1908
|
+
"content": 'import * as React from "react";\nimport { cn } from "tailwind-variants";\n\nexport interface IDropdownToggleProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /**\n * The content of the toggle button\n */\n children?: React.ReactNode;\n /**\n * Additional class name for the toggle button\n */\n ctaClassName?: string;\n}\n\nexport const DropdownToggle = React.forwardRef<HTMLButtonElement, IDropdownToggleProps>(\n ({ children, className, ctaClassName, ...props }, ref ) => {\n return children ? (\n <button\n aria-haspopup="menu"\n data-testid="dropdown-button"\n className={cn( className, ctaClassName )}\n ref={ref}\n {...props}\n >\n {children}\n </button>\n ) : null;\n }\n);\n\nDropdownToggle.displayName = "DropdownToggle";\n'
|
|
1938
1909
|
},
|
|
1939
1910
|
{
|
|
1940
1911
|
"name": "dropdownItem.tsx",
|
|
1941
1912
|
"content": `import * as React from "react";
|
|
1942
|
-
import {
|
|
1943
|
-
import { dropdownItemVariants } from "./dropdownVariants";
|
|
1913
|
+
import { dropdownVariants } from "./dropdownVariants";
|
|
1944
1914
|
|
|
1945
1915
|
export interface IDropdownItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
1946
1916
|
/**
|
|
@@ -2043,15 +2013,14 @@ export const DropdownItem = React.forwardRef<HTMLDivElement, IDropdownItemProps>
|
|
|
2043
2013
|
}
|
|
2044
2014
|
}, [ dropdownShow, index ]);
|
|
2045
2015
|
|
|
2016
|
+
const { item } = dropdownVariants({ size, disabled, position });
|
|
2017
|
+
|
|
2046
2018
|
return children ? (
|
|
2047
2019
|
<div
|
|
2048
2020
|
tabIndex={0}
|
|
2049
2021
|
data-testid="dropdown-item"
|
|
2050
2022
|
ref={combinedRef}
|
|
2051
|
-
className={
|
|
2052
|
-
dropdownItemVariants({ size, disabled, position }),
|
|
2053
|
-
className
|
|
2054
|
-
)}
|
|
2023
|
+
className={item({ class: className })}
|
|
2055
2024
|
onClick={handleOnClick}
|
|
2056
2025
|
onKeyDown={handleOnEnter}
|
|
2057
2026
|
{...props}
|
|
@@ -2067,7 +2036,7 @@ DropdownItem.displayName = "DropdownItem";
|
|
|
2067
2036
|
},
|
|
2068
2037
|
{
|
|
2069
2038
|
"name": "dropdown.tsx",
|
|
2070
|
-
"content": 'import React, { useState, useCallback, useEffect, forwardRef } from "react";\nimport ReactDOM from "react-dom";\nimport { useFloating, offset, flip, shift, autoUpdate } from "@floating-ui/react";\nimport { cn } from "
|
|
2039
|
+
"content": 'import React, { useState, useCallback, useEffect, forwardRef } from "react";\nimport ReactDOM from "react-dom";\nimport { useFloating, offset, flip, shift, autoUpdate } from "@floating-ui/react";\nimport { cn } from "tailwind-variants";\n\nimport { dropdownVariants } from "./dropdownVariants";\nimport { DropdownToggle } from "./dropdownToggle";\nimport { type IDropdownItemProps } from "./dropdownItem";\n\nimport { keyboardKeys } from "@constants";\n\nexport interface IDropdownProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Size of the dropdown.\n * @default md\n */\n size?: "md" | "lg";\n /**\n * Use to show the title or header\n */\n header?: React.ReactNode | string;\n /**\n * Child elements of the dropdown (typically DropdownItem components)\n */\n children?: React.ReactNode;\n /**\n * JSX to be rendered inside of dropdown button\n */\n ctaContent?: React.ReactNode;\n /**\n * Pass a reference to the element where the dropdown needs to be rendered\n */\n portalTarget?: Element;\n /**\n * Pass space separated class names to style the CTA\n */\n ctaClassName?: string;\n /**\n * Determines whether the CTA should be disabled or not\n */\n disabled?: boolean;\n /**\n * Controls whether the dropdown should close when an item is clicked\n * @default true\n */\n shouldCloseOnItemClick?: boolean;\n /**\n * Controls the open state of the dropdown (controlled component)\n */\n open?: boolean;\n /**\n * Callback function called when the dropdown should be closed\n */\n onClose?: () => void;\n /**\n * Callback function called when the dropdown toggle (CTA) is clicked (used in controlled mode)\n */\n onToggle?: () => void;\n}\n\nexport const Dropdown = forwardRef<HTMLDivElement, IDropdownProps>(\n ({\n size = "md",\n children,\n header,\n ctaContent,\n portalTarget = typeof document !== "undefined" ? document.body : undefined,\n disabled = false,\n ctaClassName,\n className,\n shouldCloseOnItemClick = true,\n open,\n onClose,\n onToggle,\n ...props\n }, ref ) => {\n const [ itemList, setItem ] = useState<React.ReactNode[] | React.ReactPortal[]>([]);\n const [ internalOpen, setInternalOpen ] = useState( false );\n\n // Determine if component is controlled or uncontrolled\n const isControlled = open !== undefined;\n const dropdownShow = isControlled ? open : internalOpen;\n\n // Floating UI hook\n const { x, y, refs, strategy, update } = useFloating({\n placement: "bottom-start",\n middleware: [ offset( 8 ), flip(), shift() ],\n whileElementsMounted: autoUpdate\n });\n\n // Function to open/toggle the dropdown\n const openDropdown = () => {\n if ( isControlled ) {\n // In controlled mode, notify parent via onToggle\n onToggle?.();\n } else {\n setInternalOpen( true );\n }\n setTimeout(() => update?.(), 0 );\n };\n\n // Function to close the dropdown\n const closeDropdown = useCallback(() => {\n if ( isControlled ) {\n onClose?.();\n } else {\n setInternalOpen( false );\n }\n }, [ isControlled, onClose ]);\n\n // Listener to close the dropdown when focus moves away from it\n const handleBlur = ( event: React.FocusEvent ) => {\n if ( !event.currentTarget.contains( event.relatedTarget as Node )) {\n closeDropdown();\n }\n };\n\n // Listener to add func to esc, up, down arrows for nav\n const dropdownKeyPress = ( event: React.KeyboardEvent ) => {\n if ( event.key === keyboardKeys.escape ) {\n closeDropdown();\n } else if ( event.key === keyboardKeys.arrowUp ) {\n if ( document.activeElement?.previousElementSibling ) {\n ( document.activeElement.previousElementSibling as HTMLDivElement ).focus();\n }\n } else if ( event.key === keyboardKeys.arrowDown ) {\n if ( document.activeElement?.nextElementSibling ) {\n ( document.activeElement.nextElementSibling as HTMLDivElement ).focus();\n }\n }\n };\n\n // Effect to calibrate children and button content\n useEffect(() => {\n if ( !children ) {\n return;\n }\n\n const itemArray: React.ReactElement<IDropdownItemProps>[] = [];\n\n React.Children.forEach( children, ( child, index: number ) => {\n // Skip null, undefined, or non-React elements\n if ( !React.isValidElement( child )) {\n return;\n }\n\n const childElement = child as React.ReactElement<IDropdownItemProps>;\n const position = index === 0\n ? "first"\n : index === React.Children.count( children ) - 1\n ? "last"\n : "middle";\n\n const clone = React.cloneElement( childElement, {\n dropdownShow,\n setShowDropdown: shouldCloseOnItemClick ? closeDropdown : undefined,\n index,\n key: index,\n size,\n position,\n shouldCloseOnItemClick\n });\n\n itemArray.push( clone );\n });\n\n setItem( itemArray );\n }, [ children, ctaContent, dropdownShow, size, shouldCloseOnItemClick, closeDropdown ]);\n\n const { menu } = dropdownVariants({ size });\n const { header: headerClass } = dropdownVariants({\n headerType: typeof header === "string" ? "text" : "custom"\n });\n\n return children ? (\n <>\n <DropdownToggle\n ref={refs.setReference}\n onClick={openDropdown}\n ctaClassName={ctaClassName}\n disabled={disabled}\n >\n {ctaContent}\n </DropdownToggle>\n {portalTarget && ReactDOM.createPortal(\n <div\n className={cn( className )}\n {...props}\n ref={ref}\n >\n <div\n ref={refs.setFloating}\n onKeyUp={dropdownKeyPress}\n style={{\n position: strategy,\n top: y ?? 0,\n left: x ?? 0,\n width: "max-content"\n }}\n onBlur={handleBlur}\n className={menu({ class: dropdownShow ? "block" : "hidden" })}\n >\n {header && (\n <div className={headerClass()}>\n {header}\n </div>\n )}\n {itemList.map(( item ) => item )}\n </div>\n </div>,\n portalTarget\n )}\n </>\n ) : null;\n }\n);\n\nDropdown.displayName = "Dropdown";\n'
|
|
2071
2040
|
},
|
|
2072
2041
|
{
|
|
2073
2042
|
"name": "README.md",
|
|
@@ -2080,8 +2049,9 @@ DropdownItem.displayName = "DropdownItem";
|
|
|
2080
2049
|
"name": "emptyState",
|
|
2081
2050
|
"description": "The EmptyState component displays a placeholder when there's no content to show, with optional actions. **When to use:** - Empty search results - No data to display - First-time user experiences - Deleted or cleared content areas - Error states **Component Architecture:** - Styled with Tailwind CSS and cva - Supports custom illustrations - Call-to-action button support - Multiple variants",
|
|
2082
2051
|
"dependencies": [
|
|
2083
|
-
"
|
|
2084
|
-
"react"
|
|
2052
|
+
"tailwind-variants",
|
|
2053
|
+
"react",
|
|
2054
|
+
"tailwind-merge"
|
|
2085
2055
|
],
|
|
2086
2056
|
"internalDependencies": [
|
|
2087
2057
|
"button"
|
|
@@ -2089,23 +2059,23 @@ DropdownItem.displayName = "DropdownItem";
|
|
|
2089
2059
|
"files": [
|
|
2090
2060
|
{
|
|
2091
2061
|
"name": "index.ts",
|
|
2092
|
-
"content": 'export {\n EmptyState,\n type IEmptyStateProps\n} from "./emptyState";\n\nexport {\n EmptyStateContent,\n type IEmptyStateContentProps\n} from "./emptyStateContent";\n\nexport {\n EmptyStateCTA,\n type IEmptyStateCTAProps\n} from "./emptyStateCta";\n\nexport {
|
|
2062
|
+
"content": 'export {\n EmptyState,\n type IEmptyStateProps\n} from "./emptyState";\n\nexport {\n EmptyStateContent,\n type IEmptyStateContentProps\n} from "./emptyStateContent";\n\nexport {\n EmptyStateCTA,\n type IEmptyStateCTAProps\n} from "./emptyStateCta";\n\nexport { emptyStateVariants } from "./emptyStateVariants";'
|
|
2093
2063
|
},
|
|
2094
2064
|
{
|
|
2095
2065
|
"name": "emptyStateVariants.ts",
|
|
2096
|
-
"content": 'import {
|
|
2066
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const emptyStateVariants = tv({\n slots: {\n base: "flex flex-col items-center justify-center space-y-6 break-words whitespace-pre-wrap overflow-hidden",\n contentWrapper: "flex flex-col items-center justify-between text-center",\n content: "space-y-6 dark:text-secondary-200",\n title: "text-2xl font-medium",\n cta: "gap-3 flex flex-wrap"\n }\n});\n'
|
|
2097
2067
|
},
|
|
2098
2068
|
{
|
|
2099
2069
|
"name": "emptyStateCta.tsx",
|
|
2100
|
-
"content": 'import * as React from "react";\nimport {
|
|
2070
|
+
"content": 'import * as React from "react";\nimport { emptyStateVariants } from "./emptyStateVariants";\n\nconst { cta: ctaClass } = emptyStateVariants();\n\nexport interface IEmptyStateCTAProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Provide custom classes for the component */\n className?: string;\n /** CTA children (e.g., buttons) */\n children?: React.ReactNode;\n}\n\nexport const EmptyStateCTA = React.forwardRef<HTMLDivElement, IEmptyStateCTAProps>(\n ({ className, children, ...props }, ref ) => {\n if ( !children ) {\n return null;\n }\n return (\n <div ref={ref} className={ctaClass({ class: className })} {...props}>\n {children}\n </div>\n );\n }\n);\n\nEmptyStateCTA.displayName = "EmptyStateCTA";\n\nexport default EmptyStateCTA;\n'
|
|
2101
2071
|
},
|
|
2102
2072
|
{
|
|
2103
2073
|
"name": "emptyStateContent.tsx",
|
|
2104
|
-
"content": 'import * as React from "react";\nimport {
|
|
2074
|
+
"content": 'import * as React from "react";\nimport { emptyStateVariants } from "./emptyStateVariants";\n\nconst { content: contentClass, title: titleClass } = emptyStateVariants();\n\nexport interface IEmptyStateContentProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Provide custom classes for the component */\n className?: string;\n /** Content title */\n title?: string;\n /** Content children */\n children?: React.ReactNode;\n}\n\nexport const EmptyStateContent = React.forwardRef<HTMLDivElement, IEmptyStateContentProps>(\n ({ className, title, children, ...props }, ref ) => {\n if ( !title && !children ) {\n return null;\n }\n return (\n <div ref={ref} className={contentClass({ class: className })} {...props}>\n {title && <h3 className={titleClass()}>{title}</h3>}\n {children}\n </div>\n );\n }\n);\n\nEmptyStateContent.displayName = "EmptyStateContent";\nexport default EmptyStateContent;\n'
|
|
2105
2075
|
},
|
|
2106
2076
|
{
|
|
2107
2077
|
"name": "emptyState.tsx",
|
|
2108
|
-
"content": 'import * as React from "react";\nimport {
|
|
2078
|
+
"content": 'import * as React from "react";\nimport {\n EmptyStateContent,\n type IEmptyStateContentProps\n} from "./emptyStateContent";\nimport {\n EmptyStateCTA,\n type IEmptyStateCTAProps\n} from "./emptyStateCta";\nimport { emptyStateVariants } from "./emptyStateVariants";\n\nconst { base, contentWrapper } = emptyStateVariants();\n\n// Import all SVGs for light mode\nimport analyticsLight from "./svgs/analytics.svg";\nimport dashboardLight from "./svgs/dashboard.svg";\nimport dataUploadLight from "./svgs/dataUpload.svg";\nimport workspacesLight from "./svgs/workspaces.svg";\nimport waitingLight from "./svgs/waiting.svg";\nimport otpLight from "./svgs/otp.svg";\nimport searchingLight from "./svgs/searching.svg";\nimport notificationLight from "./svgs/notification.svg";\nimport noFilesLight from "./svgs/noFiles.svg";\nimport noAccessLight from "./svgs/noAccess.svg";\nimport noConnectionsLight from "./svgs/noConnections.svg";\nimport forgotPasswordLight from "./svgs/forgotPassword.svg";\nimport favoriteLight from "./svgs/favorite.svg";\nimport loginLight from "./svgs/login.svg";\nimport fileUploadLight from "./svgs/fileUpload.svg";\nimport emptyDataLight from "./svgs/emptyData.svg";\nimport emptyData2Light from "./svgs/emptyData2.svg";\nimport emptyData3Light from "./svgs/emptyData3.svg";\nimport emptyData4Light from "./svgs/emptyData4.svg";\nimport datapipelineLight from "./svgs/datapipeline.svg";\n\n// Import all SVGs for dark mode\nimport analyticsDark from "./darkModeSvgs/analytics_dark.svg";\nimport dashboardDark from "./darkModeSvgs/dashboard_dark.svg";\nimport dataUploadDark from "./darkModeSvgs/dataupload_dark.svg";\nimport workspacesDark from "./darkModeSvgs/workspace_dark.svg";\nimport searchingDark from "./darkModeSvgs/searching_dark.svg";\nimport notificationDark from "./darkModeSvgs/notification_dark.svg";\nimport noAccessDark from "./darkModeSvgs/noaccess_dark.svg";\nimport noConnectionsDark from "./darkModeSvgs/noConnection_dark.svg";\nimport forgotPasswordDark from "./darkModeSvgs/forgotPassword_dark.svg";\nimport loginDark from "./darkModeSvgs/login_dark.svg";\nimport favoriteDark from "./darkModeSvgs/favorite_dark.svg";\nimport fileUploadDark from "./darkModeSvgs/fileUpload_dark.svg";\nimport emptyDark from "./darkModeSvgs/empty_dark.svg";\nimport otpDark from "./darkModeSvgs/OTP_dark.svg";\n\nconst lightSvgs = {\n analytics: analyticsLight,\n dashboard: dashboardLight,\n "data-upload": dataUploadLight,\n workspaces: workspacesLight,\n waiting: waitingLight,\n otp: otpLight,\n searching: searchingLight,\n notification: notificationLight,\n "no-files": noFilesLight,\n "no-access": noAccessLight,\n "no-connections": noConnectionsLight,\n "forgot-password": forgotPasswordLight,\n favorite: favoriteLight,\n login: loginLight,\n "file-upload": fileUploadLight,\n "empty-data": emptyDataLight,\n "empty-data-2": emptyData2Light,\n "empty-data-3": emptyData3Light,\n "empty-data-4": emptyData4Light,\n datapipeline: datapipelineLight\n};\n\nconst darkSvgs = {\n analytics: analyticsDark,\n dashboard: dashboardDark,\n "data-upload": dataUploadDark,\n workspaces: workspacesDark,\n searching: searchingDark,\n notification: notificationDark,\n "no-access": noAccessDark,\n "no-connections": noConnectionsDark,\n "forgot-password": forgotPasswordDark,\n login: loginDark,\n favorite: favoriteDark,\n "file-upload": fileUploadDark,\n "empty-data": emptyDark,\n otp: otpDark\n};\n\nexport interface IEmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Set a supported Empty State image variant */\n imageVariant?: keyof typeof lightSvgs;\n /** Alt text for the image (used only when there is no content) */\n imageAltText?: string;\n /** Custom image element, if provided it will override the variant images */\n customImage?: React.ReactElement<HTMLImageElement>;\n /** Override container styles */\n className?: string;\n /** Provide content and/or CTA children */\n children?:\n | React.ReactElement<IEmptyStateContentProps>\n | React.ReactElement<IEmptyStateCTAProps>\n | [React.ReactElement<IEmptyStateContentProps>, React.ReactElement<IEmptyStateCTAProps>];\n}\n\n// Compound component type to allow static subcomponents\ntype TEmptyStateComponent = React.ForwardRefExoticComponent<IEmptyStateProps & React.RefAttributes<HTMLDivElement>> & {\n Content: typeof EmptyStateContent;\n CTA: typeof EmptyStateCTA;\n};\n\n// Main EmptyState component\nexport const EmptyState = React.forwardRef<HTMLDivElement, IEmptyStateProps>(\n (\n {\n className,\n imageVariant = "analytics",\n imageAltText = "",\n customImage,\n children,\n ...props\n },\n ref\n ) => {\n const [ content, setContent ] = React.useState<React.ReactElement<IEmptyStateContentProps> | null>( null );\n const [ cta, setCta ] = React.useState<React.ReactElement<IEmptyStateCTAProps> | null>( null );\n\n React.useEffect(() => {\n setContent( null );\n setCta( null );\n React.Children.forEach( children, ( child ) => {\n if ( React.isValidElement<IEmptyStateContentProps>( child ) && child.type === EmptyStateContent ) {\n setContent( child );\n } else if ( React.isValidElement<IEmptyStateCTAProps>( child ) && child.type === EmptyStateCTA ) {\n setCta( child );\n }\n });\n }, [children]);\n\n // Resolve SVG srcs for given variant, fallback to analytics\n const key = imageVariant in lightSvgs ? imageVariant : "analytics";\n const srcLight = lightSvgs[key as keyof typeof lightSvgs];\n const srcDark = darkSvgs[key as keyof typeof darkSvgs] ?? srcLight;\n\n return (\n <div ref={ref} className={base({ class: className })} {...props}>\n {customImage ? (\n customImage\n ) : (\n <picture>\n <source srcSet={srcDark} media="(prefers-color-scheme: dark)" />\n <img\n className="empty-state-image"\n src={srcLight}\n alt={content ? "" : imageAltText}\n />\n </picture>\n )}\n <section className={contentWrapper()}>\n {content}\n {cta}\n </section>\n </div>\n );\n }\n) as TEmptyStateComponent;\n\nEmptyState.displayName = "EmptyState";\nEmptyState.Content = EmptyStateContent;\nEmptyState.CTA = EmptyStateCTA;\n'
|
|
2109
2079
|
},
|
|
2110
2080
|
{
|
|
2111
2081
|
"name": "README.md",
|
|
@@ -2303,7 +2273,8 @@ DropdownItem.displayName = "DropdownItem";
|
|
|
2303
2273
|
"description": "The FileUpload component provides a file selection and upload interface with drag-and-drop support, file validation, and upload progress tracking. **When to use:** - File upload forms - Image uploads - Document submissions - Profile picture uploads - Bulk file uploads **Component Architecture:** - Styled with Tailwind CSS and cva - Drag and drop support - Multiple file selection - File type and size validation - Upload progress tracking with status cards - Error handling and retry functionality",
|
|
2304
2274
|
"dependencies": [
|
|
2305
2275
|
"react",
|
|
2306
|
-
"
|
|
2276
|
+
"tailwind-variants",
|
|
2277
|
+
"tailwind-merge"
|
|
2307
2278
|
],
|
|
2308
2279
|
"internalDependencies": [
|
|
2309
2280
|
"adpIcon",
|
|
@@ -2318,11 +2289,11 @@ DropdownItem.displayName = "DropdownItem";
|
|
|
2318
2289
|
},
|
|
2319
2290
|
{
|
|
2320
2291
|
"name": "statusCardVariants.ts",
|
|
2321
|
-
"content": 'import {
|
|
2292
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const statusCardVariants = tv({\n slots: {\n card: [\n "px-4 py-2 rounded-md transition-colors duration-200 border bg-white",\n "flex items-center justify-between gap-4 w-full",\n "dark:bg-iridium dark:border-secondary-500"\n ],\n iconWrapper: "w-8 h-8 flex items-center justify-center rounded-full shrink-0 dark:bg-secondary-600",\n icon: "cursor-pointer"\n },\n variants: {\n state: {\n default: { card: "border-secondary-100 dark:border-secondary-100" },\n success: { card: "border-success-500 dark:border-success-400" },\n error: { card: "border-error-500 dark:border-error-400" },\n warning: { card: "border-warning-500 dark:border-warning-400" }\n },\n fileState: {\n primary: {\n iconWrapper: "bg-primary-50",\n icon: "text-primary-500 dark:text-primary-400"\n },\n success: {\n iconWrapper: "bg-success-50",\n icon: "text-success-500 dark:text-success-400"\n },\n error: {\n iconWrapper: "bg-error-50",\n icon: "text-error-500 dark:text-error-400"\n },\n warning: {\n iconWrapper: "bg-warning-50",\n icon: "text-warning-500 dark:text-warning-400"\n }\n },\n disabled: {\n true: {\n icon: "text-secondary-300 cursor-not-allowed hover:text-secondary-300 dark:text-secondary-400"\n }\n }\n },\n defaultVariants: {\n state: "default",\n disabled: false\n }\n});\n'
|
|
2322
2293
|
},
|
|
2323
2294
|
{
|
|
2324
2295
|
"name": "statusCard.tsx",
|
|
2325
|
-
"content": 'import * as React from "react";\nimport {
|
|
2296
|
+
"content": 'import * as React from "react";\nimport { readableFileSize } from "@utils";\nimport { ADPIcon, type TIconType } from "../adpIcon";\nimport { Progress } from "../progress";\nimport { Tooltip } from "../tooltip";\nimport { Button } from "../button";\nimport { statusCardVariants } from "./statusCardVariants";\n\nexport interface IStatusCardProps {\n fileEntry: { file: File; state: string };\n uploadState: Record<\n string,\n { error: boolean; helperText?: React.ReactNode; progress: number; uploading: boolean }\n > | null;\n maxFileSize?: number;\n disabled: boolean;\n acceptedExtensions: string[];\n removeFile: ( fileName: string ) => void;\n}\n\nexport const StatusCard = React.forwardRef<\n HTMLDivElement,\n IStatusCardProps\n>((\n {\n fileEntry,\n uploadState,\n maxFileSize,\n disabled,\n acceptedExtensions,\n removeFile\n },\n ref\n) => {\n const { file, state } = fileEntry;\n const { progress = null, helperText = "", error = false, uploading = false } =\n uploadState?.[file.name] ?? {};\n\n // Determine file state for icon and progress\n const fileState = React.useMemo<"primary" | "warning" | "error" | "success">(\n () => {\n const statusObj = uploadState?.[file.name];\n if ( !statusObj ) {\n return state as "primary" | "warning";\n }\n if ( statusObj.error ) {\n return "error";\n }\n if ( statusObj.progress === 100 ) {\n return "success";\n }\n return state as "primary" | "warning";\n },\n [ file.name, state, uploadState ]\n );\n\n // Map primary to default variant\n const cardState =\n fileState === "primary"\n ? "default"\n : ( fileState as "default" | "warning" | "error" | "success" );\n\n const iconTypes: Record<string, TIconType> = {\n default: "document-upload",\n primary: "document-upload",\n success: "check-circle-filled",\n error: "times-circle-filled",\n warning: "danger"\n };\n\n const helperIcon = !acceptedExtensions.includes(\n `.${file.name.split( "." ).pop()?.toLowerCase()}`\n ) || ( maxFileSize && file.size > maxFileSize )\n ? "alert-filled"\n : error\n ? "times-circle-filled"\n : progress === 100\n ? "check-circle-filled"\n : uploading\n ? "spinner"\n : null;\n\n const helperMessage = (\n <>\n {!helperText && helperIcon && (\n <ADPIcon\n icon={helperIcon}\n className={\n !acceptedExtensions.includes(\n `.${file.name.split( "." ).pop()?.toLowerCase()}`\n ) || ( maxFileSize && file.size > maxFileSize )\n ? "text-warning-500 dark:text-warning-400"\n : progress === 100\n ? "text-success-400 dark:text-success-400"\n : error\n ? "text-error-400 dark:text-error-400"\n : undefined\n }\n size="xs"\n spin={\n !acceptedExtensions.includes(\n `.${file.name.split( "." ).pop()?.toLowerCase()}`\n ) || ( maxFileSize && file.size > maxFileSize )\n ? false\n : uploading\n }\n />\n )}\n <span>\n {!acceptedExtensions.includes(\n `.${file.name.split( "." ).pop()?.toLowerCase()}`\n )\n ? "The file type is not accepted"\n : maxFileSize && file.size > maxFileSize\n ? `The size of the file is more than ${readableFileSize(\n maxFileSize\n )}`\n : progress === 100\n ? "File uploaded successfully"\n : error\n ? helperText\n : ( progress ?? 0 ) < 100 && uploading\n ? "Uploading file..."\n : helperText}\n </span>\n </>\n );\n\n const { card, iconWrapper, icon } = statusCardVariants({ state: cardState, fileState, disabled });\n\n return (\n <div ref={ref}>\n <div className={card()}>\n <div className={iconWrapper()}>\n <ADPIcon\n className={icon()}\n size="xs"\n icon={iconTypes[fileState] as TIconType}\n />\n </div>\n <div className="flex flex-col w-full me-4 md:me-8">\n <div className="flex gap-1 text-sm mb-1">\n <Tooltip\n title="File Name"\n trigger={\n <span className="text-sm font-medium text-secondary-500 dark:text-secondary-50">\n {file.name.length > 30\n ? `${file.name.substring( 0, 30 )}...`\n : file.name}\n </span>\n }\n >\n {file.name}\n </Tooltip>\n </div>\n {fileState !== "error" && ( !maxFileSize || file.size <= maxFileSize ) && fileState !== "warning" && (\n <Progress\n variant="linear"\n value={progress ?? 0}\n color={\n error\n ? "error"\n : state === "warning"\n ? "primary"\n : progress === 100\n ? "success"\n : "primary"\n }\n showPercentage\n />\n )}\n </div>\n <Button\n variant="text"\n prefixIcon="cross"\n size="xs"\n disabled={disabled}\n className="text-gray-800 dark:text-gray-300"\n title={`Remove ${file.name}`}\n onClick={() => removeFile( file.name )}\n />\n </div>\n <p className="text-secondary-400 text-sm mt-2 flex items-center gap-2 dark:text-secondary-50">\n {helperMessage}\n </p>\n </div>\n );\n});\n\nStatusCard.displayName = "StatusCard";\n'
|
|
2326
2297
|
},
|
|
2327
2298
|
{
|
|
2328
2299
|
"name": "index.ts",
|
|
@@ -2330,11 +2301,11 @@ DropdownItem.displayName = "DropdownItem";
|
|
|
2330
2301
|
},
|
|
2331
2302
|
{
|
|
2332
2303
|
"name": "fileUploadVariants.ts",
|
|
2333
|
-
"content": 'import {
|
|
2304
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const fileUploadVariants = tv({\n slots: {\n dropArea: [\n "bg-white overflow-auto border-dashed",\n "h-60 border border-gray-300 rounded-md",\n "p-0 flex flex-col items-center justify-center gap-4 w-full",\n "cursor-pointer dark:text-secondary-200 dark:border-secondary-500 dark:bg-iridium"\n ],\n iconWrapper: "w-13 h-13 flex items-center justify-center rounded-full dark:bg-secondary-600",\n icon: "cursor-pointer"\n },\n variants: {\n state: {\n default: {\n iconWrapper: "bg-primary-50",\n icon: "text-primary-500 dark:text-primary-400"\n },\n success: {\n iconWrapper: "bg-success-50",\n icon: "text-success-500 bg-success-50 dark:text-success-400 dark:bg-secondary-600"\n },\n warning: {\n iconWrapper: "bg-warning-50",\n icon: "text-warning-500 bg-warning-50 dark:text-warning-400 dark:bg-secondary-600"\n },\n error: {\n iconWrapper: "bg-error-50",\n icon: "text-error-500 bg-error-50 dark:text-error-400 dark:bg-secondary-600"\n }\n },\n dragging: {\n true: {\n dropArea: "bg-primary-50 dark:bg-secondary-700"\n }\n },\n disabled: {\n true: {\n dropArea: "bg-secondary-50",\n iconWrapper: "bg-secondary-100",\n icon: "text-secondary-300 cursor-not-allowed hover:text-secondary-300 dark:text-secondary-400 dark:bg-secondary-600"\n }\n }\n },\n defaultVariants: {\n state: "default",\n dragging: false,\n disabled: false\n }\n});\n'
|
|
2334
2305
|
},
|
|
2335
2306
|
{
|
|
2336
2307
|
"name": "fileUpload.tsx",
|
|
2337
|
-
"content": 'import * as React from "react";\nimport { cn, debounce, readableFileSize } from "@utils";\nimport { ADPIcon, type TIconType } from "../adpIcon";\nimport { Button } from "../button";\nimport { StatusCard } from "./statusCard";\nimport { fileUploadVariants } from "./fileUploadVariants";\n\n/**\n * FileUpload component allows drag & drop or click-to-upload files with custom handling\n * and displays upload progress and errors.\n */\nexport interface IFileUploadProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Array of accepted file extensions */\n acceptedExtensions: string[];\n /** Custom message displayed in the drop area */\n dropAreaCustomMessage?: React.ReactNode;\n /** State tracking the upload progress and error messages for each file */\n uploadState: Record<string, { error: boolean; helperText?: React.ReactNode; progress: number; uploading: boolean }> | null;\n /** Determines if multiple file uploads are allowed */\n multiple?: boolean;\n /** Disables the upload functionality */\n disabled?: boolean;\n /** Additional informational text to display about file requirements or upload instructions */\n infoText?: React.ReactNode;\n /** Maximum allowed file size in bytes */\n maxFileSize?: number;\n /** Function to retry uploading files that failed */\n retryFailedUpload?: () => void;\n /** Function to reset the upload state */\n resetUploadState: () => void;\n /** Function triggered after file validation to handle the upload logic */\n handleUpload: ( file: File | File[]) => void;\n}\n\nexport const FileUpload = React.forwardRef<HTMLDivElement, IFileUploadProps>(({\n acceptedExtensions,\n dropAreaCustomMessage,\n uploadState = null,\n multiple = true,\n disabled = false,\n infoText = null,\n maxFileSize,\n retryFailedUpload,\n resetUploadState,\n handleUpload,\n className,\n ...props\n}, ref ) => {\n const hiddenFileInputRef = React.useRef<HTMLInputElement | null>( null );\n const [ fileEntries, setFileEntries ] = React.useState<{ file: File; state: string }[]>([]);\n const [ dragging, setDragging ] = React.useState( false );\n const existingFileNames = fileEntries.map(( entry ) => entry.file.name );\n\n const iconTypes: Record<string, TIconType> = {\n default: "document-upload",\n success: "check-circle-filled",\n error: "times-circle-filled",\n warning: "danger"\n };\n\n const debouncedSetDragging = React.useMemo(\n () => debounce(( value: boolean ) => setDragging( value ), 100 ),\n []\n );\n\n const validateFiles = React.useCallback(\n ( files: File[]) => {\n return files.map(( file ) => {\n const ext = `.${file.name.split( "." ).pop()?.toLowerCase()}`;\n const hasValidExtension = acceptedExtensions.includes( ext );\n const hasValidSize = !maxFileSize || file.size <= maxFileSize;\n\n if ( existingFileNames.includes( file.name )) {\n setFileEntries(( prev ) => prev.filter(( e ) => e.file.name !== file.name ));\n }\n\n const state = hasValidExtension && hasValidSize ? "primary" : "warning";\n return { file, state };\n });\n },\n [ acceptedExtensions, existingFileNames, maxFileSize ]\n );\n\n const updateFileList = React.useCallback(\n ( validated: { file: File; state: string }[]) => {\n setFileEntries(( prev ) => {\n const list = multiple\n ? [ ...prev.filter(( e ) => e.state !== "error" ), ...validated ]\n : validated;\n return list;\n });\n queueMicrotask(() => {\n const valid = validated.filter(( f ) => f.state === "primary" ).map(( f ) => f.file );\n if ( valid.length ) {\n handleUpload( multiple ? valid : valid[0]);\n }\n });\n },\n [ multiple, handleUpload ]\n );\n\n const handleFileSelection = ( e: React.ChangeEvent<HTMLInputElement> ) => {\n const files = Array.from( e.target.files || []);\n updateFileList( validateFiles( files ));\n if ( hiddenFileInputRef.current ) {\n hiddenFileInputRef.current.value = "";\n }\n };\n\n const handleDrop = ( e: React.DragEvent<HTMLDivElement> ) => {\n e.preventDefault();\n setDragging( false );\n if ( !disabled ) {\n let dropped = Array.from( e.dataTransfer.files );\n if ( !multiple && dropped.length > 1 ) {\n dropped = [dropped[0]];\n }\n updateFileList( validateFiles( dropped ));\n }\n };\n\n const variant = React.useMemo<"default" | "success" | "error" | "warning">(() => {\n if ( !fileEntries.length ) {\n return "default";\n }\n const stats = fileEntries.reduce(\n ( acc, f ) => {\n if ( f.state === "error" || uploadState?.[f.file.name]?.error ) {\n acc.hasErrors = true;\n }\n if ( f.state === "warning" ) {\n acc.hasWarnings = true;\n }\n if ( uploadState?.[f.file.name]?.progress !== 100 ) {\n acc.allUploaded = false;\n }\n return acc;\n },\n { hasErrors: false, hasWarnings: false, allUploaded: true }\n );\n if ( stats.hasErrors ) {\n return "error";\n }\n if ( stats.hasWarnings ) {\n return "warning";\n }\n if ( stats.allUploaded ) {\n return "success";\n }\n return "default";\n }, [ fileEntries, uploadState ]);\n\n const { failedUploadsCount, invalidExtensionCount, exceededFileSizeCount } = React.useMemo(() =>\n fileEntries.reduce(\n ( c, entry ) => {\n if ( uploadState?.[entry.file.name]?.error ) {\n c.failedUploadsCount++;\n }\n const ext = `.${entry.file.name.split( "." ).pop()?.toLowerCase()}`;\n if ( !acceptedExtensions.includes( ext )) {\n c.invalidExtensionCount++;\n }\n if ( maxFileSize && entry.file.size > maxFileSize ) {\n c.exceededFileSizeCount++;\n }\n return c;\n },\n { failedUploadsCount: 0, invalidExtensionCount: 0, exceededFileSizeCount: 0 }\n ),\n [ fileEntries, uploadState, acceptedExtensions, maxFileSize ]\n );\n\n const removeInvalidFiles = () => setFileEntries(( prev ) => prev.filter(( e ) => e.state !== "warning" ));\n\n const removeFile = React.useCallback(\n ( name: string ) => {\n setFileEntries(( prev ) => prev.filter(( e ) => e.file.name !== name ));\n resetUploadState();\n },\n [resetUploadState]\n );\n\n return (\n <>\n <div\n ref={ref}\n data-testid="upload-drop-area"\n className={cn( fileUploadVariants({ dragging, disabled }), className )}\n onDragOver={( e ) => {\n e.preventDefault();\n e.dataTransfer.dropEffect = "copy";\n }}\n onDragEnter={( e ) => {\n e.preventDefault();\n if ( !disabled ) {\n debouncedSetDragging( true );\n }\n }}\n onDragLeave={( e ) => {\n e.preventDefault();\n debouncedSetDragging( false );\n }}\n onDrop={handleDrop}\n onClick={() => {\n if ( !disabled && hiddenFileInputRef.current ) {\n hiddenFileInputRef.current.click();\n }\n }}\n {...props}\n >\n <div\n className={cn(\n "w-13 h-13 flex items-center justify-center rounded-full dark:bg-secondary-600",\n variant === "default" && "bg-primary-50",\n variant === "success" && "bg-success-50",\n variant === "warning" && "bg-warning-50",\n variant === "error" && "bg-error-50",\n disabled && "bg-secondary-100"\n )}\n >\n <ADPIcon\n size="lg"\n icon={iconTypes[variant]}\n className={cn(\n "cursor-pointer",\n disabled\n ? "text-secondary-300 cursor-not-allowed hover:text-secondary-300 dark:text-secondary-400 dark:bg-secondary-600"\n : variant === "default"\n ? "text-primary-500 dark:text-primary-400"\n : variant === "success"\n ? "text-success-500 bg-success-50 dark:text-success-400 dark:bg-secondary-600"\n : variant === "error"\n ? "text-error-500 bg-error-50 dark:text-error-400 dark:bg-secondary-600"\n : "text-warning-500 bg-warning-50 dark:text-warning-400 dark:bg-secondary-600"\n )}\n />\n </div>\n <div className="text-secondary-500 text-center text-md md:text-xl font-regular font-semibold dark:text-secondary-50">\n {dropAreaCustomMessage ?? (\n <>\n Drag & Drop file here\n <p className="text-sm md:text-md text-secondary-400 font-normal font-regular dark:text-secondary-50">\n or click to browse{maxFileSize && <span> ({readableFileSize( maxFileSize )} Max)</span>}\n </p>\n <p className="text-sm md:text-md text-secondary-400 font-normal font-regular dark:text-secondary-50">\n Supported file types: {acceptedExtensions.map(( ext ) => ext.replace( /^\\./, "" )).join( ", " )}\n </p>\n </>\n )}\n </div>\n </div>\n <input\n ref={hiddenFileInputRef}\n data-testid="file-upload-input"\n type="file"\n multiple={multiple}\n onChange={handleFileSelection}\n accept={acceptedExtensions.join( "," )}\n className="hidden"\n />\n <div className="text-secondary-400 text-xs font-normal mt-3 flex flex-col items-start justify-start gap-1 dark:text-secondary-400">\n {infoText}\n {failedUploadsCount > 0 && (\n <p className="flex items-center justify-center">\n <span className="me-1">{`${failedUploadsCount} upload(s) failed. `}</span>\n {retryFailedUpload && <Button onClick={retryFailedUpload} variant="text" color="primary" size="sm" prefixIcon="sync">Retry</Button>}\n </p>\n )}\n {( invalidExtensionCount > 0 || exceededFileSizeCount > 0 ) && multiple && (\n <p className="flex items-center justify-center">\n <span className="me-1">\n {invalidExtensionCount > 0 && `${invalidExtensionCount} file(s) have invalid extensions `}\n {invalidExtensionCount > 0 && exceededFileSizeCount > 0 && "and "}\n {exceededFileSizeCount > 0 && `${exceededFileSizeCount} file(s) exceeded the file size limit. `}\n </span>\n <Button onClick={removeInvalidFiles} variant="text" color="error" size="sm" prefixIcon="delete">Remove</Button>\n </p>\n )}\n </div>\n {fileEntries.length > 0 && (\n <div className="w-full flex flex-col gap-4 mt-4">\n {fileEntries.map(( entry ) => (\n <StatusCard\n key={entry.file.name}\n fileEntry={entry}\n uploadState={uploadState}\n removeFile={removeFile}\n maxFileSize={maxFileSize}\n disabled={disabled}\n acceptedExtensions={acceptedExtensions}\n />\n ))}\n </div>\n )}\n </>\n );\n});\n\nFileUpload.displayName = "FileUpload";\n'
|
|
2308
|
+
"content": 'import * as React from "react";\n\nimport { debounce, readableFileSize } from "@utils";\nimport { ADPIcon, type TIconType } from "../adpIcon";\nimport { Button } from "../button";\nimport { StatusCard } from "./statusCard";\nimport { fileUploadVariants } from "./fileUploadVariants";\n\n/**\n * FileUpload component allows drag & drop or click-to-upload files with custom handling\n * and displays upload progress and errors.\n */\nexport interface IFileUploadProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Array of accepted file extensions */\n acceptedExtensions: string[];\n /** Custom message displayed in the drop area */\n dropAreaCustomMessage?: React.ReactNode;\n /** State tracking the upload progress and error messages for each file */\n uploadState: Record<string, { error: boolean; helperText?: React.ReactNode; progress: number; uploading: boolean }> | null;\n /** Determines if multiple file uploads are allowed */\n multiple?: boolean;\n /** Disables the upload functionality */\n disabled?: boolean;\n /** Additional informational text to display about file requirements or upload instructions */\n infoText?: React.ReactNode;\n /** Maximum allowed file size in bytes */\n maxFileSize?: number;\n /** Function to retry uploading files that failed */\n retryFailedUpload?: () => void;\n /** Function to reset the upload state */\n resetUploadState: () => void;\n /** Function triggered after file validation to handle the upload logic */\n handleUpload: ( file: File | File[]) => void;\n}\n\nexport const FileUpload = React.forwardRef<HTMLDivElement, IFileUploadProps>(({\n acceptedExtensions,\n dropAreaCustomMessage,\n uploadState = null,\n multiple = true,\n disabled = false,\n infoText = null,\n maxFileSize,\n retryFailedUpload,\n resetUploadState,\n handleUpload,\n className,\n ...props\n}, ref ) => {\n const hiddenFileInputRef = React.useRef<HTMLInputElement | null>( null );\n const [ fileEntries, setFileEntries ] = React.useState<{ file: File; state: string }[]>([]);\n const [ dragging, setDragging ] = React.useState( false );\n const existingFileNames = fileEntries.map(( entry ) => entry.file.name );\n\n const iconTypes: Record<string, TIconType> = {\n default: "document-upload",\n success: "check-circle-filled",\n error: "times-circle-filled",\n warning: "danger"\n };\n\n const debouncedSetDragging = React.useMemo(\n () => debounce(( value: boolean ) => setDragging( value ), 100 ),\n []\n );\n\n const validateFiles = React.useCallback(\n ( files: File[]) => {\n return files.map(( file ) => {\n const ext = `.${file.name.split( "." ).pop()?.toLowerCase()}`;\n const hasValidExtension = acceptedExtensions.includes( ext );\n const hasValidSize = !maxFileSize || file.size <= maxFileSize;\n\n if ( existingFileNames.includes( file.name )) {\n setFileEntries(( prev ) => prev.filter(( e ) => e.file.name !== file.name ));\n }\n\n const state = hasValidExtension && hasValidSize ? "primary" : "warning";\n return { file, state };\n });\n },\n [ acceptedExtensions, existingFileNames, maxFileSize ]\n );\n\n const updateFileList = React.useCallback(\n ( validated: { file: File; state: string }[]) => {\n setFileEntries(( prev ) => {\n const list = multiple\n ? [ ...prev.filter(( e ) => e.state !== "error" ), ...validated ]\n : validated;\n return list;\n });\n queueMicrotask(() => {\n const valid = validated.filter(( f ) => f.state === "primary" ).map(( f ) => f.file );\n if ( valid.length ) {\n handleUpload( multiple ? valid : valid[0]);\n }\n });\n },\n [ multiple, handleUpload ]\n );\n\n const handleFileSelection = ( e: React.ChangeEvent<HTMLInputElement> ) => {\n const files = Array.from( e.target.files || []);\n updateFileList( validateFiles( files ));\n if ( hiddenFileInputRef.current ) {\n hiddenFileInputRef.current.value = "";\n }\n };\n\n const handleDrop = ( e: React.DragEvent<HTMLDivElement> ) => {\n e.preventDefault();\n setDragging( false );\n if ( !disabled ) {\n let dropped = Array.from( e.dataTransfer.files );\n if ( !multiple && dropped.length > 1 ) {\n dropped = [dropped[0]];\n }\n updateFileList( validateFiles( dropped ));\n }\n };\n\n const variant = React.useMemo<"default" | "success" | "error" | "warning">(() => {\n if ( !fileEntries.length ) {\n return "default";\n }\n const stats = fileEntries.reduce(\n ( acc, f ) => {\n if ( f.state === "error" || uploadState?.[f.file.name]?.error ) {\n acc.hasErrors = true;\n }\n if ( f.state === "warning" ) {\n acc.hasWarnings = true;\n }\n if ( uploadState?.[f.file.name]?.progress !== 100 ) {\n acc.allUploaded = false;\n }\n return acc;\n },\n { hasErrors: false, hasWarnings: false, allUploaded: true }\n );\n if ( stats.hasErrors ) {\n return "error";\n }\n if ( stats.hasWarnings ) {\n return "warning";\n }\n if ( stats.allUploaded ) {\n return "success";\n }\n return "default";\n }, [ fileEntries, uploadState ]);\n\n const { failedUploadsCount, invalidExtensionCount, exceededFileSizeCount } = React.useMemo(() =>\n fileEntries.reduce(\n ( c, entry ) => {\n if ( uploadState?.[entry.file.name]?.error ) {\n c.failedUploadsCount++;\n }\n const ext = `.${entry.file.name.split( "." ).pop()?.toLowerCase()}`;\n if ( !acceptedExtensions.includes( ext )) {\n c.invalidExtensionCount++;\n }\n if ( maxFileSize && entry.file.size > maxFileSize ) {\n c.exceededFileSizeCount++;\n }\n return c;\n },\n { failedUploadsCount: 0, invalidExtensionCount: 0, exceededFileSizeCount: 0 }\n ),\n [ fileEntries, uploadState, acceptedExtensions, maxFileSize ]\n );\n\n const removeInvalidFiles = () => setFileEntries(( prev ) => prev.filter(( e ) => e.state !== "warning" ));\n\n const removeFile = React.useCallback(\n ( name: string ) => {\n setFileEntries(( prev ) => prev.filter(( e ) => e.file.name !== name ));\n resetUploadState();\n },\n [resetUploadState]\n );\n\n const { dropArea, iconWrapper, icon: iconVariantClass } = React.useMemo(\n () => fileUploadVariants({ state: variant, dragging, disabled }),\n [ variant, dragging, disabled ]\n );\n\n return (\n <>\n <div\n ref={ref}\n data-testid="upload-drop-area"\n className={dropArea({ class: className })}\n onDragOver={( e ) => {\n e.preventDefault();\n e.dataTransfer.dropEffect = "copy";\n }}\n onDragEnter={( e ) => {\n e.preventDefault();\n if ( !disabled ) {\n debouncedSetDragging( true );\n }\n }}\n onDragLeave={( e ) => {\n e.preventDefault();\n debouncedSetDragging( false );\n }}\n onDrop={handleDrop}\n onClick={() => {\n if ( !disabled && hiddenFileInputRef.current ) {\n hiddenFileInputRef.current.click();\n }\n }}\n {...props}\n >\n <div className={iconWrapper()}>\n <ADPIcon\n size="lg"\n icon={iconTypes[variant]}\n className={iconVariantClass()}\n />\n </div>\n <div className="text-secondary-500 text-center text-md md:text-xl font-regular font-semibold dark:text-secondary-50">\n {dropAreaCustomMessage ?? (\n <>\n Drag & Drop file here\n <p className="text-sm md:text-md text-secondary-400 font-normal font-regular dark:text-secondary-50">\n or click to browse{maxFileSize && <span> ({readableFileSize( maxFileSize )} Max)</span>}\n </p>\n <p className="text-sm md:text-md text-secondary-400 font-normal font-regular dark:text-secondary-50">\n Supported file types: {acceptedExtensions.map(( ext ) => ext.replace( /^\\./, "" )).join( ", " )}\n </p>\n </>\n )}\n </div>\n </div>\n <input\n ref={hiddenFileInputRef}\n data-testid="file-upload-input"\n type="file"\n multiple={multiple}\n onChange={handleFileSelection}\n accept={acceptedExtensions.join( "," )}\n className="hidden"\n />\n <div className="text-secondary-400 text-xs font-normal mt-3 flex flex-col items-start justify-start gap-1 dark:text-secondary-400">\n {infoText}\n {failedUploadsCount > 0 && (\n <p className="flex items-center justify-center">\n <span className="me-1">{`${failedUploadsCount} upload(s) failed. `}</span>\n {retryFailedUpload && <Button onClick={retryFailedUpload} variant="text" color="primary" size="sm" prefixIcon="sync">Retry</Button>}\n </p>\n )}\n {( invalidExtensionCount > 0 || exceededFileSizeCount > 0 ) && multiple && (\n <p className="flex items-center justify-center">\n <span className="me-1">\n {invalidExtensionCount > 0 && `${invalidExtensionCount} file(s) have invalid extensions `}\n {invalidExtensionCount > 0 && exceededFileSizeCount > 0 && "and "}\n {exceededFileSizeCount > 0 && `${exceededFileSizeCount} file(s) exceeded the file size limit. `}\n </span>\n <Button onClick={removeInvalidFiles} variant="text" color="error" size="sm" prefixIcon="delete">Remove</Button>\n </p>\n )}\n </div>\n {fileEntries.length > 0 && (\n <div className="w-full flex flex-col gap-4 mt-4">\n {fileEntries.map(( entry ) => (\n <StatusCard\n key={entry.file.name}\n fileEntry={entry}\n uploadState={uploadState}\n removeFile={removeFile}\n maxFileSize={maxFileSize}\n disabled={disabled}\n acceptedExtensions={acceptedExtensions}\n />\n ))}\n </div>\n )}\n </>\n );\n});\n\nFileUpload.displayName = "FileUpload";\n'
|
|
2338
2309
|
},
|
|
2339
2310
|
{
|
|
2340
2311
|
"name": "README.md",
|
|
@@ -2347,22 +2318,23 @@ DropdownItem.displayName = "DropdownItem";
|
|
|
2347
2318
|
"name": "loader",
|
|
2348
2319
|
"description": "The Loader component displays a loading indicator for async operations. **When to use:** - Page loading states - Data fetching - Form submissions - Content loading - Async operations **Component Architecture:** - Styled with Tailwind CSS and cva - Multiple size variants - Overlay support - Custom messages",
|
|
2349
2320
|
"dependencies": [
|
|
2350
|
-
"
|
|
2351
|
-
"react"
|
|
2321
|
+
"tailwind-variants",
|
|
2322
|
+
"react",
|
|
2323
|
+
"tailwind-merge"
|
|
2352
2324
|
],
|
|
2353
2325
|
"internalDependencies": [],
|
|
2354
2326
|
"files": [
|
|
2355
2327
|
{
|
|
2356
2328
|
"name": "loaderVariants.ts",
|
|
2357
|
-
"content": 'import {
|
|
2329
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const loaderVariants = tv({\n slots: {\n base: "inline-flex gap-4 flex-col justify-center items-center min-w-min text-primary-500 dark:text-primary-400",\n label: "font-medium text-secondary-500 dark:text-secondary-50"\n },\n variants: {\n variant: {\n filled: {},\n stroked: {}\n },\n size: {\n sm: { base: "text-base", label: "text-sm" },\n md: { base: "text-md", label: "text-base" },\n lg: { base: "text-lg", label: "text-lg" }\n }\n },\n defaultVariants: {\n variant: "filled",\n size: "md"\n }\n});\n'
|
|
2358
2330
|
},
|
|
2359
2331
|
{
|
|
2360
2332
|
"name": "loader.tsx",
|
|
2361
|
-
"content": 'import * as React from "react";\nimport {
|
|
2333
|
+
"content": 'import * as React from "react";\n\nimport { loaderVariants } from "./loaderVariants";\n\n/**\n * Loader component for showing loading states.\n */\nexport interface ILoaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Variant of the Loader.\n * @default filled\n */\n variant?: "filled" | "stroked";\n /**\n * Size of the Loader.\n * @default md\n */\n size?: "sm" | "md" | "lg";\n /**\n * Regular static label.\n */\n label?: React.ReactNode;\n}\n\n// Define IconSizeMapping as an object\nconst IconSizeMapping: Record<ILoaderProps["size"] & string, 32 | 48 | 56> = {\n sm: 32,\n md: 48,\n lg: 56\n};\n\n/**\n * Loader component for showing loading states.\n */\nexport const Loader = React.forwardRef<HTMLDivElement, ILoaderProps>(\n ({\n className,\n variant = "filled",\n size = "md",\n label,\n ...props\n }, ref ) => {\n const { base, label: labelClass } = loaderVariants({ variant, size });\n\n return (\n <div\n ref={ref}\n className={base({ class: className })}\n {...props}\n >\n <svg\n width={IconSizeMapping[size]}\n height={IconSizeMapping[size]}\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n className="animate-[spin_0.9s_linear_infinite] text-primary-500 dark:text-primary-400"\n >\n {variant === "filled" ? (\n <path\n d={`M12 1.5C13.8404 1.5 15.6484 1.9837 17.2429 2.90264C18.8374 3.82157 20.1624 5.14345 21.0851 6.73581C22.0077 \n 8.32816 22.4957 10.1351 22.5 11.9754C22.5043 13.8158 22.0248 15.6249 21.1096 17.2216C20.1944 18.8182 18.8756 \n 20.1463 17.2855 21.0727C15.6953 21.9991 13.8895 22.4913 12.0492 22.4999C10.2089 22.5085 8.39858 22.0333 \n 6.79978 21.1218C5.20098 20.2104 3.86982 18.8947 2.9397 17.3067`}\n stroke="currentColor"\n strokeWidth="3"\n strokeLinecap="round"\n />\n ) : (\n <path\n d={`M12 1.75C13.346 1.75 14.6789 2.01512 15.9225 2.53023C17.1661 3.04535 18.296 3.80036 19.2478 4.75216C20.1996 \n 5.70396 20.9547 6.83391 21.4698 8.0775C21.9849 9.32108 22.25 10.654 22.25 12C22.25 13.3461 21.9849 14.6789 \n 21.4698 15.9225C20.9547 17.1661 20.1996 18.296 19.2478 19.2478C18.296 20.1996 17.1661 20.9547 15.9225 \n 21.4698C14.6789 21.9849 13.346 22.25 12 22.25C10.6539 22.25 9.32108 21.9849 8.07749 21.4698C6.8339 20.9547 \n 5.70395 20.1996 4.75215 19.2478C3.80035 18.296 3.04534 17.1661 2.53023 15.9225C2.01512 14.6789 1.75 13.346 \n 1.75 12C1.75 10.6539 2.01513 9.32108 2.53024 8.07749C3.04535 6.8339 3.80036 5.70395 4.75216 4.75215C5.70396 \n 3.80035 6.83391 3.04534 8.0775 2.53023C9.32109 2.01512 10.654 1.75 12 1.75L12 1.75Z`}\n stroke="currentColor"\n strokeWidth={size === "sm" ? "1.4" : "2.5"}\n strokeDasharray="1 3"\n />\n )}\n </svg>\n {label && (\n <span className={labelClass()}>\n {label}\n </span>\n )}\n </div>\n );\n }\n);\n\nLoader.displayName = "Loader";\n'
|
|
2362
2334
|
},
|
|
2363
2335
|
{
|
|
2364
2336
|
"name": "index.ts",
|
|
2365
|
-
"content": 'export { Loader, type ILoaderProps } from "./loader";\nexport { loaderVariants
|
|
2337
|
+
"content": 'export { Loader, type ILoaderProps } from "./loader";\nexport { loaderVariants } from "./loaderVariants";'
|
|
2366
2338
|
},
|
|
2367
2339
|
{
|
|
2368
2340
|
"name": "README.md",
|
|
@@ -2375,10 +2347,11 @@ DropdownItem.displayName = "DropdownItem";
|
|
|
2375
2347
|
"name": "modal",
|
|
2376
2348
|
"description": "The Modal component creates a dialog overlay that appears above the page content using React Portal, requiring user interaction before returning to the main interface. It uses a compound component pattern with Header and Body subcomponents. **When to use:** - Confirmation dialogs that require user decisions - Forms that need focused attention without navigation - Alert messages and important notifications - Displaying detailed information without leaving the current page - Multi-step workflows that require isolation **Component Architecture:** - Built with React compound component pattern (Modal.Header, Modal.Body) - Rendered via React Portal for proper z-index layering - Uses react-focus-on for focus management and accessibility - Styled with Tailwind CSS and class-variance-authority (cva) - Supports ESC key to close and focus trapping - Optional close button and RTL support",
|
|
2377
2349
|
"dependencies": [
|
|
2378
|
-
"
|
|
2350
|
+
"tailwind-variants",
|
|
2379
2351
|
"react",
|
|
2380
2352
|
"react-dom",
|
|
2381
|
-
"react-focus-on"
|
|
2353
|
+
"react-focus-on",
|
|
2354
|
+
"tailwind-merge"
|
|
2382
2355
|
],
|
|
2383
2356
|
"internalDependencies": [
|
|
2384
2357
|
"button"
|
|
@@ -2386,15 +2359,15 @@ DropdownItem.displayName = "DropdownItem";
|
|
|
2386
2359
|
"files": [
|
|
2387
2360
|
{
|
|
2388
2361
|
"name": "modalVariants.ts",
|
|
2389
|
-
"content": 'import {
|
|
2362
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const modalVariants = tv({\n slots: {\n container: [\n "justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 outline-none focus:outline-none",\n "z-[2000] bg-secondary-100/50"\n ],\n modal: [\n "rounded shadow-md max-w-[calc(100vw-2.5rem)] max-h-[calc(100vh-2.5rem)]",\n "p-6 overflow-hidden relative font-regular",\n "bg-white dark:bg-iridium dark:border dark:border-secondary-500"\n ]\n },\n variants: {\n headerless: {\n true: { modal: "pt-9" }\n }\n },\n defaultVariants: {\n headerless: false\n }\n});\n'
|
|
2390
2363
|
},
|
|
2391
2364
|
{
|
|
2392
2365
|
"name": "modal.tsx",
|
|
2393
|
-
"content": 'import * as React from "react";\nimport ReactDOM from "react-dom";\nimport { FocusOn } from "react-focus-on";\nimport { cn } from "
|
|
2366
|
+
"content": 'import * as React from "react";\nimport ReactDOM from "react-dom";\nimport { FocusOn } from "react-focus-on";\nimport { cn } from "tailwind-variants";\nimport { Button } from "../button";\nimport { modalVariants } from "./modalVariants";\n\nexport interface IModalProps {\n /**\n * Control modal\'s visibility\n */\n showModal: boolean;\n /**\n * Modal content\n */\n children:\n | React.ReactElement<IModalHeaderProps>\n | [React.ReactElement<IModalHeaderProps>, React.ReactElement<IModalBodyProps>];\n /**\n * Overwrite the Modal styles by passing space separated class names\n */\n className?: string;\n /**\n * Sets the modal content to right-to-left\n */\n rtl?: boolean;\n /**\n * Hide the modal on esc key press\n * @default true\n */\n escKeyClose?: boolean;\n /**\n * Show/hide the close button within the modal\n * @default true\n */\n closeButton?: boolean;\n /**\n * Pass a reference to the element where the Modal needs to be rendered\n * @default document.body\n */\n portalTarget?: HTMLElement;\n /**\n * A list of Refs to be considered as a part of the Lock. The html elements in the list will be accessible while the Modal is open.\n */\n shards?: ( HTMLElement | React.RefObject<any> )[];\n /**\n * Callback function for onHide\n */\n onHide?: () => void;\n}\n\nexport interface IModalHeaderProps {\n /**\n * Header of the Modal\n */\n children?: React.ReactNode;\n /**\n * Dynamically updates the classes applied to the component\n */\n className?: string;\n}\n\nexport interface IModalBodyProps {\n /**\n * Modal Body\n */\n children?: React.ReactNode;\n /**\n * Dynamically updates the classes applied to the component\n */\n className?: string;\n}\n\n/**\n * Modal component for displaying content in an overlay with optional header and close button.\n *\n * @example\n * ```tsx\n * <Modal showModal={modalVisible} onHide={() => setModalVisible(false)}>\n * <Modal.Header>Modal Title</Modal.Header>\n * <Modal.Body>Modal content goes here</Modal.Body>\n * </Modal>\n * ```\n */\nconst ModalComponent = React.forwardRef<HTMLDivElement, IModalProps>(\n (\n {\n showModal,\n onHide,\n closeButton = true,\n escKeyClose = true,\n rtl = false,\n portalTarget = typeof document !== "undefined" ? document.body : undefined,\n shards,\n className,\n children\n },\n ref\n ) => {\n const modalRef = React.useRef<HTMLDivElement | null>( null );\n\n let headerPresent = false;\n React.Children.forEach( children, ( child ) => {\n if ( React.isValidElement( child ) && child.type === ModalHeader ) {\n headerPresent = true;\n }\n });\n\n const { container, modal } = modalVariants({ headerless: !headerPresent });\n\n const closeModal = React.useCallback(() => {\n if ( onHide ) {\n onHide();\n }\n }, [onHide]);\n\n React.useEffect(() => {\n const handleEsc = ( e: KeyboardEvent ) => {\n if ( e.code === "Escape" && escKeyClose ) {\n closeModal();\n }\n };\n\n if ( showModal ) {\n document.addEventListener( "keydown", handleEsc );\n }\n\n return () => {\n document.removeEventListener( "keydown", handleEsc );\n };\n }, [ showModal, escKeyClose, closeModal ]);\n\n if ( !showModal || !portalTarget ) {\n return null;\n }\n\n return ReactDOM.createPortal(\n <FocusOn\n enabled={showModal}\n shards={shards}\n {...( rtl ? { dir: "rtl" } : {})}\n className={container()}\n >\n <div\n ref={( instance ) => {\n modalRef.current = instance;\n if ( typeof ref === "function" ) {\n ref( instance );\n } else if ( ref ) {\n ( ref as React.MutableRefObject<HTMLDivElement | null> ).current = instance;\n }\n }}\n data-testid="modal"\n className={modal({ class: className })}\n aria-labelledby="dialog-label"\n aria-describedby="dialog-desc"\n >\n {closeButton && (\n <Button\n prefixIcon="cross"\n onClick={closeModal}\n data-testid="close-button"\n variant="text"\n color="secondary"\n aria-label="close modal"\n className="absolute top-6 end-6 text-gray-800 dark:text-gray-800 dark:hover:text-gray-800"\n autoFocus\n />\n )}\n {children}\n </div>\n </FocusOn>,\n portalTarget\n );\n }\n);\n\nModalComponent.displayName = "Modal";\n\n/**\n * Modal header component for rendering the title in the Modal.\n *\n * @example\n * ```tsx\n * <Modal.Header>Modal Title</Modal.Header>\n * ```\n */\nconst ModalHeader = React.forwardRef<\n HTMLDivElement,\n IModalHeaderProps\n>(({ children, className }, ref ) => {\n if ( !children ) {\n return null;\n }\n return (\n <div\n ref={ref}\n id="dialog-label"\n data-testid="modal-header"\n className={cn(\n "flex flex-row flex-nowrap justify-between text-lg font-semibold me-7 text-secondary-500 dark:text-secondary-50",\n className\n )}\n >\n {children}\n </div>\n );\n});\n\nModalHeader.displayName = "ModalHeader";\n\n/**\n * Modal body component for rendering content inside the Modal.\n *\n * @example\n * ```tsx\n * <Modal.Body>Modal content goes here</Modal.Body>\n * ```\n */\nconst ModalBody = React.forwardRef<HTMLDivElement, IModalBodyProps>(\n ({ children, className }, ref ) => {\n if ( !children ) {\n return null;\n }\n return (\n <div\n ref={ref}\n id="dialog-desc"\n data-testid="modal-content"\n className={cn(\n "text-base overflow-y-auto overflow-x-hidden text-wrap mt-2 max-h-[84vh] dark:text-secondary-200",\n className\n )}\n >\n {children}\n </div>\n );\n }\n);\n\nModalBody.displayName = "ModalBody";\n\n// Define the compound component type\ntype TModalCompoundComponent = typeof ModalComponent & {\n Header: typeof ModalHeader;\n Body: typeof ModalBody;\n};\n\n// Create the compound component by casting and attaching subcomponents\nconst Modal = ModalComponent as TModalCompoundComponent;\nModal.Header = ModalHeader;\nModal.Body = ModalBody;\n\n// Export the compound component and individual components\nexport { Modal, ModalHeader, ModalBody };\n'
|
|
2394
2367
|
},
|
|
2395
2368
|
{
|
|
2396
2369
|
"name": "index.ts",
|
|
2397
|
-
"content": 'export {\n Modal,\n ModalHeader,\n ModalBody,\n type IModalProps,\n type IModalHeaderProps,\n type IModalBodyProps\n} from "./modal";\n\nexport {
|
|
2370
|
+
"content": 'export {\n Modal,\n ModalHeader,\n ModalBody,\n type IModalProps,\n type IModalHeaderProps,\n type IModalBodyProps\n} from "./modal";\n\nexport { modalVariants } from "./modalVariants";'
|
|
2398
2371
|
},
|
|
2399
2372
|
{
|
|
2400
2373
|
"name": "README.md",
|
|
@@ -2407,22 +2380,23 @@ DropdownItem.displayName = "DropdownItem";
|
|
|
2407
2380
|
"name": "otp",
|
|
2408
2381
|
"description": "The OTP (One-Time Password) component provides an input interface for entering verification codes. **When to use:** - Two-factor authentication - Email/SMS verification - Security code entry - PIN entry **Component Architecture:** - Styled with Tailwind CSS and cva - Auto-focus next input - Paste support - Customizable length",
|
|
2409
2382
|
"dependencies": [
|
|
2410
|
-
"
|
|
2411
|
-
"react"
|
|
2383
|
+
"tailwind-variants",
|
|
2384
|
+
"react",
|
|
2385
|
+
"tailwind-merge"
|
|
2412
2386
|
],
|
|
2413
2387
|
"internalDependencies": [],
|
|
2414
2388
|
"files": [
|
|
2415
2389
|
{
|
|
2416
2390
|
"name": "otpVariants.ts",
|
|
2417
|
-
"content": 'import {
|
|
2391
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const otpTextVariants = tv({\n base: "text-xs font-normal",\n variants: {\n error: {\n true: "text-error-500 dark:text-error-400",\n false: "text-secondary-400 dark:text-secondary-200"\n }\n },\n defaultVariants: {\n error: false\n }\n});\n'
|
|
2418
2392
|
},
|
|
2419
2393
|
{
|
|
2420
2394
|
"name": "otp.tsx",
|
|
2421
|
-
"content": 'import * as React from "react";\nimport { useState, useCallback, useEffect, type ReactNode } from "react";\nimport { Input } from "./input";\nimport {
|
|
2395
|
+
"content": 'import * as React from "react";\nimport { useState, useCallback, useEffect, type ReactNode } from "react";\nimport { cn } from "tailwind-variants";\n\nimport { Input } from "./input";\nimport { debounce, isRegexSafe } from "@utils";\nimport { keyboardKeys } from "@constants";\nimport { otpTextVariants } from "./otpVariants";\n\n/**\n * OTP component for one-time password input with multiple fields\n */\nexport interface IOtpProps extends Omit<React.HTMLAttributes<HTMLFieldSetElement>, "onFocus" | "onBlur" | "onChange"> {\n /**\n * Value of the OTP input.\n */\n initialValue?: string;\n /**\n * Pass a regex to restrict what can be entered in the OTP input.\n */\n regexValidation?: string;\n /**\n * Callback triggered when the OTP input value changes.\n */\n onChange?: ( val: string ) => void;\n /**\n * Callback triggered when the OTP input loses focus.\n */\n onBlur?: React.FocusEventHandler<HTMLDivElement>;\n /**\n * Callback triggered when the OTP input gains focus.\n */\n onFocus?: React.FocusEventHandler<HTMLDivElement>;\n /**\n * Regular static label.\n */\n label?: ReactNode;\n /**\n * Pass OTP length.\n * @default 6\n */\n otpLength?: number;\n /**\n * Overwrite the style by passing space separated class names.\n */\n className?: string;\n /**\n * Additional information or guidance displayed below the OTP input.\n */\n helperText?: ReactNode;\n /**\n * Pass a boolean value to indicate if the OTP input is in an error state.\n * @default false\n */\n error?: boolean;\n /**\n * Whether the OTP input is disabled.\n * @default false\n */\n disabled?: boolean;\n}\n\n/**\n * OTP input component for one-time password entry\n */\nexport const OtpInput = React.forwardRef<HTMLFieldSetElement, IOtpProps>(\n ({\n initialValue,\n regexValidation,\n onChange,\n onBlur,\n onFocus,\n label,\n helperText,\n error = false,\n otpLength = 6,\n className,\n disabled = false,\n ...props\n }: IOtpProps, ref ): React.ReactElement | null => {\n const validOtpLength = Math.abs( otpLength );\n const [ otpValue, setOtpValue ] = useState<Array<string>>(\n Array( validOtpLength ).fill( "" )\n );\n const [ focusedInput, setFocusedInput ] = useState(\n initialValue ? initialValue.length - 1 : -1\n );\n const [ isFocused, setIsFocused ] = useState( false );\n\n // This useEffect is executed if the user has provided an initial value\n useEffect(() => {\n if ( initialValue ) {\n const temp = [];\n for ( let i = 0; i < validOtpLength; i++ ) {\n temp[i] = initialValue?.[i] || "";\n }\n setOtpValue( temp );\n }\n }, [ validOtpLength, initialValue ]);\n\n // isInputValid is used to check whether the input provided is valid or not\n const isInputValid = useCallback(\n ( val: string ) => {\n // Default to numeric validation when no custom pattern provided\n if ( !regexValidation ) {\n return /[0-9]/.test( val );\n }\n\n // Validate regex pattern for ReDoS vulnerabilities\n // Note: OTP patterns should be shorter, so we add an additional length check\n const isPatternSafe = isRegexSafe( regexValidation ) && regexValidation.length <= 20;\n\n if ( !isPatternSafe ) {\n console.error(\n "[Security] Unsafe regex pattern provided to OTP component. " +\n "Pattern has been rejected to prevent ReDoS attacks. " +\n `Pattern: ${regexValidation}`\n );\n // Reject the pattern - do not accept any input when custom pattern is unsafe\n return false;\n }\n\n try {\n const regularExpression = new RegExp( regexValidation );\n return regularExpression.test( val );\n } catch ( err ) {\n console.error(\n "[Security] Invalid regex pattern provided to OTP component. " +\n `Pattern: ${regexValidation}`,\n err\n );\n // Reject invalid patterns - do not accept input\n return false;\n }\n },\n [regexValidation]\n );\n\n const onInputChange =\n ( idx: number ) => ( event: React.ChangeEvent<HTMLInputElement> ) => {\n const values = [...otpValue];\n const { value } = event.target;\n values[idx] = value;\n if ( isInputValid( value ) || value === "" ) {\n setOtpValue([...values]);\n onChange?.( values.join( "" ));\n\n if ( idx < validOtpLength - 1 && value !== "" ) {\n setFocusedInput(( index ) => index + 1 );\n } else if ( idx !== 0 && value === "" ) {\n setFocusedInput(( index ) => index - 1 );\n }\n }\n };\n\n // handleKeyDown is being used to react to the keyboard inputs that the user provides while the component is focused\n const handleKeyDown = useCallback(\n ( event: React.KeyboardEvent<HTMLInputElement> ) => {\n let currentFocused = focusedInput;\n const { key } = event;\n if ( key === keyboardKeys.tab ) {\n return;\n }\n switch ( key ) {\n case keyboardKeys.arrowLeft:\n if ( currentFocused !== 0 ) {\n currentFocused--;\n }\n break;\n case keyboardKeys.backspace:\n if ( otpValue[currentFocused] === "" && currentFocused !== 0 ) {\n currentFocused--;\n }\n break;\n case keyboardKeys.arrowRight:\n if ( currentFocused < validOtpLength - 1 ) {\n currentFocused++;\n }\n break;\n default:\n if (\n otpValue[currentFocused] === key &&\n currentFocused < validOtpLength - 1\n ) {\n currentFocused++;\n }\n }\n setFocusedInput( currentFocused );\n },\n [ focusedInput, otpValue, validOtpLength ]\n );\n\n //handlePaste is used to enter text into the component that has been copied by the user\n const handlePaste = useCallback(\n ( event: React.ClipboardEvent<HTMLDivElement> ) => {\n const pastedValue = event.clipboardData\n .getData( "text" )\n ?.substring( 0, validOtpLength );\n const otp = [];\n\n for ( let i = 0; i < validOtpLength; i++ ) {\n otp.push( isInputValid( pastedValue[i]) ? pastedValue[i] : "" );\n }\n\n setOtpValue( otp );\n onChange?.( otp.join( "" ));\n setFocusedInput(\n pastedValue.length <= validOtpLength - 1\n ? pastedValue.length\n : validOtpLength - 1\n );\n },\n [ isInputValid, onChange, validOtpLength ]\n );\n\n const onFocusCallback = useCallback(\n ( event: React.FocusEvent<HTMLDivElement> ) => {\n if ( !isFocused ) {\n setIsFocused( true );\n onFocus?.( event );\n setFocusedInput( Number( event.target.id ));\n }\n },\n [ isFocused, onFocus ]\n );\n\n const onBlurCallback = useCallback(\n ( event: React.FocusEvent<HTMLDivElement> ) => {\n if ( isFocused ) {\n setIsFocused( false );\n onBlur?.( event );\n setFocusedInput( -1 );\n }\n },\n [ isFocused, onBlur ]\n );\n\n const handleEvents = useCallback(\n ( event: React.FocusEvent<HTMLDivElement> ) =>\n event.type === "focus" ? onFocusCallback( event ) : onBlurCallback( event ),\n [ onBlurCallback, onFocusCallback ]\n );\n\n /* Debounce has been used because of the shifting focus of inputs in the otpInput component and we want the whole component\n to act like one focusable component and therefore we have debounced the whole onFocus and onBlur events in order to avoid\n the continuous firing of these events while the focused inputs inside are changing */\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const handleFocusAndBlur = useCallback(\n debounce(( event ) => handleEvents( event ), 50 ),\n [handleEvents]\n );\n\n const otpHelperTextClass = otpTextVariants({ error });\n\n return validOtpLength\n ? (\n <fieldset\n className={cn( "inline-flex flex-col gap-2 m-0 p-0 border-0 min-w-0", className )}\n disabled={disabled}\n ref={ref}\n {...props}\n >\n {label && (\n <legend className={cn( "font-medium text-base text-secondary-500 dark:text-secondary-300 mb-2 p-0" )}>\n {label}\n </legend>\n )}\n <div\n className={cn( "inline-flex gap-2" )}\n onPaste={handlePaste}\n onBlur={handleFocusAndBlur}\n onFocus={handleFocusAndBlur}\n >\n {\n Array( validOtpLength )\n .fill( "" )\n .map(( _, index ) => (\n <Input\n key={index}\n index={index}\n handleChange={onInputChange( index )}\n focused={focusedInput === index}\n handleKeyDown={handleKeyDown}\n value={otpValue?.[index]}\n id={`${index}`}\n error={error}\n />\n ))}\n </div>\n {helperText && (\n <div\n className={otpHelperTextClass}\n >\n {helperText}\n </div>\n )}\n </fieldset>\n )\n : null;\n }\n);\n\nOtpInput.displayName = "OtpInput";'
|
|
2422
2396
|
},
|
|
2423
2397
|
{
|
|
2424
2398
|
"name": "input.tsx",
|
|
2425
|
-
"content": 'import * as React from "react";\nimport { cn } from "
|
|
2399
|
+
"content": 'import * as React from "react";\nimport { cn } from "tailwind-variants";\n\ninterface IInputProps {\n /**\n * The value of the input\n */\n value: string;\n /**\n * Handler for input changes\n */\n handleChange: React.ChangeEventHandler<HTMLInputElement>;\n /**\n * Handler for keyboard events\n */\n handleKeyDown: React.KeyboardEventHandler<HTMLInputElement>;\n /**\n * Whether the input is focused\n */\n focused: boolean;\n /**\n * The index of the input\n */\n index: number;\n /**\n * The ID of the input\n */\n id: string;\n /**\n * Whether the input is in an error state\n */\n error: boolean;\n}\n\n/**\n * Input component for OTP field\n */\nexport const Input: React.FC<IInputProps> = ({\n value,\n handleChange,\n handleKeyDown,\n focused,\n index,\n id,\n error\n}: IInputProps ) => {\n const inputRef = React.useRef<HTMLInputElement | null>( null );\n const handleFocus = ( event: React.FocusEvent<HTMLInputElement> ) => event.target.select();\n\n React.useEffect(() => {\n if ( focused ) {\n setTimeout(() => inputRef.current?.focus(), 0 );\n }\n }, [focused]);\n\n return (\n <input\n id={id}\n type="text"\n inputMode="numeric"\n autoComplete="one-time-code"\n value={value}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n onFocus={handleFocus}\n aria-label={`Enter character ${index + 1}`}\n maxLength={1}\n ref={inputRef}\n className={cn(\n "w-[2.625rem] px-2 py-[0.625rem] text-center",\n "border border-gray-200 rounded text-base focus:outline-none focus:ring-1 focus:ring-primary-500",\n "disabled:bg-gray-50 disabled:text-secondary-200",\n "dark:border-secondary-500",\n "dark:bg-iridium",\n "dark:disabled:bg-secondary-800",\n "dark:disabled:text-secondary-500",\n "dark:focus:ring-primary-400",\n "dark:text-secondary-200",\n error && "border-error-500 text-error-500 dark:border-error-400 dark:text-error-400"\n )}\n />\n );\n};\n\nInput.displayName = "OtpInput.Input";\n'
|
|
2426
2400
|
},
|
|
2427
2401
|
{
|
|
2428
2402
|
"name": "index.ts",
|
|
@@ -2439,8 +2413,9 @@ DropdownItem.displayName = "DropdownItem";
|
|
|
2439
2413
|
"name": "pagination",
|
|
2440
2414
|
"description": "The Pagination component provides navigation controls for paginated content, allowing users to navigate through pages of data. **When to use:** - Large datasets split across multiple pages - Search results - Table data with many rows - List views with numerous items **Component Architecture:** - Controlled component - Styled with Tailwind CSS and cva - Supports page size selection - Keyboard accessible",
|
|
2441
2415
|
"dependencies": [
|
|
2442
|
-
"
|
|
2443
|
-
"react"
|
|
2416
|
+
"tailwind-variants",
|
|
2417
|
+
"react",
|
|
2418
|
+
"tailwind-merge"
|
|
2444
2419
|
],
|
|
2445
2420
|
"internalDependencies": [
|
|
2446
2421
|
"button",
|
|
@@ -2449,15 +2424,15 @@ DropdownItem.displayName = "DropdownItem";
|
|
|
2449
2424
|
"files": [
|
|
2450
2425
|
{
|
|
2451
2426
|
"name": "paginationVariants.ts",
|
|
2452
|
-
"content": 'import {
|
|
2427
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const paginationVariants = tv({\n slots: {\n root: "flex items-center justify-between gap-4",\n pageContainer: "flex items-center gap-1",\n pageButton: "h-8 w-8 rounded flex justify-center text-base p-2.5 font-medium outline-none",\n ellipsis: "h-8 w-8 rounded flex justify-center text-base p-2.5 font-medium dark:text-secondary-200",\n text: "text-base text-secondary-500 dark:text-secondary-200"\n },\n variants: {\n layout: {\n detailed: {},\n basic: {},\n minimal: {}\n },\n variant: {\n filled: {\n pageButton: "text-secondary-500 dark:text-secondary-200 hover:bg-primary-50 dark:hover:bg-primary-400 " +\n "dark:hover:text-iridium focus:bg-primary-100 dark:focus:bg-primary-400"\n },\n outline: {\n pageButton: "text-secondary-500 dark:text-secondary-200 focus:outline-gray-200 focus:bg-transparent " +\n "hover:bg-gray-50 outline-offset-0 dark:focus:outline-primary-100 dark:focus:bg-transparent " +\n "dark:hover:bg-secondary-600"\n }\n }\n },\n defaultVariants: {\n layout: "detailed",\n variant: "filled"\n }\n});\n'
|
|
2453
2428
|
},
|
|
2454
2429
|
{
|
|
2455
2430
|
"name": "pagination.tsx",
|
|
2456
|
-
"content": 'import * as React from "react";\nimport { useCallback, useEffect, useMemo, useState } from "react";\nimport {
|
|
2431
|
+
"content": 'import * as React from "react";\nimport { useCallback, useEffect, useMemo, useState } from "react";\nimport { Button } from "../button";\nimport { iconList } from "../adpIcon";\nimport { paginationVariants } from "./paginationVariants";\n\n/**\n * Pagination component for navigating through multiple pages of content.\n */\nexport interface IPaginationProps {\n /**\n * The total number of pages.\n */\n totalPages?: number;\n /**\n * Use the prop to set the active page number. The value for activePageNo needs to start from 1.\n */\n activePageNo?: number;\n /**\n * Layout of the pagination.\n * @default detailed\n */\n layout?: "detailed" | "basic" | "minimal";\n /**\n * Pagination variant.\n * @default filled\n */\n variant?: "filled" | "outline";\n /**\n * Use the prop to hide the labels "Next" & "Previous"\n * @default true\n */\n showLabels?: boolean;\n /**\n * Callback function triggered when the active page changes.\n * @param activePageNo - The new page number that was selected\n */\n onPageChange?: ( activePageNo: number ) => void;\n /**\n * Optional CSS class name.\n */\n className?: string;\n}\n\nconst formatPageNumber = ( number: number ) => {\n return number.toString().padStart( 2, "0" ); // Always ensure a minimum of two digits\n};\n\n/**\n * Pagination component for navigating through multiple pages of content.\n */\nexport const Pagination = React.forwardRef<HTMLDivElement, IPaginationProps>(\n ({\n totalPages = 1,\n activePageNo = 1,\n layout = "detailed",\n variant = "filled",\n showLabels = true,\n onPageChange,\n className,\n ...props\n }, ref ) => {\n const [ activePgNo, setActivePgNo ] = useState<number>( activePageNo );\n\n // Sync internal state with the controlled activePageNo prop if provided\n useEffect(() => {\n if ( activePageNo !== undefined ) {\n setActivePgNo( activePageNo );\n }\n }, [ activePageNo, totalPages ]);\n\n const handleClick = useCallback(\n ( page: number ) => {\n if ( page >= 1 && page <= totalPages ) {\n setActivePgNo( page );\n onPageChange?.( page );\n }\n },\n [ totalPages, onPageChange, setActivePgNo ]\n );\n\n const { root, pageContainer, text } = paginationVariants({ layout, variant });\n\n const renderPageNumbers = useMemo(() => {\n const { pageButton, ellipsis } = paginationVariants({ variant });\n\n // Render Page Button\n const PageButton = ({ pageNumber }: { pageNumber: number }) =>\n activePgNo === pageNumber ? (\n <Button\n size="sm"\n variant={variant}\n className="h-8 w-8 p-2.5"\n onClick={() => handleClick( pageNumber )}\n color={variant === "filled" ? "primary" : "secondary"}\n aria-current="page"\n aria-label={`Page ${pageNumber}, current`}\n >\n {formatPageNumber( pageNumber )}\n </Button>\n ) : (\n <button\n className={pageButton({ class: variant === "filled" ? "button-text" : "" })}\n onClick={() => handleClick( pageNumber )}\n aria-label={`Page ${pageNumber}`}\n >\n {formatPageNumber( pageNumber )}\n </button>\n );\n\n const Ellipsis = () => (\n <span className={ellipsis()}>...</span>\n );\n\n if ( totalPages <= 10 ) {\n // Simple pagination for 10 or fewer pages\n return Array.from({ length: totalPages }, ( _, i ) => i + 1 ).map(( i ) => (\n <PageButton key={i} pageNumber={i} />\n ));\n } else {\n // Advanced pagination for greater than 10 pages\n if ( activePgNo <= 4 ) {\n // Active page is less than or equals to 4\n return [\n ...Array.from({ length: 5 }, ( _, i ) => i + 1 ).map(( i ) => (\n <PageButton key={i} pageNumber={i} />\n )),\n <Ellipsis key="start" />,\n <PageButton key={totalPages} pageNumber={totalPages} />\n ];\n } else if ( activePgNo > 4 && activePgNo <= totalPages - 4 ) {\n // Active page is greater than 4 and less than totalpage - 4\n return [\n <PageButton key={1} pageNumber={1} />,\n <Ellipsis key="start" />,\n <PageButton key={activePgNo - 1} pageNumber={activePgNo - 1} />,\n <PageButton key={activePgNo} pageNumber={activePgNo} />,\n <PageButton key={activePgNo + 1} pageNumber={activePgNo + 1} />,\n <Ellipsis key="end" />,\n <PageButton key={totalPages} pageNumber={totalPages} />\n ];\n } else {\n // Active page is greater than total page - 4\n return [\n <PageButton key={1} pageNumber={1} />,\n <Ellipsis key="end" />,\n ...Array.from({ length: 5 }, ( _, i ) => totalPages - 4 + i ).map(( i ) => (\n <PageButton key={i} pageNumber={i} />\n ))\n ];\n }\n }\n }, [ totalPages, activePgNo, variant, handleClick ]);\n\n return (\n <div\n ref={ref}\n className={root({ class: className })}\n {...props}\n >\n <Button\n size="sm"\n prefixIcon={iconList.leftArrowLong}\n color={variant === "filled" ? "primary" : "secondary"}\n variant={variant}\n className="rtl:rotate-180"\n onClick={() => handleClick( activePgNo - 1 )}\n disabled={activePgNo === 1}\n aria-label="Previous Page"\n aria-disabled={activePgNo === 1}\n >\n {showLabels ? "Previous" : null}\n </Button>\n <div className={pageContainer()}>\n {layout === "detailed" && renderPageNumbers}\n {layout === "basic" && (\n <div className={text()} aria-live="polite">\n Page {activePgNo} of {totalPages}\n </div>\n )}\n </div>\n <Button\n size="sm"\n prefixIcon={iconList.rightArrowLong}\n color={variant === "filled" ? "primary" : "secondary"}\n variant={variant}\n className="rtl:rotate-180"\n onClick={() => handleClick( activePgNo + 1 )}\n disabled={activePgNo === totalPages}\n aria-label="Next Page"\n aria-disabled={activePgNo === totalPages}\n >\n {showLabels ? "Next" : null}\n </Button>\n </div>\n );\n }\n);\n\nPagination.displayName = "Pagination";\n'
|
|
2457
2432
|
},
|
|
2458
2433
|
{
|
|
2459
2434
|
"name": "index.ts",
|
|
2460
|
-
"content": '// Export component and type\nexport { Pagination } from "./pagination";\n// eslint-disable-next-line no-duplicate-imports\nexport type { IPaginationProps } from "./pagination";\n\n// Export variants\nexport {
|
|
2435
|
+
"content": '// Export component and type\nexport { Pagination } from "./pagination";\n// eslint-disable-next-line no-duplicate-imports\nexport type { IPaginationProps } from "./pagination";\n\n// Export variants\nexport { paginationVariants } from "./paginationVariants";'
|
|
2461
2436
|
},
|
|
2462
2437
|
{
|
|
2463
2438
|
"name": "README.md",
|
|
@@ -2470,10 +2445,11 @@ DropdownItem.displayName = "DropdownItem";
|
|
|
2470
2445
|
"name": "panel",
|
|
2471
2446
|
"description": "The Panel component provides a sliding side panel (drawer) that appears from the edge of the screen using React Portal. It's ideal for displaying secondary content, forms, and detailed information without navigating away. Uses a compound component pattern with Header, Body, and Footer subcomponents. **When to use:** - Displaying detailed information or forms without leaving the current page - Navigation drawers and sidebars - Settings and configuration panels - Multi-step workflows or wizards - Filtering and advanced search options **Component Architecture:** - Built with React compound component pattern (Panel.Header, Panel.Body, Panel.Footer) - Rendered via React Portal for proper z-index layering - Uses react-focus-on for focus management and accessibility - Styled with Tailwind CSS and class-variance-authority (cva) - Configurable slide direction: panel can slide from left or right - Two size variants: compact and extended - RTL (right-to-left) support with inverted slide direction - Overlay backdrop that triggers close when clicked",
|
|
2472
2447
|
"dependencies": [
|
|
2473
|
-
"
|
|
2448
|
+
"tailwind-variants",
|
|
2474
2449
|
"react",
|
|
2475
2450
|
"react-dom",
|
|
2476
|
-
"react-focus-on"
|
|
2451
|
+
"react-focus-on",
|
|
2452
|
+
"tailwind-merge"
|
|
2477
2453
|
],
|
|
2478
2454
|
"internalDependencies": [
|
|
2479
2455
|
"button",
|
|
@@ -2482,13 +2458,12 @@ DropdownItem.displayName = "DropdownItem";
|
|
|
2482
2458
|
"files": [
|
|
2483
2459
|
{
|
|
2484
2460
|
"name": "panelVariants.ts",
|
|
2485
|
-
"content": 'import {
|
|
2461
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const panelVariants = tv({\n slots: {\n container: "z-[1080] relative",\n overlay: "hidden md:fixed md:inset-0 bg-black/40",\n panel: "!flex flex-col bg-white dark:bg-iridium dark:text-secondary-300",\n header: `flex px-6 py-4 min-h-11 border-b border-secondary-100 justify-between items-center gap-6 \n flex-shrink-0 dark:bg-secondary-800 dark:border-secondary-500`,\n body: "p-6 flex-1 overflow-y-auto",\n footer: "px-6 py-3 min-h-11 flex-shrink-0 border-t border-secondary-100 dark:bg-secondary-800 dark:border-secondary-500"\n },\n variants: {\n variant: {\n extended: { panel: "w-4/5 h-full inset-y-0" },\n compact: { panel: "w-2/5 h-[calc(100%-5rem)]" }\n },\n animation: {\n slideInRight: { panel: "animate-[panel-slide-in_0.5s]" },\n slideOutRight: { panel: "animate-[panel-slide-out_0.5s]" },\n slideInLeft: { panel: "animate-[panel-slide-in-rtl_0.5s]" },\n slideOutLeft: { panel: "animate-[panel-slide-out-rtl_0.5s]" },\n appear: { overlay: "animate-[panel-appear_0.5s] md:block" },\n disappear: { overlay: "animate-[panel-disappear_0.5s] md:block" },\n none: {}\n },\n visibility: {\n visible: { panel: "block", overlay: "md:block" },\n hidden: { panel: "hidden", overlay: "hidden" }\n },\n direction: {\n right: { panel: "rounded-l-xl fixed left-auto right-0" },\n left: { panel: "rounded-r-xl fixed right-auto left-0" }\n }\n },\n compoundVariants: [\n {\n variant: "compact",\n direction: "right",\n class: { panel: "rounded-xl right-8 inset-y-8" }\n },\n {\n variant: "compact",\n direction: "left",\n class: { panel: "rounded-xl left-8 inset-y-8" }\n }\n ],\n defaultVariants: {\n variant: "compact",\n animation: "none",\n visibility: "visible",\n direction: "right"\n }\n});\n\n// Variant for the toggleVisibilityUsingCss mode \u2014 uses CSS transitions instead of keyframe animations\nexport const cssVisibilityVariants = tv({\n slots: {\n container: "z-[1080] relative",\n overlay: "hidden md:fixed md:inset-0 bg-black/40 opacity-0 transition-opacity duration-500",\n panel: "shadow-xl transition-transform duration-500 flex flex-col bg-white dark:bg-iridium dark:text-secondary-300"\n },\n variants: {\n direction: {\n right: { panel: "fixed right-0 left-auto translate-x-full" },\n left: { panel: "fixed left-0 right-auto -translate-x-full" }\n },\n variant: {\n compact: { panel: "w-2/5 h-[calc(100%-5rem)] inset-y-8 rounded-xl" },\n extended: { panel: "w-4/5 h-full" }\n },\n showPanel: {\n true: { overlay: "block opacity-100 animate-[panel-appear_0.5s]" },\n false: {}\n }\n },\n compoundVariants: [\n {\n variant: "extended",\n direction: "right",\n class: { panel: "rounded-l-xl" }\n },\n {\n variant: "extended",\n direction: "left",\n class: { panel: "rounded-r-xl" }\n },\n {\n showPanel: true,\n direction: "right",\n variant: "compact",\n class: { panel: "-translate-x-8" }\n },\n {\n showPanel: true,\n direction: "right",\n variant: "extended",\n class: { panel: "-translate-x-0" }\n },\n {\n showPanel: true,\n direction: "left",\n variant: "compact",\n class: { panel: "translate-x-8" }\n },\n {\n showPanel: true,\n direction: "left",\n variant: "extended",\n class: { panel: "translate-x-0" }\n }\n ],\n defaultVariants: {\n direction: "right",\n variant: "compact",\n showPanel: false\n }\n});\n'
|
|
2486
2462
|
},
|
|
2487
2463
|
{
|
|
2488
2464
|
"name": "panelHeader.tsx",
|
|
2489
2465
|
"content": `import * as React from "react";
|
|
2490
|
-
import {
|
|
2491
|
-
import { panelHeaderVariants } from "./panelVariants";
|
|
2466
|
+
import { panelVariants } from "./panelVariants";
|
|
2492
2467
|
import { Button } from "../button";
|
|
2493
2468
|
import { iconList } from "../adpIcon";
|
|
2494
2469
|
|
|
@@ -2546,9 +2521,11 @@ export const PanelHeader = React.forwardRef<HTMLDivElement, IPanelHeaderProps>(
|
|
|
2546
2521
|
[onCloseHandler]
|
|
2547
2522
|
);
|
|
2548
2523
|
|
|
2524
|
+
const { header: headerClass } = panelVariants();
|
|
2525
|
+
|
|
2549
2526
|
return (
|
|
2550
2527
|
<div
|
|
2551
|
-
className={
|
|
2528
|
+
className={headerClass({ class: className })}
|
|
2552
2529
|
data-testid="panel-header"
|
|
2553
2530
|
ref={ref}
|
|
2554
2531
|
{...props}
|
|
@@ -2591,19 +2568,19 @@ PanelHeader.displayName = "PanelHeader";
|
|
|
2591
2568
|
},
|
|
2592
2569
|
{
|
|
2593
2570
|
"name": "panelFooter.tsx",
|
|
2594
|
-
"content": 'import * as React from "react";\nimport {
|
|
2571
|
+
"content": 'import * as React from "react";\nimport { panelVariants } from "./panelVariants";\n\nexport interface IPanelFooterProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Panel Footer\n */\n children?: React.ReactNode;\n /**\n * Overwrite the Panel footer styles by passing space separated class names\n */\n className?: string;\n}\n\nexport const PanelFooter = React.forwardRef<HTMLDivElement, IPanelFooterProps>(\n ({\n className,\n children,\n ...props\n }, ref ) => {\n const { footer } = panelVariants();\n\n return children ? (\n <div\n className={footer({ class: className })}\n data-testid="panel-footer"\n ref={ref}\n {...props}\n >\n {children}\n </div>\n ) : null;\n }\n);\n\nPanelFooter.displayName = "PanelFooter";\n'
|
|
2595
2572
|
},
|
|
2596
2573
|
{
|
|
2597
2574
|
"name": "panelBody.tsx",
|
|
2598
|
-
"content": 'import * as React from "react";\nimport {
|
|
2575
|
+
"content": 'import * as React from "react";\nimport { panelVariants } from "./panelVariants";\n\nexport interface IPanelBodyProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Panel Body\n */\n children?: React.ReactNode;\n /**\n * Overwrite the Panel body styles by passing space separated class names\n */\n className?: string;\n}\n\nexport const PanelBody = React.forwardRef<HTMLDivElement, IPanelBodyProps>(\n ({\n className,\n children,\n ...props\n }, ref ) => {\n const { body } = panelVariants();\n\n return children ? (\n <div\n className={body({ class: className })}\n data-testid="panel-content"\n id="panel-description"\n ref={ref}\n {...props}\n >\n {children}\n </div>\n ) : null;\n }\n);\n\nPanelBody.displayName = "PanelBody";\n'
|
|
2599
2576
|
},
|
|
2600
2577
|
{
|
|
2601
2578
|
"name": "panel.tsx",
|
|
2602
|
-
"content": 'import * as React from "react";\nimport ReactDOM from "react-dom";\nimport { FocusOn } from "react-focus-on";\nimport {
|
|
2579
|
+
"content": 'import * as React from "react";\nimport ReactDOM from "react-dom";\nimport { FocusOn } from "react-focus-on";\n\nimport { panelVariants, cssVisibilityVariants } from "./panelVariants";\nimport { PanelHeader } from "./panelHeader";\nimport { PanelBody } from "./panelBody";\nimport { PanelFooter } from "./panelFooter";\n\nexport interface IPanelProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "dir"> {\n /**\n * Variant of the Panel\n * @default compact\n */\n variant?: "extended" | "compact";\n /**\n * Prop to control the panel display\n * @default false\n */\n showPanel?: boolean;\n /**\n * Toggle the visibility of the Panel without unmounting it\n * @default false\n */\n toggleVisibilityUsingCss?: boolean;\n /**\n * Overwrite the Panel styles by passing space separated class names\n */\n className?: string;\n /**\n * Accepts children of ReactNode type\n */\n children: React.ReactNode;\n /**\n * Direction from which the panel slides in\n * @default "right"\n */\n slideDirection?: "left" | "right";\n /**\n * If true, indicates that the app is in RTL (right-to-left) mode\n * When true in RTL mode, slideDirection="left" will make panel open from right\n * @default false\n */\n rtl?: boolean;\n /**\n * Pass a reference to the element where the Panel needs to be rendered\n * @default document.body\n */\n portalTarget?: HTMLElement;\n /**\n * A list of Refs to be considered as a part of the Lock. The html elements in the list will be accessible while the Panel is open.\n */\n shards?: ( HTMLElement | React.RefObject<HTMLElement> )[];\n /**\n * Optional callback function to call when panel is closed\n */\n onClose?: ( e: React.MouseEvent<HTMLDivElement> | KeyboardEvent ) => void;\n}\n\n// Create the base Panel component\nconst PanelComponent = React.forwardRef<HTMLDivElement, IPanelProps>(\n ({\n variant = "compact",\n showPanel = false,\n toggleVisibilityUsingCss = false,\n className,\n children,\n rtl,\n slideDirection = "right",\n portalTarget = typeof document !== "undefined" ? document.body : undefined,\n shards,\n onClose,\n ...props\n }, ref ) => {\n // In RTL mode, we invert the slide direction\n const effectiveSlideDirection = rtl\n ? ( slideDirection === "left" ? "right" : "left" )\n : slideDirection;\n const panelRef = React.useRef<HTMLDivElement>( null );\n const [ shouldRender, setShouldRender ] = React.useState( false );\n\n // Handle showing/hiding the panel\n React.useEffect(() => {\n if ( showPanel ) {\n setShouldRender( true );\n }\n\n // When toggleVisibilityUsingCss = false, this timeout hides the panel before the slide out animation ends.\n if ( shouldRender && !showPanel ) {\n setTimeout(() => setShouldRender( false ), 490 );\n }\n }, [ showPanel, shouldRender ]);\n\n // Handle escape key for closing\n React.useEffect(() => {\n // Esc key handling\n const handleEscClick = ( event: KeyboardEvent ) => event.code === "Escape" && onClose?.( event );\n\n const removeListener = () => {\n document.removeEventListener( "keydown", handleEscClick, false );\n };\n\n // Event Listener added for the keypress and click when component mounts\n if ( showPanel ) {\n document.addEventListener( "keydown", handleEscClick, false );\n } else {\n removeListener();\n }\n\n return () => {\n // Event Listener removed for the keypress and click when component unmounts\n removeListener();\n };\n }, [ showPanel, onClose ]);\n\n // Using CSS to control visibility\n if ( toggleVisibilityUsingCss ) {\n const { container, overlay, panel } = cssVisibilityVariants({\n direction: effectiveSlideDirection,\n variant,\n showPanel\n });\n\n return portalTarget ? ReactDOM.createPortal(\n <FocusOn\n as="div"\n enabled={showPanel}\n shards={shards}\n className={container()}\n >\n <div\n className={overlay()}\n onClick={onClose}\n id="panel-overlay"\n />\n\n {children && (\n <div\n id="panel"\n aria-modal="true"\n role="dialog"\n {...rtl ? { dir: "rtl" } : {}}\n ref={ref || panelRef}\n className={panel({ class: className })}\n {...props}\n >\n {children}\n </div>\n )}\n </FocusOn>,\n portalTarget\n ) : null;\n } else {\n const panelAnimation = (\n showPanel\n ? `slideIn${effectiveSlideDirection === "right" ? "Right" : "Left"}`\n : `slideOut${effectiveSlideDirection === "right" ? "Right" : "Left"}`\n ) as "slideInRight" | "slideOutRight" | "slideInLeft" | "slideOutLeft";\n\n const { container } = panelVariants();\n const { overlay } = panelVariants({\n animation: showPanel ? "appear" : "disappear",\n visibility: showPanel ? "visible" : "hidden"\n });\n const { panel } = panelVariants({\n variant,\n animation: panelAnimation,\n visibility: "visible",\n direction: effectiveSlideDirection\n });\n\n return shouldRender && portalTarget ? ReactDOM.createPortal(\n <FocusOn\n as="div"\n enabled={showPanel}\n shards={shards}\n className={container()}\n >\n <div\n className={overlay()}\n onClick={onClose}\n id="panel-overlay"\n />\n\n {children && (\n <div\n id="panel"\n aria-modal="true"\n role="dialog"\n {...rtl ? { dir: "rtl" } : {}}\n ref={ref || panelRef}\n className={panel({ class: className })}\n {...props}\n >\n {children}\n </div>\n )}\n </FocusOn>,\n portalTarget\n ) : null;\n }\n }\n);\n\nPanelComponent.displayName = "Panel";\n\n// Define the compound component type\ntype TPanelCompoundComponent = typeof PanelComponent & {\n Header: typeof PanelHeader;\n Body: typeof PanelBody;\n Footer: typeof PanelFooter;\n};\n\n// Create the compound component by casting\nexport const Panel = PanelComponent as TPanelCompoundComponent;\n\n// Attach the subcomponents\nPanel.Header = PanelHeader;\nPanel.Body = PanelBody;\nPanel.Footer = PanelFooter;\n'
|
|
2603
2580
|
},
|
|
2604
2581
|
{
|
|
2605
2582
|
"name": "index.ts",
|
|
2606
|
-
"content": 'import { Panel as PanelComponent, type IPanelProps } from "./panel";\nimport { PanelHeader, type IPanelHeaderProps } from "./panelHeader";\nimport { PanelBody, type IPanelBodyProps } from "./panelBody";\nimport { PanelFooter, type IPanelFooterProps } from "./panelFooter";\nimport {
|
|
2583
|
+
"content": 'import { Panel as PanelComponent, type IPanelProps } from "./panel";\nimport { PanelHeader, type IPanelHeaderProps } from "./panelHeader";\nimport { PanelBody, type IPanelBodyProps } from "./panelBody";\nimport { PanelFooter, type IPanelFooterProps } from "./panelFooter";\nimport { panelVariants, cssVisibilityVariants } from "./panelVariants";\n\ntype TPanelCompoundComponent = typeof PanelComponent & {\n Header: typeof PanelHeader;\n Body: typeof PanelBody;\n Footer: typeof PanelFooter;\n};\n\nconst Panel = PanelComponent as TPanelCompoundComponent;\nPanel.Header = PanelHeader;\nPanel.Body = PanelBody;\nPanel.Footer = PanelFooter;\n\nexport {\n Panel,\n PanelHeader,\n PanelBody,\n PanelFooter,\n panelVariants,\n cssVisibilityVariants\n};\n\nexport type {\n IPanelProps,\n IPanelHeaderProps,\n IPanelBodyProps,\n IPanelFooterProps\n};'
|
|
2607
2584
|
},
|
|
2608
2585
|
{
|
|
2609
2586
|
"name": "README.md",
|
|
@@ -2616,8 +2593,9 @@ PanelHeader.displayName = "PanelHeader";
|
|
|
2616
2593
|
"name": "panelGroup",
|
|
2617
2594
|
"description": "The PanelGroup component manages multiple panels with coordinated state and transitions. **When to use:** - Multi-panel workflows - Wizard-style interfaces - Nested navigation - Complex forms with multiple steps - Settings with subsections **Component Architecture:** - Manages multiple Panel components - Styled with Tailwind CSS and cva - Coordinated open/close states - Stack-based navigation",
|
|
2618
2595
|
"dependencies": [
|
|
2619
|
-
"
|
|
2620
|
-
"react"
|
|
2596
|
+
"tailwind-variants",
|
|
2597
|
+
"react",
|
|
2598
|
+
"tailwind-merge"
|
|
2621
2599
|
],
|
|
2622
2600
|
"internalDependencies": [
|
|
2623
2601
|
"panel",
|
|
@@ -2626,13 +2604,12 @@ PanelHeader.displayName = "PanelHeader";
|
|
|
2626
2604
|
"files": [
|
|
2627
2605
|
{
|
|
2628
2606
|
"name": "panelGroupVariants.ts",
|
|
2629
|
-
"content": 'import {
|
|
2607
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const panelGroupVariants = tv({\n slots: {\n root: "panel-group",\n item: ""\n },\n variants: {\n visibility: {\n visible: { root: "" },\n hidden: { root: "panel-group--close" }\n },\n position: {\n current: { item: "" },\n subsequent: { item: "panel-group__subsequent-panel" },\n previous: { item: "panel-group__previous-panel bg-secondary-100/50" }\n },\n animation: {\n slideIn: { item: "slide-in-animation" }\n },\n spacing: {\n default: { item: "" },\n withMargin: { item: "me-[2.5rem]" }\n }\n },\n defaultVariants: {\n visibility: "visible",\n position: "current"\n }\n});\n'
|
|
2630
2608
|
},
|
|
2631
2609
|
{
|
|
2632
2610
|
"name": "panelGroup.tsx",
|
|
2633
2611
|
"content": `import * as React from "react";
|
|
2634
|
-
import {
|
|
2635
|
-
import { panelGroupVariants, panelGroupItemVariants } from "./panelGroupVariants";
|
|
2612
|
+
import { panelGroupVariants } from "./panelGroupVariants";
|
|
2636
2613
|
|
|
2637
2614
|
export interface IPanelGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
2638
2615
|
/**
|
|
@@ -2663,15 +2640,12 @@ export const PanelGroup = React.forwardRef<HTMLDivElement, IPanelGroupProps>(
|
|
|
2663
2640
|
showPanels = false,
|
|
2664
2641
|
...props
|
|
2665
2642
|
}, ref ) => {
|
|
2643
|
+
const { root, item } = panelGroupVariants({ visibility: showPanels ? "visible" : "hidden" });
|
|
2644
|
+
|
|
2666
2645
|
return (
|
|
2667
2646
|
<div
|
|
2668
2647
|
ref={ref}
|
|
2669
|
-
className={
|
|
2670
|
-
panelGroupVariants({
|
|
2671
|
-
visibility: showPanels ? "visible" : "hidden"
|
|
2672
|
-
}),
|
|
2673
|
-
className
|
|
2674
|
-
)}
|
|
2648
|
+
className={root({ class: className })}
|
|
2675
2649
|
data-testid="panel-group-container"
|
|
2676
2650
|
{...props}
|
|
2677
2651
|
>
|
|
@@ -2680,19 +2654,17 @@ export const PanelGroup = React.forwardRef<HTMLDivElement, IPanelGroupProps>(
|
|
|
2680
2654
|
key: idx,
|
|
2681
2655
|
showPanel: showPanels && ( activeIndex >= idx ),
|
|
2682
2656
|
toggleVisibilityUsingCss: false,
|
|
2683
|
-
className:
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
?
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
})
|
|
2695
|
-
)
|
|
2657
|
+
className: item({
|
|
2658
|
+
position:
|
|
2659
|
+
idx >= 1
|
|
2660
|
+
? activeIndex >= idx + 1
|
|
2661
|
+
? "previous"
|
|
2662
|
+
: "subsequent"
|
|
2663
|
+
: "current",
|
|
2664
|
+
spacing: activeIndex !== 0 && activeIndex === idx
|
|
2665
|
+
? "withMargin"
|
|
2666
|
+
: "default"
|
|
2667
|
+
})
|
|
2696
2668
|
})
|
|
2697
2669
|
))}
|
|
2698
2670
|
</div>
|
|
@@ -2745,22 +2717,23 @@ PanelGroup.displayName = "PanelGroup";
|
|
|
2745
2717
|
"name": "progress",
|
|
2746
2718
|
"description": "The Progress component displays a progress bar for indicating completion status. **When to use:** - File uploads - Form completion - Task progress - Loading progress - Multi-step processes **Component Architecture:** - Styled with Tailwind CSS and cva - Animated transitions - Color variants - Label support",
|
|
2747
2719
|
"dependencies": [
|
|
2748
|
-
"
|
|
2749
|
-
"react"
|
|
2720
|
+
"tailwind-variants",
|
|
2721
|
+
"react",
|
|
2722
|
+
"tailwind-merge"
|
|
2750
2723
|
],
|
|
2751
2724
|
"internalDependencies": [],
|
|
2752
2725
|
"files": [
|
|
2753
2726
|
{
|
|
2754
2727
|
"name": "progressVariants.ts",
|
|
2755
|
-
"content": 'import {
|
|
2728
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const progressVariants = tv({\n slots: {\n container: "flex items-center gap-2 w-full",\n track: "bg-gray-200 h-2 rounded-2xl w-full dark:bg-secondary-600",\n bar: "flex justify-center items-center outline rounded-sm text-xs font-medium transition-all duration-100 h-full py-1 relative group",\n text: [\n "flex items-center justify-center text-xs-fit text-white dark:text-secondary-800",\n "h-8 w-8 border rounded-full dark:border-secondary-800 opacity-0 group-hover:opacity-100",\n "transition-opacity duration-300 ease-in-out z-10 absolute -end-4"\n ],\n valueText: "text-base text-center text-secondary-500 font-medium min-w-8 dark:text-secondary-50",\n circular: "",\n circularTrack: "dark:stroke-secondary-600",\n circularProgress: "",\n circularText: "fill-secondary-500 font-semibold dark:fill-secondary-50"\n },\n variants: {\n color: {\n primary: {\n bar: "!bg-primary-500 outline-primary-400 dark:bg-primary-400 dark:outline-primary-500",\n text: "bg-primary-500 dark:bg-primary-400",\n circularTrack: "stroke-primary-50",\n circularProgress: "stroke-primary-500 dark:stroke-primary-400"\n },\n secondary: {\n bar: "bg-gray-500 outline-gray-400 dark:bg-secondary-400 dark:outline-gray-500",\n text: "bg-gray-500 dark:bg-secondary-400",\n circularTrack: "stroke-gray-50",\n circularProgress: "stroke-gray-500 dark:stroke-secondary-400"\n },\n success: {\n bar: "bg-success-500 outline-success-400 dark:bg-success-400 dark:outline-success-500",\n text: "bg-success-500 dark:bg-success-400",\n circularTrack: "stroke-success-50",\n circularProgress: "stroke-success-500 dark:stroke-success-400"\n },\n warning: {\n bar: "bg-warning-500 outline-warning-400 dark:bg-warning-400 dark:outline-warning-500",\n text: "bg-warning-500 dark:bg-warning-400",\n circularTrack: "stroke-warning-50",\n circularProgress: "stroke-warning-500 dark:stroke-warning-400"\n },\n error: {\n bar: "bg-error-500 outline-error-400 dark:bg-error-400 dark:outline-error-500",\n text: "bg-error-500 dark:bg-error-400",\n circularTrack: "stroke-error-50",\n circularProgress: "stroke-error-500 dark:stroke-error-400"\n }\n },\n size: {\n md: { circularText: "text-xxs" },\n lg: { circularText: "text-xs-fit" }\n },\n striped: {\n true: {\n bar: [\n "bg-[size:0.5rem_0.5rem]",\n "bg-[image:repeating-linear-gradient(45deg,rgba(255,255,255,0.15)_0_0.25rem,transparent_0.25rem_0.5rem)]"\n ]\n }\n },\n animate: {\n true: { bar: "animate-stripes" }\n },\n position: {\n top: { text: "bottom-3" },\n right: {},\n bottom: { text: "top-3" },\n inline: { text: "-end-4" }\n }\n },\n defaultVariants: {\n color: "primary",\n size: "md",\n position: "right"\n }\n});\n'
|
|
2756
2729
|
},
|
|
2757
2730
|
{
|
|
2758
2731
|
"name": "progress.tsx",
|
|
2759
|
-
"content": 'import * as React from "react";\nimport {
|
|
2732
|
+
"content": 'import * as React from "react";\nimport { progressVariants } from "./progressVariants";\n\nexport enum EColors {\n PRIMARY = "primary",\n SECONDARY = "secondary",\n SUCCESS = "success",\n WARNING = "warning",\n ERROR = "error"\n}\n\nexport enum EValuePositions {\n TOP = "top",\n RIGHT = "right",\n BOTTOM = "bottom",\n INLINE = "inline"\n}\n\nexport type TColorType = `${EColors}`;\nexport type TValuePositionType = `${EValuePositions}`;\n\n/**\n * Progress component for displaying progress bars in linear or circular variants.\n */\nexport interface IProgressProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Controls the colors of Progress\n * Only For linear progress\n * @default primary\n */\n color?: TColorType;\n /**\n * Variant of the Progress\n * @default linear\n */\n variant?: "linear" | "circular";\n /**\n * Use the prop to change the size of the Circular Progress\n * @default "md"\n */\n size?: "md" | "lg";\n /**\n * Value is used to display the percentage of the progress\n * @default 0\n * @range 0 to 100\n */\n value: number;\n /**\n * Prop controlling the position of the value of the Progress\n * @default inline\n */\n valuePosition?: TValuePositionType;\n /**\n * Set to true to display the percentage of progress.\n * @default false\n */\n showPercentage?: boolean;\n /**\n * Prop to set striped background\n * @default false\n */\n striped?: boolean;\n /**\n * Prop to set animated background, use in conjunction with striped property\n * @default false\n */\n animate?: boolean;\n}\n\n/**\n * Progress component for displaying progress bars in linear or circular variants.\n */\nexport const Progress = React.forwardRef<HTMLDivElement, IProgressProps>(\n ({\n className,\n color = "primary",\n variant = "linear",\n size = "md",\n value = 0,\n valuePosition = "inline",\n showPercentage = false,\n striped = false,\n animate = false,\n ...props\n }, ref ) => {\n const percentage = value >= 0 ? ( value > 99 ? 100 : value ) : 0;\n const percentageText = value < 10 ? `0${percentage}%` : `${percentage}%`;\n\n const { container, track, bar, text, valueText } = progressVariants({\n color, striped, animate, position: valuePosition\n });\n\n const CircularProgress = (): React.ReactElement => {\n // Clamp percentage between 0 and 100\n const validPercentage = Math.max( 0, Math.min( value, 100 ));\n\n // Define size dimensions based on `size` prop\n const dimensions = {\n md: { size: 56, strokeWidth: 7 },\n lg: { size: 80, strokeWidth: 10 }\n };\n\n const { size: diameter, strokeWidth } = dimensions[size];\n const radius = ( diameter - strokeWidth ) / 2;\n const circumference = 2 * Math.PI * radius;\n const offset = circumference - ( validPercentage / 100 ) * circumference;\n\n const { circular, circularTrack, circularProgress, circularText } = progressVariants({ size, color });\n\n return (\n <svg\n width={diameter}\n height={diameter}\n role="progressbar"\n aria-valuenow={validPercentage}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-live="polite"\n tabIndex={0}\n className={circular({ class: className })}\n >\n <circle\n className={circularTrack()}\n cx={diameter / 2}\n cy={diameter / 2}\n r={radius}\n fill="transparent"\n strokeWidth={strokeWidth}\n />\n <circle\n cx={diameter / 2}\n cy={diameter / 2}\n r={radius}\n fill="transparent"\n strokeWidth={strokeWidth}\n strokeDasharray={circumference}\n strokeDashoffset={offset}\n strokeLinecap="round"\n className={circularProgress()}\n style={{ transition: "stroke-dashoffset 0.5s ease" }}\n />\n {showPercentage && (\n <text\n className={circularText()}\n x="50%"\n y="50%"\n textAnchor="middle"\n dy=".3em"\n >\n {`${validPercentage}%`}\n </text>\n )}\n </svg>\n );\n };\n\n return variant === "linear" ? (\n <div\n className={container({ class: className })}\n ref={ref}\n {...props}\n >\n <div\n className={track()}\n role="progressbar"\n aria-valuenow={value}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-live="polite"\n tabIndex={0}\n >\n {percentage > 0 && (\n <div\n style={{ width: `${percentage}%` }}\n className={bar()}\n >\n {showPercentage && valuePosition !== EValuePositions.RIGHT && (\n <span className={text({ class: "text" })}>\n {percentageText}\n </span>\n )}\n </div>\n )}\n </div>\n {showPercentage && valuePosition === EValuePositions.RIGHT && (\n <span className={valueText()}>\n {percentageText}\n </span>\n )}\n </div>\n ) : variant === "circular" ? (\n <CircularProgress />\n ) : null;\n }\n);\n\nProgress.displayName = "Progress";\n'
|
|
2760
2733
|
},
|
|
2761
2734
|
{
|
|
2762
2735
|
"name": "index.ts",
|
|
2763
|
-
"content": 'export { Progress, EColors, EValuePositions, type IProgressProps, type TColorType, type TValuePositionType } from "./progress";\nexport {
|
|
2736
|
+
"content": 'export { Progress, EColors, EValuePositions, type IProgressProps, type TColorType, type TValuePositionType } from "./progress";\nexport { progressVariants } from "./progressVariants";'
|
|
2764
2737
|
},
|
|
2765
2738
|
{
|
|
2766
2739
|
"name": "README.md",
|
|
@@ -2773,74 +2746,61 @@ PanelGroup.displayName = "PanelGroup";
|
|
|
2773
2746
|
"name": "radio",
|
|
2774
2747
|
"description": "The Radio component provides a single selection input from a group of options. **When to use:** - Single choice from multiple options - Mutually exclusive selections - Settings with single value - Quiz or survey answers **Component Architecture:** - Styled with Tailwind CSS and cva - Accessible with proper ARIA attributes - Typically used within RadioGroup",
|
|
2775
2748
|
"dependencies": [
|
|
2776
|
-
"
|
|
2777
|
-
"react"
|
|
2749
|
+
"tailwind-variants",
|
|
2750
|
+
"react",
|
|
2751
|
+
"tailwind-merge"
|
|
2778
2752
|
],
|
|
2779
2753
|
"internalDependencies": [],
|
|
2780
2754
|
"files": [
|
|
2781
2755
|
{
|
|
2782
2756
|
"name": "radioVariants.ts",
|
|
2783
|
-
"content": `import {
|
|
2784
|
-
|
|
2785
|
-
export const radioVariants = cva(
|
|
2786
|
-
[
|
|
2787
|
-
"appearance-none outline-none flex justify-center items-center rounded-full",
|
|
2788
|
-
"border-1.5 border-secondary-200 ease-in-out duration-300",
|
|
2789
|
-
"hover:border-primary-500",
|
|
2790
|
-
"focus:border-primary-500 focus:ring-primary-500 focus:shadow-primary-2px",
|
|
2791
|
-
"checked:border-primary-500",
|
|
2792
|
-
"disabled:bg-secondary-50 disabled:border-secondary-200",
|
|
2793
|
-
"dark:border-gray-600 dark:bg-iridium dark:hover:border-primary-400 dark:disabled:border-gray-900",
|
|
2794
|
-
"dark:focus:border-primary-400 dark:focus:ring-primary-400 dark:checked:border-primary-400",
|
|
2795
|
-
"checked:bg-primary-50",
|
|
2796
|
-
"checked:disabled:bg-secondary-50",
|
|
2797
|
-
"dark:checked:disabled:bg-iridium dark:checked:bg-iridium dark:checked:disabled:border-gray-900",
|
|
2798
|
-
"before:content-[''] before:scale-0 before:[clip-path:circle(48%_at_50%_50%)] before:bg-primary-500 before:disabled:bg-secondary-200",
|
|
2799
|
-
"dark:before:bg-primary-400 dark:before:disabled:bg-gray-900",
|
|
2800
|
-
"checked:before:scale-100 checked:before:border-primary-500",
|
|
2801
|
-
"dark:checked:before:border-primary-400"
|
|
2802
|
-
].join( " " ),
|
|
2803
|
-
{
|
|
2804
|
-
variants: {
|
|
2805
|
-
size: {
|
|
2806
|
-
sm: "w-4 h-4 before:h-1.5 before:w-1.5",
|
|
2807
|
-
md: "w-5 h-5 before:h-2 before:w-2",
|
|
2808
|
-
lg: "w-6 h-6 before:h-2.5 before:w-2.5"
|
|
2809
|
-
}
|
|
2810
|
-
},
|
|
2811
|
-
defaultVariants: {
|
|
2812
|
-
size: "sm"
|
|
2813
|
-
}
|
|
2814
|
-
}
|
|
2815
|
-
);
|
|
2757
|
+
"content": `import { tv } from "tailwind-variants";
|
|
2816
2758
|
|
|
2817
|
-
export const
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
disabled:
|
|
2827
|
-
|
|
2828
|
-
|
|
2759
|
+
export const radioVariants = tv({
|
|
2760
|
+
slots: {
|
|
2761
|
+
input: [
|
|
2762
|
+
"appearance-none outline-none flex justify-center items-center rounded-full",
|
|
2763
|
+
"border-1.5 border-secondary-200 ease-in-out duration-300",
|
|
2764
|
+
"hover:border-primary-500",
|
|
2765
|
+
"focus:border-primary-500 focus:ring-primary-500 focus:shadow-primary-2px",
|
|
2766
|
+
"checked:border-primary-500",
|
|
2767
|
+
"disabled:bg-secondary-50 disabled:border-secondary-200",
|
|
2768
|
+
"dark:border-gray-600 dark:bg-iridium dark:hover:border-primary-400 dark:disabled:border-gray-900",
|
|
2769
|
+
"dark:focus:border-primary-400 dark:focus:ring-primary-400 dark:checked:border-primary-400",
|
|
2770
|
+
"checked:bg-primary-50",
|
|
2771
|
+
"checked:disabled:bg-secondary-50",
|
|
2772
|
+
"dark:checked:disabled:bg-iridium dark:checked:bg-iridium dark:checked:disabled:border-gray-900",
|
|
2773
|
+
"before:content-[''] before:scale-0 before:[clip-path:circle(48%_at_50%_50%)] before:bg-primary-500 before:disabled:bg-secondary-200",
|
|
2774
|
+
"dark:before:bg-primary-400 dark:before:disabled:bg-gray-900",
|
|
2775
|
+
"checked:before:scale-100 checked:before:border-primary-500",
|
|
2776
|
+
"dark:checked:before:border-primary-400"
|
|
2777
|
+
],
|
|
2778
|
+
label: "text-secondary-500 dark:text-secondary-100 disabled:text-secondary-200 disabled:dark:text-secondary-400"
|
|
2779
|
+
},
|
|
2780
|
+
variants: {
|
|
2781
|
+
size: {
|
|
2782
|
+
sm: { input: "w-4 h-4 before:h-1.5 before:w-1.5", label: "text-base" },
|
|
2783
|
+
md: { input: "w-5 h-5 before:h-2 before:w-2", label: "text-md" },
|
|
2784
|
+
lg: { input: "w-6 h-6 before:h-2.5 before:w-2.5", label: "text-lg" }
|
|
2829
2785
|
},
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
disabled: false
|
|
2786
|
+
disabled: {
|
|
2787
|
+
true: { label: "text-secondary-200 dark:text-secondary-400" }
|
|
2833
2788
|
}
|
|
2789
|
+
},
|
|
2790
|
+
defaultVariants: {
|
|
2791
|
+
size: "sm",
|
|
2792
|
+
disabled: false
|
|
2834
2793
|
}
|
|
2835
|
-
)
|
|
2794
|
+
});
|
|
2795
|
+
`
|
|
2836
2796
|
},
|
|
2837
2797
|
{
|
|
2838
2798
|
"name": "radio.tsx",
|
|
2839
|
-
"content": 'import * as React from "react";\nimport { forwardRef, useRef, useId } from "react";\nimport {
|
|
2799
|
+
"content": 'import * as React from "react";\nimport { forwardRef, useRef, useId } from "react";\nimport { radioVariants } from "./radioVariants";\n\n/**\n * Radio component props\n */\nexport interface IRadioProps extends Omit<React.HTMLProps<HTMLInputElement>, "label" | "size"> {\n /**\n * Size of the Radio button\n * @default sm\n */\n size?: "sm" | "md" | "lg";\n /**\n * Label for the Radio button\n */\n label?: React.ReactNode;\n}\n\n/**\n * Radio component for single-option selection within a group\n */\nexport const Radio = forwardRef<HTMLInputElement, IRadioProps>(\n ({\n size = "sm",\n label,\n className,\n id,\n disabled,\n ...rest\n }: IRadioProps, ref ): React.ReactElement => {\n const inputId = useRef( useId());\n const { input, label: labelClass } = radioVariants({ size, disabled: Boolean( disabled ) });\n\n return (\n <div className="inline-flex items-center gap-2">\n <input\n id={id || inputId.current}\n type="radio"\n className={input({ class: className })}\n ref={ref}\n disabled={disabled}\n {...rest}\n />\n {typeof label !== "undefined" && (\n <label\n htmlFor={id || inputId.current}\n className={labelClass()}\n >\n {label}\n </label>\n )}\n </div>\n );\n }\n);\n\nRadio.displayName = "Radio";\n'
|
|
2840
2800
|
},
|
|
2841
2801
|
{
|
|
2842
2802
|
"name": "index.ts",
|
|
2843
|
-
"content": 'export { Radio, type IRadioProps } from "./radio";\nexport { radioVariants
|
|
2803
|
+
"content": 'export { Radio, type IRadioProps } from "./radio";\nexport { radioVariants } from "./radioVariants";'
|
|
2844
2804
|
},
|
|
2845
2805
|
{
|
|
2846
2806
|
"name": "README.md",
|
|
@@ -2853,8 +2813,9 @@ export const radioLabelVariants = cva(
|
|
|
2853
2813
|
"name": "radioGroup",
|
|
2854
2814
|
"description": "The RadioGroup component manages a collection of radio buttons for single-choice selection. **When to use:** - Single choice from multiple options - Settings with mutually exclusive options - Quiz or survey questions - Configuration options **Component Architecture:** - Manages single selected value - Styled with Tailwind CSS and cva - Built on Radio component - Supports controlled and uncontrolled modes",
|
|
2855
2815
|
"dependencies": [
|
|
2856
|
-
"
|
|
2857
|
-
"react"
|
|
2816
|
+
"tailwind-variants",
|
|
2817
|
+
"react",
|
|
2818
|
+
"tailwind-merge"
|
|
2858
2819
|
],
|
|
2859
2820
|
"internalDependencies": [
|
|
2860
2821
|
"radio"
|
|
@@ -2862,15 +2823,15 @@ export const radioLabelVariants = cva(
|
|
|
2862
2823
|
"files": [
|
|
2863
2824
|
{
|
|
2864
2825
|
"name": "radioGroupVariants.ts",
|
|
2865
|
-
"content": 'import {
|
|
2826
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const radioGroupVariants = tv({\n slots: {\n root: "inline-flex flex-col",\n item: "inline-flex gap-2 items-start outline-none cursor-pointer pl-0",\n label: "text-secondary-500 dark:text-secondary-50"\n },\n variants: {\n variant: {\n outline: {\n root: "gap-4",\n item: [\n "rounded border border-secondary-100",\n "hover:border hover:border-primary-500",\n "focus-within:border-primary-500 focus-within:shadow-primary-2px",\n "dark:border-secondary-500",\n "dark:hover:border-primary-400",\n "dark:focus-within:border-primary-400 dark:focus-within:shadow-primary-2px"\n ]\n },\n text: {\n root: "border border-gray-100 rounded dark:border-secondary-500",\n item: ""\n }\n },\n size: {\n sm: { item: "p-3 text-base" },\n md: { item: "p-4 text-base" },\n lg: { item: "p-4 text-md" }\n },\n checked: {\n true: { item: "bg-primary-50 dark:bg-secondary-600" }\n },\n disabled: {\n true: {\n item: "bg-secondary-50 cursor-not-allowed dark:bg-secondary-700 dark:text-secondary-400",\n label: "text-secondary-200 dark:text-secondary-400"\n }\n }\n },\n defaultVariants: {\n variant: "outline",\n size: "md"\n }\n});\n'
|
|
2866
2827
|
},
|
|
2867
2828
|
{
|
|
2868
2829
|
"name": "radioGroup.tsx",
|
|
2869
|
-
"content": 'import * as React from "react";\nimport { type ReactElement, type ChangeEvent, useState, useEffect, useRef, useId } from "react";\nimport {
|
|
2830
|
+
"content": 'import * as React from "react";\nimport { type ReactElement, type ChangeEvent, useState, useEffect, useRef, useId } from "react";\n\nimport { Radio, type IRadioProps } from "../radio";\nimport { radioGroupVariants } from "./radioGroupVariants";\n\nexport type TRadioGroupSize = "sm" | "md" | "lg";\n\n/**\n * RadioGroup component props\n */\nexport interface IRadioGroupProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "onChange"> {\n /**\n * Size of the Radio group.\n * @default md\n */\n size?: TRadioGroupSize;\n /**\n * Pass space separated class names to override the RadioGroup styling\n */\n className?: string;\n /**\n * Pass boolean value to disable the component.\n * @default false\n */\n disabled?: boolean;\n /**\n * Selected value\n */\n selectedValue?: string;\n /**\n * An array of objects of radio props type\n */\n options: IRadioProps[];\n /**\n * Callback function that is called when the radio group selection changes.\n * A selected value is passed as an argument to the function.\n */\n onChange?: ( arg: string ) => void;\n /**\n * Placement of the radio\n * @default "start"\n */\n radioPlacement?: "start" | "end";\n /**\n * Variant of the RadioGroup\n * @default outline\n */\n variant?: "outline" | "text";\n}\n\n/**\n * RadioGroup component for grouping radio button options\n */\nexport const RadioGroup = ({\n size = "md",\n options,\n onChange,\n className,\n selectedValue,\n disabled,\n radioPlacement = "start",\n variant = "outline"\n}: IRadioGroupProps ): ReactElement => {\n const [ selected, setSelected ] = useState<string>( selectedValue ?? "" );\n const inputId = useRef( useId());\n\n const onChangeHandler = ( event: ChangeEvent<HTMLInputElement> ): void => {\n const newSelected = event.currentTarget.checked ? event.target.value : "";\n\n setSelected( newSelected );\n onChange?.( newSelected );\n };\n\n useEffect(() => {\n if ( selectedValue ) {\n setSelected( selectedValue );\n }\n }, [selectedValue]);\n\n const { root } = radioGroupVariants({ variant });\n\n return (\n <div\n data-testid="radio-group"\n className={root({ class: [\n disabled && variant === "text" ? "bg-secondary-50 dark:bg-secondary-700 dark:text-secondary-400" : "",\n className\n ] })}>\n {options?.map(( option, index ) => {\n const { id, name, value, label, className: optionClassName, disabled: optionDisabled, ...restOptionProps } = option;\n const isChecked = selected === value;\n const isDisabled = disabled || optionDisabled;\n\n const { item, label: labelClass } = radioGroupVariants({\n variant,\n size,\n checked: isChecked && variant === "outline",\n disabled: isDisabled && variant === "outline"\n });\n\n const renderRadio = (\n <Radio\n size={size}\n name={name}\n id={id || inputId.current}\n value={value}\n onChange={onChangeHandler}\n disabled={isDisabled}\n checked={isChecked}\n className={size === "sm" ? "mt-[2px]" : undefined}\n {...restOptionProps}\n />\n );\n\n return (\n <label\n key={index}\n htmlFor={id || inputId.current}\n className={item({ class: [\n index !== 0 && variant === "text" ? "pt-0" : "",\n optionClassName\n ] })}\n >\n {radioPlacement === "start" && renderRadio}\n {label && (\n <div className={labelClass()}>\n {label}\n </div>\n )}\n {radioPlacement === "end" && renderRadio}\n </label>\n );\n })}\n </div>\n );\n};\n\nRadioGroup.displayName = "RadioGroup";\n'
|
|
2870
2831
|
},
|
|
2871
2832
|
{
|
|
2872
2833
|
"name": "index.ts",
|
|
2873
|
-
"content": 'export { RadioGroup, type IRadioGroupProps, type TRadioGroupSize } from "./radioGroup";\nexport { radioGroupVariants
|
|
2834
|
+
"content": 'export { RadioGroup, type IRadioGroupProps, type TRadioGroupSize } from "./radioGroup";\nexport { radioGroupVariants } from "./radioGroupVariants";'
|
|
2874
2835
|
},
|
|
2875
2836
|
{
|
|
2876
2837
|
"name": "README.md",
|
|
@@ -2883,9 +2844,10 @@ export const radioLabelVariants = cva(
|
|
|
2883
2844
|
"name": "scrollbar",
|
|
2884
2845
|
"description": "The Scrollbar component provides a custom-styled scrollbar for content containers. **When to use:** - Custom scrollable areas - Chat interfaces - Code editors - Long content lists - Panels with overflow **Component Architecture:** - Styled with Tailwind CSS and cva - Cross-browser compatible - Customizable appearance - Smooth scrolling",
|
|
2885
2846
|
"dependencies": [
|
|
2886
|
-
"
|
|
2847
|
+
"tailwind-variants",
|
|
2887
2848
|
"react",
|
|
2888
|
-
"react-merge-refs"
|
|
2849
|
+
"react-merge-refs",
|
|
2850
|
+
"tailwind-merge"
|
|
2889
2851
|
],
|
|
2890
2852
|
"internalDependencies": [],
|
|
2891
2853
|
"files": [
|
|
@@ -2895,144 +2857,64 @@ export const radioLabelVariants = cva(
|
|
|
2895
2857
|
},
|
|
2896
2858
|
{
|
|
2897
2859
|
"name": "scrollbarVariants.ts",
|
|
2898
|
-
"content": `import {
|
|
2899
|
-
|
|
2900
|
-
export const scrollbarVariants = cva(
|
|
2901
|
-
"w-full h-full overflow-hidden relative",
|
|
2902
|
-
{
|
|
2903
|
-
variants: {
|
|
2904
|
-
visibility: {
|
|
2905
|
-
default: "group",
|
|
2906
|
-
alwaysVisible: ""
|
|
2907
|
-
}
|
|
2908
|
-
},
|
|
2909
|
-
defaultVariants: {
|
|
2910
|
-
visibility: "default"
|
|
2911
|
-
}
|
|
2912
|
-
}
|
|
2913
|
-
);
|
|
2914
|
-
|
|
2915
|
-
export const scrollbarContentVariants = cva(
|
|
2916
|
-
"overflow-auto w-full h-full [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
|
|
2917
|
-
);
|
|
2918
|
-
|
|
2919
|
-
export const scrollbarYContainerVariants = cva(
|
|
2920
|
-
"border-none absolute top-0 transition-all duration-200 ease-linear w-2 right-0 rtl:left-0 rtl:right-auto h-[calc(100%-0.5rem)]",
|
|
2921
|
-
{
|
|
2922
|
-
variants: {
|
|
2923
|
-
visibility: {
|
|
2924
|
-
default: "",
|
|
2925
|
-
alwaysVisible: ""
|
|
2926
|
-
},
|
|
2927
|
-
active: {
|
|
2928
|
-
true: "w-2 md:w-3"
|
|
2929
|
-
}
|
|
2930
|
-
},
|
|
2931
|
-
defaultVariants: {
|
|
2932
|
-
visibility: "default",
|
|
2933
|
-
active: false
|
|
2934
|
-
}
|
|
2935
|
-
}
|
|
2936
|
-
);
|
|
2937
|
-
|
|
2938
|
-
export const scrollbarTrackYVariants = cva(
|
|
2939
|
-
"transition-opacity duration-200 ease-linear h-full absolute top-0 w-full rounded-full" +
|
|
2940
|
-
" bg-secondary-100 dark:bg-secondary-500",
|
|
2941
|
-
{
|
|
2942
|
-
variants: {
|
|
2943
|
-
visibility: {
|
|
2944
|
-
default: "opacity-0 [.group:hover_>_[data-testid='scrollbar-y']_>&]:opacity-100",
|
|
2945
|
-
alwaysVisible: "opacity-100"
|
|
2946
|
-
}
|
|
2947
|
-
},
|
|
2948
|
-
defaultVariants: {
|
|
2949
|
-
visibility: "default"
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
);
|
|
2953
|
-
|
|
2954
|
-
export const scrollbarThumbYVariants = cva(
|
|
2955
|
-
"transition-opacity duration-200 ease-linear rounded-full cursor-grab absolute w-full" +
|
|
2956
|
-
" border border-secondary-100 bg-secondary-300 dark:border-secondary-500 dark:bg-secondary-400",
|
|
2957
|
-
{
|
|
2958
|
-
variants: {
|
|
2959
|
-
visibility: {
|
|
2960
|
-
default: "opacity-0 [.group:hover_>_[data-testid='scrollbar-y']_>&]:opacity-100",
|
|
2961
|
-
alwaysVisible: "opacity-100"
|
|
2962
|
-
},
|
|
2963
|
-
active: {
|
|
2964
|
-
true: "cursor-grabbing"
|
|
2965
|
-
}
|
|
2966
|
-
},
|
|
2967
|
-
defaultVariants: {
|
|
2968
|
-
visibility: "default",
|
|
2969
|
-
active: false
|
|
2970
|
-
}
|
|
2971
|
-
}
|
|
2972
|
-
);
|
|
2973
|
-
|
|
2974
|
-
export const scrollbarXContainerVariants = cva(
|
|
2975
|
-
"absolute bottom-0 transition-all duration-200 ease-linear left-0 rtl:right-0 h-2 w-[calc(100%-0.5rem)]",
|
|
2976
|
-
{
|
|
2977
|
-
variants: {
|
|
2978
|
-
visibility: {
|
|
2979
|
-
default: "",
|
|
2980
|
-
alwaysVisible: ""
|
|
2981
|
-
},
|
|
2982
|
-
active: {
|
|
2983
|
-
true: "h-2 md:h-3"
|
|
2984
|
-
}
|
|
2985
|
-
},
|
|
2986
|
-
defaultVariants: {
|
|
2987
|
-
visibility: "default",
|
|
2988
|
-
active: false
|
|
2989
|
-
}
|
|
2990
|
-
}
|
|
2991
|
-
);
|
|
2860
|
+
"content": `import { tv } from "tailwind-variants";
|
|
2992
2861
|
|
|
2993
|
-
export const
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
2862
|
+
export const scrollbarVariants = tv({
|
|
2863
|
+
slots: {
|
|
2864
|
+
root: "w-full h-full overflow-hidden relative",
|
|
2865
|
+
content: "overflow-auto w-full h-full [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden",
|
|
2866
|
+
yContainer: "border-none absolute top-0 transition-all duration-200 ease-linear w-2 right-0 rtl:left-0 rtl:right-auto h-[calc(100%-0.5rem)]",
|
|
2867
|
+
trackY: "transition-opacity duration-200 ease-linear h-full absolute top-0 w-full rounded-full bg-secondary-100 dark:bg-secondary-500",
|
|
2868
|
+
thumbY: [
|
|
2869
|
+
"transition-opacity duration-200 ease-linear rounded-full cursor-grab absolute w-full",
|
|
2870
|
+
"border border-secondary-100 bg-secondary-300 dark:border-secondary-500 dark:bg-secondary-400"
|
|
2871
|
+
],
|
|
2872
|
+
xContainer: "absolute bottom-0 transition-all duration-200 ease-linear left-0 rtl:right-0 h-2 w-[calc(100%-0.5rem)]",
|
|
2873
|
+
trackX: "transition-opacity duration-200 ease-linear h-full absolute top-0 rounded-full w-full bg-secondary-100 dark:bg-secondary-500",
|
|
2874
|
+
thumbX: [
|
|
2875
|
+
"transition-opacity duration-200 ease-linear border rounded-full cursor-grab absolute h-full",
|
|
2876
|
+
"border-secondary-100 bg-secondary-300 dark:border-secondary-500 dark:bg-secondary-400"
|
|
2877
|
+
]
|
|
2878
|
+
},
|
|
2879
|
+
variants: {
|
|
2880
|
+
visibility: {
|
|
2881
|
+
default: {
|
|
2882
|
+
root: "group",
|
|
2883
|
+
trackY: "opacity-0 [.group:hover_>_[data-testid='scrollbar-y']_>&]:opacity-100",
|
|
2884
|
+
thumbY: "opacity-0 [.group:hover_>_[data-testid='scrollbar-y']_>&]:opacity-100",
|
|
2885
|
+
trackX: "opacity-0 [.group:hover_>_[data-testid='scrollbar-x']_>&]:opacity-100",
|
|
2886
|
+
thumbX: "opacity-0 [.group:hover_>_[data-testid='scrollbar-x']_>&]:opacity-100"
|
|
2887
|
+
},
|
|
2888
|
+
alwaysVisible: {
|
|
2889
|
+
trackY: "opacity-100",
|
|
2890
|
+
thumbY: "opacity-100",
|
|
2891
|
+
trackX: "opacity-100",
|
|
2892
|
+
thumbX: "opacity-100"
|
|
3001
2893
|
}
|
|
3002
2894
|
},
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
export const scrollbarThumbXVariants = cva(
|
|
3010
|
-
"transition-opacity duration-200 ease-linear border rounded-full cursor-grab absolute h-full" +
|
|
3011
|
-
" border-secondary-100 bg-secondary-300 dark:border-secondary-500 dark:bg-secondary-400",
|
|
3012
|
-
{
|
|
3013
|
-
variants: {
|
|
3014
|
-
visibility: {
|
|
3015
|
-
default: "opacity-0 [.group:hover_>_[data-testid='scrollbar-x']_>&]:opacity-100",
|
|
3016
|
-
alwaysVisible: "opacity-100"
|
|
3017
|
-
},
|
|
3018
|
-
active: {
|
|
3019
|
-
true: "cursor-grabbing"
|
|
2895
|
+
active: {
|
|
2896
|
+
true: {
|
|
2897
|
+
yContainer: "w-2 md:w-3",
|
|
2898
|
+
thumbY: "cursor-grabbing",
|
|
2899
|
+
xContainer: "h-2 md:h-3",
|
|
2900
|
+
thumbX: "cursor-grabbing"
|
|
3020
2901
|
}
|
|
3021
|
-
},
|
|
3022
|
-
defaultVariants: {
|
|
3023
|
-
visibility: "default",
|
|
3024
|
-
active: false
|
|
3025
2902
|
}
|
|
2903
|
+
},
|
|
2904
|
+
defaultVariants: {
|
|
2905
|
+
visibility: "default",
|
|
2906
|
+
active: false
|
|
3026
2907
|
}
|
|
3027
|
-
)
|
|
2908
|
+
});
|
|
2909
|
+
`
|
|
3028
2910
|
},
|
|
3029
2911
|
{
|
|
3030
2912
|
"name": "scrollbar.tsx",
|
|
3031
|
-
"content": 'import * as React from "react";\nimport { useRef, useEffect, useCallback, type ElementType, useState, forwardRef } from "react";\nimport { cn, findParentAttribute } from "@utils";\nimport { useWindowSize } from "@hooks";\nimport mergeRefs from "react-merge-refs";\nimport { keyboardKeys, mobileBreakPoint } from "@constants";\nimport { updateCursor, resetCursor, handleScrollToHelper } from "./utils";\nimport {\n scrollbarVariants,\n scrollbarContentVariants,\n scrollbarYContainerVariants,\n scrollbarTrackYVariants,\n scrollbarThumbYVariants,\n scrollbarXContainerVariants,\n scrollbarTrackXVariants,\n scrollbarThumbXVariants\n} from "./scrollbarVariants";\n\n// Avoid void elements that can\'t have children\ntype TVoidElements = "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link" |\n "meta" | "param" | "source" | "track" | "wbr" | "audio" | "video" | "canvas" | "iframe" |\n "embed" | "object" | "param" | "picture" | "portal" | "svg" | "math" | "svg";\n\n/**\n * Props for the Scrollbar component\n */\nexport interface IScrollbarProps extends Omit<React.HTMLAttributes<HTMLElement>, "as"> {\n /**\n * children to render inside the scroll container\n */\n children: React.ReactNode;\n\n /**\n * The type of tag to render for the scroll container, the default value is "div".\n * @default div\n */\n as?: keyof Omit<JSX.IntrinsicElements, TVoidElements>;\n\n /**\n * Overwrite the children container styles by passing space separated class names.\n * @optional\n */\n className?: string;\n\n /**\n * Pass a boolean value to set the visibility of the scroller, by default the scrollbar will be visible on content hover.\n * @optional\n * @default false\n */\n alwaysVisible?: boolean;\n\n /**\n * Set it to true if you want to directly scroll to the click position,\n * by default the scroller will move in the direction of the click\n * @default false\n */\n scrollToClickPosition?: boolean;\n\n /**\n * Optional ID for the scrollbar container\n */\n id?: string;\n}\n\n/**\n * Scrollbar component provides a custom scrollbar with support for both vertical and horizontal scrolling.\n * It includes features like custom styling, touch support, RTL support, and keyboard navigation.\n */\nexport const Scrollbar = forwardRef<HTMLElement, IScrollbarProps>(\n ({\n children,\n as = "div",\n className,\n alwaysVisible = false,\n scrollToClickPosition = false,\n id,\n ...rest\n }, ref ) => {\n // Set up the element tag\n const Tag = as as ElementType;\n\n // Refs for DOM elements\n const scrollbarContainerRef = useRef<HTMLDivElement>( null );\n const contentRef = useRef<HTMLDivElement>( null );\n\n const trackRefY = useRef<HTMLDivElement>( null );\n const thumbRefY = useRef<HTMLDivElement>( null );\n const scrollerContainerRefY = useRef<HTMLDivElement>( null );\n\n const trackRefX = useRef<HTMLDivElement>( null );\n const thumbRefX = useRef<HTMLDivElement>( null );\n const scrollerContainerRefX = useRef<HTMLDivElement>( null );\n\n // State for RTL and mobile detection\n const [ isRtl, setIsRtl ] = useState<boolean>( false );\n const { width } = useWindowSize();\n const isMobile = width <= mobileBreakPoint;\n const scrollThumbTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>( null );\n\n // Handle the scrolling event\n const handleScrollContent = useCallback(() => {\n const contentElement = contentRef.current;\n const thumbY = thumbRefY.current;\n const thumbX = thumbRefX.current;\n\n const trackY = trackRefY.current;\n const trackX = trackRefX.current;\n\n if ( !thumbY || !contentElement || !thumbX || !trackY || !trackX ) {\n return;\n }\n\n // Calculate the scroll percentage\n const scrollPercentageY = ( contentElement.scrollTop * 100 ) / contentElement.scrollHeight;\n const scrollPercentageX = ( contentElement.scrollLeft * 100 ) / contentElement.scrollWidth;\n\n // Set the Y scrollbar thumb position\n thumbY.style.top = `${scrollPercentageY}%`;\n\n // if in case of rtl, behaviour of scrolling on X-Axis would be inversed\n if ( isRtl ) {\n const maxScrollX = contentElement.scrollWidth - contentElement.clientWidth;\n const thumbPositionX = ( maxScrollX * 100 ) / contentElement.scrollWidth;\n thumbX.style.left = `${thumbPositionX + scrollPercentageX}%`;\n } else {\n thumbX.style.left = `${scrollPercentageX}%`;\n }\n\n if ( isMobile ) {\n // For mobile browsers, show the scroll thumb immediately\n thumbY.style.opacity = "1";\n thumbX.style.opacity = "1";\n trackY.style.opacity = "1";\n trackX.style.opacity = "1";\n\n // Clear any existing timeout\n if ( scrollThumbTimeoutRef.current ) {\n clearTimeout( scrollThumbTimeoutRef.current );\n }\n\n // Set a new timeout to hide the scroll thumb after 1 second of inactivity\n scrollThumbTimeoutRef.current = setTimeout(() => {\n thumbY.style.opacity = "0";\n thumbX.style.opacity = "0";\n trackY.style.opacity = "0";\n trackX.style.opacity = "0";\n }, 1000 );\n }\n }, [ isMobile, isRtl ]);\n\n /**\n * Handle the scrollbar track click\n * When the scrollbar track is clicked, it captures the click\'s position and scrolls toward it.\n */\n const handleClickTrack = useCallback(\n ( event: React.MouseEvent<HTMLDivElement>, scrollDirection: "X" | "Y" ) => {\n const contentElement = contentRef.current;\n const trackY = trackRefY.current;\n const thumbY = thumbRefY.current;\n const trackX = trackRefX.current;\n const thumbX = thumbRefX.current;\n\n if ( !trackY || !thumbY || !contentElement || !trackX || !thumbX ) {\n return;\n }\n\n if ( scrollToClickPosition ) {\n if ( scrollDirection === "Y" ) {\n const boundY = trackY.getBoundingClientRect();\n const percentageY = ( event.clientY - boundY.top ) / boundY.height;\n const scrollHeight = contentElement.scrollHeight - contentElement.clientHeight;\n const scrollToOffsetY = percentageY * scrollHeight;\n\n handleScrollToHelper( contentElement, scrollToOffsetY, "top" );\n } else if ( scrollDirection === "X" ) {\n const boundX = trackX.getBoundingClientRect();\n let percentageX = ( event.clientX - boundX.left ) / boundX.width;\n\n if ( isRtl ) {\n percentageX = ( event.clientX - boundX.right ) / boundX.width;\n }\n\n const scrollWidth = contentElement.scrollWidth - contentElement.clientWidth;\n const scrollToOffsetX = percentageX * scrollWidth;\n\n handleScrollToHelper( contentElement, scrollToOffsetX, "left" );\n }\n } else {\n if ( scrollDirection === "Y" ) {\n const boundY = trackY.getBoundingClientRect();\n const thumbBoundY = thumbY.getBoundingClientRect();\n const clickPositionY = event.clientY - boundY.top;\n const thumbMidY = thumbBoundY.top + ( thumbBoundY.bottom - thumbBoundY.top ) / 2;\n const scrollAmount = contentElement.clientHeight * 0.5;\n const scrollDirectionUnits = clickPositionY > thumbMidY ? scrollAmount : -scrollAmount;\n\n handleScrollToHelper( contentElement, contentElement.scrollTop + scrollDirectionUnits, "top" );\n } else if ( scrollDirection === "X" ) {\n const boundX = trackX.getBoundingClientRect();\n const thumbBoundX = thumbX.getBoundingClientRect();\n const clickPositionX = event.clientX - boundX.left;\n const thumbMidX = thumbBoundX.left + ( thumbBoundX.right - thumbBoundX.left ) / 2;\n const scrollAmount = contentElement.clientWidth * 0.5;\n const scrollDirectionUnits = clickPositionX > thumbMidX ? scrollAmount : -scrollAmount;\n\n handleScrollToHelper( contentElement, contentElement.scrollLeft + scrollDirectionUnits, "left" );\n }\n }\n },\n [ isRtl, scrollToClickPosition ]\n );\n\n /**\n * Handles the mouse down event on the scrollbar thumb to enable scrolling by dragging the thumb.\n */\n const handleMouseDown = useCallback(\n ( parentEvent: React.MouseEvent<HTMLDivElement>, scrollDirection: "X" | "Y" ) => {\n const contentElement = contentRef.current;\n const thumbY = thumbRefY.current;\n const thumbX = thumbRefX.current;\n\n if ( !thumbY || !contentElement || !thumbX ) {\n return;\n }\n\n const startPos = {\n top: contentElement.scrollTop,\n left: contentElement.scrollLeft,\n x: parentEvent.clientX,\n y: parentEvent.clientY\n };\n\n const handleMouseMove = ( childEvent: MouseEvent ) => {\n const scrollRatioY = contentElement.clientHeight / contentElement.scrollHeight;\n const scrollRatioX = contentElement.clientWidth / contentElement.scrollWidth;\n\n if ( scrollDirection === "Y" ) {\n const differenceY = childEvent.clientY - startPos.y;\n contentElement.scrollTop = startPos.top + differenceY / scrollRatioY;\n } else if ( scrollDirection === "X" ) {\n let differenceX = childEvent.clientX - startPos.x;\n if ( isRtl ) {\n differenceX = startPos.x - childEvent.clientX;\n }\n\n if ( isRtl ) {\n contentElement.scrollLeft = startPos.left - differenceX / scrollRatioX;\n } else {\n contentElement.scrollLeft = startPos.left + differenceX / scrollRatioX;\n }\n }\n updateCursor();\n };\n\n const preventSelection = ( event: Event ) => {\n event.preventDefault(); // Prevent text selection\n };\n\n // Helper function to remove and reset the events and cursor style\n const handleMouseUp = () => {\n document.body.removeEventListener( "mousemove", handleMouseMove );\n document.body.removeEventListener( "mouseup", handleMouseUp );\n document.body.removeEventListener( "selectstart", preventSelection );\n resetCursor();\n };\n\n // Attach the events to the body on mouseMove event and when mouse is released remove\n document.body.addEventListener( "mousemove", handleMouseMove );\n document.body.addEventListener( "mouseup", handleMouseUp );\n document.body.addEventListener( "selectstart", preventSelection );\n },\n [isRtl]\n );\n\n /**\n * To handle the thumb drag for screen touch devices\n */\n const handleTouchStart = useCallback(\n ( parentEvent: React.TouchEvent<HTMLDivElement>, scrollDirection: "X" | "Y" ) => {\n const contentElement = contentRef.current;\n const thumbY = thumbRefY.current;\n const thumbX = thumbRefX.current;\n\n if ( !thumbY || !contentElement || !thumbX ) {\n return;\n }\n\n const parentTouchArray = Array.from( parentEvent.targetTouches );\n const [parentTouch] = parentTouchArray;\n\n const startPos = {\n top: contentElement.scrollTop,\n left: contentElement.scrollLeft,\n x: parentTouch.clientX,\n y: parentTouch.clientY\n };\n\n const handleTouchMove = ( childEvent: TouchEvent ) => {\n const childTouchArray = Array.from( childEvent.targetTouches );\n const [childTouch] = childTouchArray;\n\n const scrollRatioY = contentElement.clientHeight / contentElement.scrollHeight;\n const scrollRatioX = contentElement.clientWidth / contentElement.scrollWidth;\n\n if ( scrollDirection === "Y" ) {\n const differenceY = childTouch?.clientY - startPos.y;\n contentElement.scrollTop = startPos.top + differenceY / scrollRatioY;\n } else if ( scrollDirection === "X" ) {\n let differenceX = childTouch?.clientX - startPos.x;\n if ( isRtl ) {\n differenceX = startPos.x - childTouch?.clientX;\n }\n\n // The scroll behaviour is reversed if the rtl value is true/enabled\n if ( isRtl ) {\n contentElement.scrollLeft = startPos.left - differenceX / scrollRatioX;\n } else {\n contentElement.scrollLeft = startPos.left + differenceX / scrollRatioX;\n }\n }\n\n updateCursor();\n };\n\n // Helper function to remove and reset the events and cursor style\n const handleTouchEnd = () => {\n if ( scrollDirection === "Y" ) {\n thumbY.removeEventListener( "touchmove", handleTouchMove );\n thumbY.removeEventListener( "touchend", handleTouchEnd );\n } else {\n thumbX.removeEventListener( "touchmove", handleTouchMove );\n thumbX.removeEventListener( "touchend", handleTouchEnd );\n }\n resetCursor();\n };\n\n // Attach the events to the Thumb Element on touchMove event and when touch is released remove\n if ( scrollDirection === "Y" ) {\n thumbY.addEventListener( "touchmove", handleTouchMove );\n thumbY.addEventListener( "touchend", handleTouchEnd );\n } else {\n thumbX.addEventListener( "touchmove", handleTouchMove );\n thumbX.addEventListener( "touchend", handleTouchEnd );\n }\n },\n [isRtl]\n );\n\n /**\n * Calculates the dimensions of the scrollbar thumb elements based on the content size and visibility.\n */\n const calculateThumbDimensions = useCallback(() => {\n const contentElement = contentRef.current;\n const thumbY = thumbRefY.current;\n const scrollbarContainerY = scrollerContainerRefY.current;\n const thumbX = thumbRefX.current;\n const scrollbarContainerX = scrollerContainerRefX.current;\n\n if ( !thumbY || !contentElement || !thumbX ) {\n return;\n }\n\n const scrollRatioY = contentElement.clientHeight / contentElement.scrollHeight;\n if ( scrollRatioY >= 1 ) {\n scrollbarContainerY?.classList.add( "invisible" );\n } else {\n scrollbarContainerY?.classList.remove( "invisible" );\n thumbY.style.height = `${scrollRatioY * 100}%`;\n }\n\n const scrollRatioX = contentElement.clientWidth / contentElement.scrollWidth;\n if ( scrollRatioX >= 1 ) {\n scrollbarContainerX?.classList.add( "invisible" );\n } else {\n scrollbarContainerX?.classList.remove( "invisible" );\n thumbX.style.width = `${scrollRatioX * 100}%`;\n }\n }, []);\n\n // Check for RTL direction\n useEffect(() => {\n const scrollbarContainerElement = scrollbarContainerRef.current;\n if ( scrollbarContainerElement ) {\n const dir = findParentAttribute( scrollbarContainerElement, "dir" );\n setIsRtl( dir === "rtl" );\n }\n }, [scrollbarContainerRef]);\n\n // Calculate initial dimensions and handle RTL positioning\n useEffect(() => {\n calculateThumbDimensions();\n\n const contentElement = contentRef.current;\n const thumbX = thumbRefX.current;\n\n if ( !contentElement || !thumbX ) {\n return;\n }\n\n if ( isRtl ) {\n const maxScrollX = contentElement.scrollWidth - contentElement.clientWidth;\n const thumbPositionX = ( maxScrollX * 100 ) / contentElement.scrollWidth;\n thumbX.style.left = `${thumbPositionX}%`;\n }\n }, [ calculateThumbDimensions, isRtl ]);\n\n // Set up observers and keyboard event handling\n useEffect(() => {\n const mutationObserver = new MutationObserver( calculateThumbDimensions );\n const resizeObserver = new ResizeObserver( calculateThumbDimensions );\n\n if ( contentRef.current ) {\n mutationObserver.observe( contentRef.current, { childList: true, subtree: true });\n resizeObserver.observe( contentRef.current );\n }\n\n const scrollbarContainerElement = scrollbarContainerRef.current;\n\n const handleKeyDown = ( event: KeyboardEvent ) => {\n const { key } = event;\n const contentElement = contentRef.current;\n\n if ( !contentElement ) {\n return;\n }\n\n const scrollStep = 50;\n\n switch ( key ) {\n case keyboardKeys.arrowUp: {\n handleScrollToHelper( contentElement, contentElement.scrollTop - scrollStep, "top" );\n break;\n }\n case keyboardKeys.arrowDown: {\n handleScrollToHelper( contentElement, contentElement.scrollTop + scrollStep, "top" );\n break;\n }\n case keyboardKeys.arrowLeft: {\n handleScrollToHelper( contentElement, contentElement.scrollLeft - scrollStep, "left" );\n break;\n }\n case keyboardKeys.arrowRight: {\n handleScrollToHelper( contentElement, contentElement.scrollLeft + scrollStep, "left" );\n break;\n }\n case keyboardKeys.pageUp: {\n handleScrollToHelper( contentElement, contentElement.scrollTop - contentElement.clientHeight, "top" );\n break;\n }\n case keyboardKeys.pageDown: {\n handleScrollToHelper( contentElement, contentElement.scrollTop + contentElement.clientHeight, "top" );\n break;\n }\n case keyboardKeys.home: {\n handleScrollToHelper( contentElement, 0, "top" );\n break;\n }\n case keyboardKeys.end: {\n handleScrollToHelper( contentElement, contentElement.scrollHeight, "top" );\n break;\n }\n default:\n break;\n }\n };\n\n if ( scrollbarContainerElement ) {\n scrollbarContainerElement.addEventListener( "keydown", handleKeyDown );\n }\n\n return () => {\n mutationObserver.disconnect();\n resizeObserver.disconnect();\n\n if ( scrollbarContainerElement ) {\n scrollbarContainerElement.removeEventListener( "keydown", handleKeyDown );\n }\n };\n }, [calculateThumbDimensions]);\n\n const visibility = alwaysVisible ? "alwaysVisible" : "default";\n\n return (\n <Tag\n id={id}\n data-testid="scrollbar"\n className={cn( scrollbarVariants({ visibility }))}\n ref={mergeRefs([ ref, scrollbarContainerRef ])}\n {...rest}\n >\n <div\n className={cn( scrollbarContentVariants(), className )}\n ref={contentRef}\n onScroll={handleScrollContent}\n >\n {children}\n </div>\n\n {/* y-axis scrollbar */}\n <div\n ref={scrollerContainerRefY}\n data-testid="scrollbar-y"\n className={cn( scrollbarYContainerVariants({ visibility }))}\n >\n <div\n data-testid="scrollbar-track-y"\n className={cn( scrollbarTrackYVariants({ visibility }))}\n ref={trackRefY}\n onMouseDown={( event ) => handleClickTrack( event, "Y" )}\n />\n <div\n data-testid="scrollbar-thumb-y"\n className={cn( scrollbarThumbYVariants({ visibility }))}\n ref={thumbRefY}\n onMouseDown={( event ) => handleMouseDown( event, "Y" )}\n onTouchStart={( event ) => handleTouchStart( event, "Y" )}\n />\n </div>\n\n {/* x-axis scrollbar */}\n <div\n data-testid="scrollbar-x"\n ref={scrollerContainerRefX}\n className={cn( scrollbarXContainerVariants({ visibility }))}\n >\n <div\n className={cn( scrollbarTrackXVariants({ visibility }))}\n ref={trackRefX}\n onMouseDown={( event ) => handleClickTrack( event, "X" )}\n />\n <div\n data-testid="scrollbar-thumb-x"\n className={cn( scrollbarThumbXVariants({ visibility }))}\n ref={thumbRefX}\n onMouseDown={( event ) => handleMouseDown( event, "X" )}\n onTouchStart={( event ) => handleTouchStart( event, "X" )}\n />\n </div>\n </Tag>\n );\n }\n);\n\nScrollbar.displayName = "Scrollbar";'
|
|
2913
|
+
"content": 'import * as React from "react";\nimport { useRef, useEffect, useCallback, type ElementType, useState, forwardRef } from "react";\nimport { findParentAttribute } from "@utils";\nimport { useWindowSize } from "@hooks";\nimport mergeRefs from "react-merge-refs";\nimport { keyboardKeys, mobileBreakPoint } from "@constants";\nimport { updateCursor, resetCursor, handleScrollToHelper } from "./utils";\nimport { scrollbarVariants } from "./scrollbarVariants";\n\n// Avoid void elements that can\'t have children\ntype TVoidElements = "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link" |\n "meta" | "param" | "source" | "track" | "wbr" | "audio" | "video" | "canvas" | "iframe" |\n "embed" | "object" | "param" | "picture" | "portal" | "svg" | "math" | "svg";\n\n/**\n * Props for the Scrollbar component\n */\nexport interface IScrollbarProps extends Omit<React.HTMLAttributes<HTMLElement>, "as"> {\n /**\n * children to render inside the scroll container\n */\n children: React.ReactNode;\n\n /**\n * The type of tag to render for the scroll container, the default value is "div".\n * @default div\n */\n as?: keyof Omit<JSX.IntrinsicElements, TVoidElements>;\n\n /**\n * Overwrite the children container styles by passing space separated class names.\n * @optional\n */\n className?: string;\n\n /**\n * Pass a boolean value to set the visibility of the scroller, by default the scrollbar will be visible on content hover.\n * @optional\n * @default false\n */\n alwaysVisible?: boolean;\n\n /**\n * Set it to true if you want to directly scroll to the click position,\n * by default the scroller will move in the direction of the click\n * @default false\n */\n scrollToClickPosition?: boolean;\n\n /**\n * Optional ID for the scrollbar container\n */\n id?: string;\n}\n\n/**\n * Scrollbar component provides a custom scrollbar with support for both vertical and horizontal scrolling.\n * It includes features like custom styling, touch support, RTL support, and keyboard navigation.\n */\nexport const Scrollbar = forwardRef<HTMLElement, IScrollbarProps>(\n ({\n children,\n as = "div",\n className,\n alwaysVisible = false,\n scrollToClickPosition = false,\n id,\n ...rest\n }, ref ) => {\n // Set up the element tag\n const Tag = as as ElementType;\n\n // Refs for DOM elements\n const scrollbarContainerRef = useRef<HTMLDivElement>( null );\n const contentRef = useRef<HTMLDivElement>( null );\n\n const trackRefY = useRef<HTMLDivElement>( null );\n const thumbRefY = useRef<HTMLDivElement>( null );\n const scrollerContainerRefY = useRef<HTMLDivElement>( null );\n\n const trackRefX = useRef<HTMLDivElement>( null );\n const thumbRefX = useRef<HTMLDivElement>( null );\n const scrollerContainerRefX = useRef<HTMLDivElement>( null );\n\n // State for RTL and mobile detection\n const [ isRtl, setIsRtl ] = useState<boolean>( false );\n const { width } = useWindowSize();\n const isMobile = width <= mobileBreakPoint;\n const scrollThumbTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>( null );\n\n // Handle the scrolling event\n const handleScrollContent = useCallback(() => {\n const contentElement = contentRef.current;\n const thumbY = thumbRefY.current;\n const thumbX = thumbRefX.current;\n\n const trackY = trackRefY.current;\n const trackX = trackRefX.current;\n\n if ( !thumbY || !contentElement || !thumbX || !trackY || !trackX ) {\n return;\n }\n\n // Calculate the scroll percentage\n const scrollPercentageY = ( contentElement.scrollTop * 100 ) / contentElement.scrollHeight;\n const scrollPercentageX = ( contentElement.scrollLeft * 100 ) / contentElement.scrollWidth;\n\n // Set the Y scrollbar thumb position\n thumbY.style.top = `${scrollPercentageY}%`;\n\n // if in case of rtl, behaviour of scrolling on X-Axis would be inversed\n if ( isRtl ) {\n const maxScrollX = contentElement.scrollWidth - contentElement.clientWidth;\n const thumbPositionX = ( maxScrollX * 100 ) / contentElement.scrollWidth;\n thumbX.style.left = `${thumbPositionX + scrollPercentageX}%`;\n } else {\n thumbX.style.left = `${scrollPercentageX}%`;\n }\n\n if ( isMobile ) {\n // For mobile browsers, show the scroll thumb immediately\n thumbY.style.opacity = "1";\n thumbX.style.opacity = "1";\n trackY.style.opacity = "1";\n trackX.style.opacity = "1";\n\n // Clear any existing timeout\n if ( scrollThumbTimeoutRef.current ) {\n clearTimeout( scrollThumbTimeoutRef.current );\n }\n\n // Set a new timeout to hide the scroll thumb after 1 second of inactivity\n scrollThumbTimeoutRef.current = setTimeout(() => {\n thumbY.style.opacity = "0";\n thumbX.style.opacity = "0";\n trackY.style.opacity = "0";\n trackX.style.opacity = "0";\n }, 1000 );\n }\n }, [ isMobile, isRtl ]);\n\n /**\n * Handle the scrollbar track click\n * When the scrollbar track is clicked, it captures the click\'s position and scrolls toward it.\n */\n const handleClickTrack = useCallback(\n ( event: React.MouseEvent<HTMLDivElement>, scrollDirection: "X" | "Y" ) => {\n const contentElement = contentRef.current;\n const trackY = trackRefY.current;\n const thumbY = thumbRefY.current;\n const trackX = trackRefX.current;\n const thumbX = thumbRefX.current;\n\n if ( !trackY || !thumbY || !contentElement || !trackX || !thumbX ) {\n return;\n }\n\n if ( scrollToClickPosition ) {\n if ( scrollDirection === "Y" ) {\n const boundY = trackY.getBoundingClientRect();\n const percentageY = ( event.clientY - boundY.top ) / boundY.height;\n const scrollHeight = contentElement.scrollHeight - contentElement.clientHeight;\n const scrollToOffsetY = percentageY * scrollHeight;\n\n handleScrollToHelper( contentElement, scrollToOffsetY, "top" );\n } else if ( scrollDirection === "X" ) {\n const boundX = trackX.getBoundingClientRect();\n let percentageX = ( event.clientX - boundX.left ) / boundX.width;\n\n if ( isRtl ) {\n percentageX = ( event.clientX - boundX.right ) / boundX.width;\n }\n\n const scrollWidth = contentElement.scrollWidth - contentElement.clientWidth;\n const scrollToOffsetX = percentageX * scrollWidth;\n\n handleScrollToHelper( contentElement, scrollToOffsetX, "left" );\n }\n } else {\n if ( scrollDirection === "Y" ) {\n const boundY = trackY.getBoundingClientRect();\n const thumbBoundY = thumbY.getBoundingClientRect();\n const clickPositionY = event.clientY - boundY.top;\n const thumbMidY = thumbBoundY.top + ( thumbBoundY.bottom - thumbBoundY.top ) / 2;\n const scrollAmount = contentElement.clientHeight * 0.5;\n const scrollDirectionUnits = clickPositionY > thumbMidY ? scrollAmount : -scrollAmount;\n\n handleScrollToHelper( contentElement, contentElement.scrollTop + scrollDirectionUnits, "top" );\n } else if ( scrollDirection === "X" ) {\n const boundX = trackX.getBoundingClientRect();\n const thumbBoundX = thumbX.getBoundingClientRect();\n const clickPositionX = event.clientX - boundX.left;\n const thumbMidX = thumbBoundX.left + ( thumbBoundX.right - thumbBoundX.left ) / 2;\n const scrollAmount = contentElement.clientWidth * 0.5;\n const scrollDirectionUnits = clickPositionX > thumbMidX ? scrollAmount : -scrollAmount;\n\n handleScrollToHelper( contentElement, contentElement.scrollLeft + scrollDirectionUnits, "left" );\n }\n }\n },\n [ isRtl, scrollToClickPosition ]\n );\n\n /**\n * Handles the mouse down event on the scrollbar thumb to enable scrolling by dragging the thumb.\n */\n const handleMouseDown = useCallback(\n ( parentEvent: React.MouseEvent<HTMLDivElement>, scrollDirection: "X" | "Y" ) => {\n const contentElement = contentRef.current;\n const thumbY = thumbRefY.current;\n const thumbX = thumbRefX.current;\n\n if ( !thumbY || !contentElement || !thumbX ) {\n return;\n }\n\n const startPos = {\n top: contentElement.scrollTop,\n left: contentElement.scrollLeft,\n x: parentEvent.clientX,\n y: parentEvent.clientY\n };\n\n const handleMouseMove = ( childEvent: MouseEvent ) => {\n const scrollRatioY = contentElement.clientHeight / contentElement.scrollHeight;\n const scrollRatioX = contentElement.clientWidth / contentElement.scrollWidth;\n\n if ( scrollDirection === "Y" ) {\n const differenceY = childEvent.clientY - startPos.y;\n contentElement.scrollTop = startPos.top + differenceY / scrollRatioY;\n } else if ( scrollDirection === "X" ) {\n let differenceX = childEvent.clientX - startPos.x;\n if ( isRtl ) {\n differenceX = startPos.x - childEvent.clientX;\n }\n\n if ( isRtl ) {\n contentElement.scrollLeft = startPos.left - differenceX / scrollRatioX;\n } else {\n contentElement.scrollLeft = startPos.left + differenceX / scrollRatioX;\n }\n }\n updateCursor();\n };\n\n const preventSelection = ( event: Event ) => {\n event.preventDefault(); // Prevent text selection\n };\n\n // Helper function to remove and reset the events and cursor style\n const handleMouseUp = () => {\n document.body.removeEventListener( "mousemove", handleMouseMove );\n document.body.removeEventListener( "mouseup", handleMouseUp );\n document.body.removeEventListener( "selectstart", preventSelection );\n resetCursor();\n };\n\n // Attach the events to the body on mouseMove event and when mouse is released remove\n document.body.addEventListener( "mousemove", handleMouseMove );\n document.body.addEventListener( "mouseup", handleMouseUp );\n document.body.addEventListener( "selectstart", preventSelection );\n },\n [isRtl]\n );\n\n /**\n * To handle the thumb drag for screen touch devices\n */\n const handleTouchStart = useCallback(\n ( parentEvent: React.TouchEvent<HTMLDivElement>, scrollDirection: "X" | "Y" ) => {\n const contentElement = contentRef.current;\n const thumbY = thumbRefY.current;\n const thumbX = thumbRefX.current;\n\n if ( !thumbY || !contentElement || !thumbX ) {\n return;\n }\n\n const parentTouchArray = Array.from( parentEvent.targetTouches );\n const [parentTouch] = parentTouchArray;\n\n const startPos = {\n top: contentElement.scrollTop,\n left: contentElement.scrollLeft,\n x: parentTouch.clientX,\n y: parentTouch.clientY\n };\n\n const handleTouchMove = ( childEvent: TouchEvent ) => {\n const childTouchArray = Array.from( childEvent.targetTouches );\n const [childTouch] = childTouchArray;\n\n const scrollRatioY = contentElement.clientHeight / contentElement.scrollHeight;\n const scrollRatioX = contentElement.clientWidth / contentElement.scrollWidth;\n\n if ( scrollDirection === "Y" ) {\n const differenceY = childTouch?.clientY - startPos.y;\n contentElement.scrollTop = startPos.top + differenceY / scrollRatioY;\n } else if ( scrollDirection === "X" ) {\n let differenceX = childTouch?.clientX - startPos.x;\n if ( isRtl ) {\n differenceX = startPos.x - childTouch?.clientX;\n }\n\n // The scroll behaviour is reversed if the rtl value is true/enabled\n if ( isRtl ) {\n contentElement.scrollLeft = startPos.left - differenceX / scrollRatioX;\n } else {\n contentElement.scrollLeft = startPos.left + differenceX / scrollRatioX;\n }\n }\n\n updateCursor();\n };\n\n // Helper function to remove and reset the events and cursor style\n const handleTouchEnd = () => {\n if ( scrollDirection === "Y" ) {\n thumbY.removeEventListener( "touchmove", handleTouchMove );\n thumbY.removeEventListener( "touchend", handleTouchEnd );\n } else {\n thumbX.removeEventListener( "touchmove", handleTouchMove );\n thumbX.removeEventListener( "touchend", handleTouchEnd );\n }\n resetCursor();\n };\n\n // Attach the events to the Thumb Element on touchMove event and when touch is released remove\n if ( scrollDirection === "Y" ) {\n thumbY.addEventListener( "touchmove", handleTouchMove );\n thumbY.addEventListener( "touchend", handleTouchEnd );\n } else {\n thumbX.addEventListener( "touchmove", handleTouchMove );\n thumbX.addEventListener( "touchend", handleTouchEnd );\n }\n },\n [isRtl]\n );\n\n /**\n * Calculates the dimensions of the scrollbar thumb elements based on the content size and visibility.\n */\n const calculateThumbDimensions = useCallback(() => {\n const contentElement = contentRef.current;\n const thumbY = thumbRefY.current;\n const scrollbarContainerY = scrollerContainerRefY.current;\n const thumbX = thumbRefX.current;\n const scrollbarContainerX = scrollerContainerRefX.current;\n\n if ( !thumbY || !contentElement || !thumbX ) {\n return;\n }\n\n const scrollRatioY = contentElement.clientHeight / contentElement.scrollHeight;\n if ( scrollRatioY >= 1 ) {\n scrollbarContainerY?.classList.add( "invisible" );\n } else {\n scrollbarContainerY?.classList.remove( "invisible" );\n thumbY.style.height = `${scrollRatioY * 100}%`;\n }\n\n const scrollRatioX = contentElement.clientWidth / contentElement.scrollWidth;\n if ( scrollRatioX >= 1 ) {\n scrollbarContainerX?.classList.add( "invisible" );\n } else {\n scrollbarContainerX?.classList.remove( "invisible" );\n thumbX.style.width = `${scrollRatioX * 100}%`;\n }\n }, []);\n\n // Check for RTL direction\n useEffect(() => {\n const scrollbarContainerElement = scrollbarContainerRef.current;\n if ( scrollbarContainerElement ) {\n const dir = findParentAttribute( scrollbarContainerElement, "dir" );\n setIsRtl( dir === "rtl" );\n }\n }, [scrollbarContainerRef]);\n\n // Calculate initial dimensions and handle RTL positioning\n useEffect(() => {\n calculateThumbDimensions();\n\n const contentElement = contentRef.current;\n const thumbX = thumbRefX.current;\n\n if ( !contentElement || !thumbX ) {\n return;\n }\n\n if ( isRtl ) {\n const maxScrollX = contentElement.scrollWidth - contentElement.clientWidth;\n const thumbPositionX = ( maxScrollX * 100 ) / contentElement.scrollWidth;\n thumbX.style.left = `${thumbPositionX}%`;\n }\n }, [ calculateThumbDimensions, isRtl ]);\n\n // Set up observers and keyboard event handling\n useEffect(() => {\n const mutationObserver = new MutationObserver( calculateThumbDimensions );\n const resizeObserver = new ResizeObserver( calculateThumbDimensions );\n\n if ( contentRef.current ) {\n mutationObserver.observe( contentRef.current, { childList: true, subtree: true });\n resizeObserver.observe( contentRef.current );\n }\n\n const scrollbarContainerElement = scrollbarContainerRef.current;\n\n const handleKeyDown = ( event: KeyboardEvent ) => {\n const { key } = event;\n const contentElement = contentRef.current;\n\n if ( !contentElement ) {\n return;\n }\n\n const scrollStep = 50;\n\n switch ( key ) {\n case keyboardKeys.arrowUp: {\n handleScrollToHelper( contentElement, contentElement.scrollTop - scrollStep, "top" );\n break;\n }\n case keyboardKeys.arrowDown: {\n handleScrollToHelper( contentElement, contentElement.scrollTop + scrollStep, "top" );\n break;\n }\n case keyboardKeys.arrowLeft: {\n handleScrollToHelper( contentElement, contentElement.scrollLeft - scrollStep, "left" );\n break;\n }\n case keyboardKeys.arrowRight: {\n handleScrollToHelper( contentElement, contentElement.scrollLeft + scrollStep, "left" );\n break;\n }\n case keyboardKeys.pageUp: {\n handleScrollToHelper( contentElement, contentElement.scrollTop - contentElement.clientHeight, "top" );\n break;\n }\n case keyboardKeys.pageDown: {\n handleScrollToHelper( contentElement, contentElement.scrollTop + contentElement.clientHeight, "top" );\n break;\n }\n case keyboardKeys.home: {\n handleScrollToHelper( contentElement, 0, "top" );\n break;\n }\n case keyboardKeys.end: {\n handleScrollToHelper( contentElement, contentElement.scrollHeight, "top" );\n break;\n }\n default:\n break;\n }\n };\n\n if ( scrollbarContainerElement ) {\n scrollbarContainerElement.addEventListener( "keydown", handleKeyDown );\n }\n\n return () => {\n mutationObserver.disconnect();\n resizeObserver.disconnect();\n\n if ( scrollbarContainerElement ) {\n scrollbarContainerElement.removeEventListener( "keydown", handleKeyDown );\n }\n };\n }, [calculateThumbDimensions]);\n\n const visibility = alwaysVisible ? "alwaysVisible" : "default";\n const {\n root,\n content,\n yContainer,\n trackY,\n thumbY,\n xContainer,\n trackX,\n thumbX\n } = scrollbarVariants({ visibility });\n\n return (\n <Tag\n id={id}\n data-testid="scrollbar"\n className={root()}\n ref={mergeRefs([ ref, scrollbarContainerRef ])}\n {...rest}\n >\n <div\n className={content({ class: className })}\n ref={contentRef}\n onScroll={handleScrollContent}\n >\n {children}\n </div>\n\n {/* y-axis scrollbar */}\n <div\n ref={scrollerContainerRefY}\n data-testid="scrollbar-y"\n className={yContainer()}\n >\n <div\n data-testid="scrollbar-track-y"\n className={trackY()}\n ref={trackRefY}\n onMouseDown={( event ) => handleClickTrack( event, "Y" )}\n />\n <div\n data-testid="scrollbar-thumb-y"\n className={thumbY()}\n ref={thumbRefY}\n onMouseDown={( event ) => handleMouseDown( event, "Y" )}\n onTouchStart={( event ) => handleTouchStart( event, "Y" )}\n />\n </div>\n\n {/* x-axis scrollbar */}\n <div\n data-testid="scrollbar-x"\n ref={scrollerContainerRefX}\n className={xContainer()}\n >\n <div\n className={trackX()}\n ref={trackRefX}\n onMouseDown={( event ) => handleClickTrack( event, "X" )}\n />\n <div\n data-testid="scrollbar-thumb-x"\n className={thumbX()}\n ref={thumbRefX}\n onMouseDown={( event ) => handleMouseDown( event, "X" )}\n onTouchStart={( event ) => handleTouchStart( event, "X" )}\n />\n </div>\n </Tag>\n );\n }\n);\n\nScrollbar.displayName = "Scrollbar";'
|
|
3032
2914
|
},
|
|
3033
2915
|
{
|
|
3034
2916
|
"name": "index.ts",
|
|
3035
|
-
"content": 'export { Scrollbar, type IScrollbarProps } from "./scrollbar";\nexport {
|
|
2917
|
+
"content": 'export { Scrollbar, type IScrollbarProps } from "./scrollbar";\nexport { scrollbarVariants } from "./scrollbarVariants";'
|
|
3036
2918
|
},
|
|
3037
2919
|
{
|
|
3038
2920
|
"name": "README.md",
|
|
@@ -3047,8 +2929,8 @@ export const scrollbarThumbXVariants = cva(
|
|
|
3047
2929
|
"dependencies": [
|
|
3048
2930
|
"react-select",
|
|
3049
2931
|
"react",
|
|
3050
|
-
"
|
|
3051
|
-
"
|
|
2932
|
+
"tailwind-variants",
|
|
2933
|
+
"tailwind-merge"
|
|
3052
2934
|
],
|
|
3053
2935
|
"internalDependencies": [
|
|
3054
2936
|
"checkbox",
|
|
@@ -3066,11 +2948,11 @@ export const scrollbarThumbXVariants = cva(
|
|
|
3066
2948
|
},
|
|
3067
2949
|
{
|
|
3068
2950
|
"name": "selectVariants.ts",
|
|
3069
|
-
"content": 'import {
|
|
2951
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const selectVariants = tv({\n slots: {\n base: "flex flex-col gap-2",\n label: "text-base text-secondary-400 font-medium dark:text-secondary-300",\n helperText: "text-xs font-normal"\n },\n variants: {\n error: {\n true: { helperText: "text-error-500 dark:text-error-400" },\n false: { helperText: "text-secondary-400 dark:text-secondary-200" }\n }\n },\n defaultVariants: {\n error: false\n }\n});\n'
|
|
3070
2952
|
},
|
|
3071
2953
|
{
|
|
3072
2954
|
"name": "select.tsx",
|
|
3073
|
-
"content": 'import * as React from "react";\nimport { type ReactNode, type ReactElement, useId, useRef, type ComponentType } from "react";\nimport ReactSelect, {\n type Props as ReactSelectProps,\n components,\n type GroupBase,\n type OptionProps,\n type MultiValueProps,\n type DropdownIndicatorProps,\n type MenuProps,\n type ValueContainerProps,\n type InputProps\n} from "react-select";\nimport CreatableSelect from "react-select/creatable";\nimport clsx from "clsx";\n// Disable exhaustive-deps: customComponents only needs to update when menuFooter changes\n\nimport { Checkbox } from "../checkbox";\nimport { ADPIcon, iconList, type TIconType } from "../adpIcon";\nimport { reactSelectCustomStyles } from "./utils";\nimport { cn } from "@utils";\nimport {\n selectVariants,\n selectLabelVariants,\n selectHelperTextVariants\n} from "./selectVariants";\n\nimport { ESelectSizes, type TSelectSize, type IOption } from "./types";\n\n/**\n * Props for the Select component\n */\nexport interface ISelectProps<\n IsMulti extends boolean = false,\n Group extends GroupBase<IOption> = GroupBase<IOption>\n> extends Omit<ReactSelectProps<IOption, IsMulti, Group>, "options" | "value" | "className"> {\n /** Unique id for the select input */\n id?: string;\n /** Custom className for wrapper */\n className?: string;\n /** Label displayed above select */\n label?: ReactNode;\n /** Select size variant */\n size?: TSelectSize;\n /** Options or groups to render */\n options?: ReactSelectProps<IOption, IsMulti, Group>["options"];\n /** Current value of the select */\n value?: ReactSelectProps<IOption, IsMulti, Group>["value"];\n /** Helper text displayed below */\n helperText?: ReactNode;\n /** Display error styling */\n error?: boolean;\n /** Disable the select */\n disabled?: boolean;\n /** Enable creatable select */\n creatable?: boolean;\n /** Field touched state */\n touched?: boolean;\n /** Max number of visible values in multi */\n maxVisibleValues?: number;\n /** Label for truncated values */\n maxVisibleValuesMsg?: string;\n /** Custom footer in menu */\n menuFooter?: ReactNode;\n /** Prefix icon for the control */\n prefixIcon?: TIconType | ReactNode;\n /** Suffix icon for the control */\n suffixIcon?: TIconType | ReactNode;\n}\n\n/**\n * Select component for single or multi selection\n */\nexport function Select<\n IsMulti extends boolean = false,\n Group extends GroupBase<IOption> = GroupBase<IOption>\n>(\n selectProps: ISelectProps<IsMulti, Group>\n): ReactElement {\n const {\n id,\n className,\n label,\n size = ESelectSizes.MD,\n options = [],\n value,\n helperText,\n error = false,\n disabled = false,\n creatable = false,\n maxVisibleValues = 3,\n maxVisibleValuesMsg,\n menuFooter,\n prefixIcon,\n suffixIcon,\n ...restProps\n } = selectProps;\n\n // Dropdown indicator icon\n const DropdownIndicator = (\n props: DropdownIndicatorProps<IOption, IsMulti, Group>\n ) => {\n const {\n selectProps: { menuIsOpen }\n } = props;\n return (\n <ADPIcon\n size="xs"\n icon={menuIsOpen ? iconList.upArrow : iconList.downArrow}\n />\n );\n };\n\n // Custom option rendering (handles select-all and checkboxes)\n const OptionComponent = ( props: OptionProps<IOption, IsMulti, Group> ) => {\n const optionStyles = props.getStyles( "option", props );\n const baseClassName = props.cx(\n {\n option: true,\n "option--is-disabled": props.isDisabled,\n "option--is-focused": props.isFocused,\n "option--is-selected": props.isSelected\n },\n props.className\n );\n const selectedValues = props.getValue();\n const areAllSelected =\n selectedValues.filter(( o ) => o.value !== "select-all" ).length ===\n props.options.length - 1;\n\n const handleClick: React.MouseEventHandler<HTMLDivElement> = ( e ) => {\n e.stopPropagation();\n if ( props.isDisabled || props.data.isFixed ) {\n return;\n }\n if ( props.data.value === "select-all" ) {\n const others = props.options.filter(\n ( o ) => "value" in o && o.value !== "select-all"\n ) as IOption[];\n if ( areAllSelected ) {\n props.clearValue();\n } else {\n props.setValue( others as any, "select-option" );\n }\n } else {\n props.selectOption( props.data );\n }\n };\n\n const handleChange: React.ChangeEventHandler<HTMLInputElement> = ( e ) => {\n e.stopPropagation();\n if ( props.isDisabled || props.data.isFixed ) {\n return;\n }\n if ( props.data.value === "select-all" ) {\n const others = props.options.filter(\n ( o ) => "value" in o && o.value !== "select-all"\n ) as IOption[];\n if ( areAllSelected ) {\n const fixed = props.options.filter(\n ( o ) => "value" in o && o.isFixed\n ) as IOption[];\n props.setValue( fixed as any, "select-option" );\n } else {\n props.setValue( others as any, "select-option" );\n }\n } else {\n props.selectOption( props.data );\n }\n };\n\n return (\n <div\n style={optionStyles as React.CSSProperties}\n className={clsx(\n baseClassName,\n "select__custom-option",\n ( props.isSelected || props.isFocused ) &&\n "dark:!bg-secondary-800",\n props.isMulti && "select__custom-option--is-multi"\n )}\n >\n {restProps.isMulti && (\n <Checkbox\n className="me-2"\n onChange={handleChange}\n checked={areAllSelected || props.isSelected}\n disabled={props.isDisabled || props.data.isFixed}\n />\n )}\n <components.Option\n {...props}\n className="!p-0"\n innerProps={{\n ...props.innerProps,\n ...( restProps.isMulti ? { onClick: handleClick } : {})\n }}\n />\n </div>\n );\n };\n\n // Custom value container to handle prefix/suffix icons and truncation\n const ValueContainer = (\n { children, ...props }: ValueContainerProps<IOption, IsMulti, Group>\n ) => {\n const values = props.getValue();\n const withData = React.Children.toArray( children ).filter(\n ( c ) => ( c as ReactElement ).props.data\n );\n const inputChild = React.Children.toArray( children ).filter(\n ( c ) => !( c as ReactElement ).props.data\n );\n return (\n <>\n {prefixIcon &&\n ( typeof prefixIcon === "string" ? (\n <ADPIcon icon={prefixIcon as TIconType} size="xs" />\n ) : (\n prefixIcon\n ))}\n <components.ValueContainer {...props}>\n {withData}\n {values.length > maxVisibleValues && (\n <div\n className={clsx(\n "select__truncated-multi-value",\n disabled && "select__truncated-multi-value--is-disabled"\n )}\n >\n +{values.length - maxVisibleValues}{" "}\n {maxVisibleValuesMsg ?? "More"}\n </div>\n )}\n {inputChild}\n </components.ValueContainer>\n {suffixIcon &&\n ( typeof suffixIcon === "string" ? (\n <ADPIcon icon={suffixIcon as TIconType} size="xs" />\n ) : (\n suffixIcon\n ))}\n </>\n );\n };\n\n // Hide extra MultiValue components\n const MultiValue = ( props: MultiValueProps<IOption> ) => {\n if ( props.index !== undefined && props.index >= maxVisibleValues ) {\n return null;\n }\n return <components.MultiValue {...props} />;\n };\n\n // Custom input to prevent backspace removing truncated values\n const Input = ( props: InputProps<IOption, IsMulti, Group> ) => {\n const handleKeyDown = ( e: React.KeyboardEvent<HTMLInputElement> ) => {\n if ( e.key === "Backspace" ) {\n const vals = props.getValue();\n if ( vals.length > maxVisibleValues ) {\n e.stopPropagation();\n e.preventDefault();\n }\n }\n };\n return <components.Input {...props} onKeyDown={handleKeyDown} />;\n };\n\n // Footer for menu if provided\n const Footer = ( props: MenuProps<IOption, IsMulti, Group> ) => {\n const { children, innerProps, innerRef } = props;\n return (\n <components.Menu {...props} innerProps={innerProps} innerRef={innerRef}>\n {children}\n {menuFooter}\n </components.Menu>\n );\n };\n\n const customComponents = {\n IndicatorSeparator: () => null,\n DropdownIndicator,\n Option: OptionComponent,\n ValueContainer,\n MultiValue,\n Input,\n ...( menuFooter ? { Menu: Footer } : {})\n };\n\n const inputId = useRef( useId());\n // unify component type for creatable and default select\n type TRSComponent = ComponentType<ReactSelectProps<IOption, IsMulti, Group>>;\n const SelectType: TRSComponent = creatable\n ? ( CreatableSelect as TRSComponent )\n : ( ReactSelect as TRSComponent );\n\n return (\n <div className={cn( selectVariants({ size, error, disabled }), className )}>\n {label !== undefined && (\n <label\n htmlFor={id || inputId.current}\n className={cn( selectLabelVariants())}\n >\n {label}\n </label>\n )}\n <SelectType\n classNamePrefix="select"\n inputId={id || inputId.current}\n options={\n restProps.isMulti && ( options as IOption[]).length\n ? ([{ value: "select-all", label: "Select All" }] as IOption[]).concat(\n options as IOption[]\n )\n : options\n }\n value={value}\n hideSelectedOptions={false}\n menuPlacement="auto"\n components={customComponents}\n className={clsx( "select", `select-${size}` )}\n styles={\n reactSelectCustomStyles<IsMulti, Group>(\n error,\n size,\n restProps.isMulti as boolean\n )\n }\n isDisabled={disabled}\n closeMenuOnSelect={!restProps.isMulti}\n {...restProps}\n />\n {helperText && (\n <div className={cn( selectHelperTextVariants({ error }))}>\n {helperText}\n </div>\n )}\n </div>\n );\n}\n\nSelect.displayName = "Select";\n// Exported types SelectProps and Option are declared above and can be re-exported via index.ts\n'
|
|
2955
|
+
"content": 'import * as React from "react";\nimport { type ReactNode, type ReactElement, useId, useRef, type ComponentType } from "react";\nimport ReactSelect, {\n type Props as ReactSelectProps,\n components,\n type GroupBase,\n type OptionProps,\n type MultiValueProps,\n type DropdownIndicatorProps,\n type MenuProps,\n type ValueContainerProps,\n type InputProps\n} from "react-select";\nimport CreatableSelect from "react-select/creatable";\nimport { cx } from "tailwind-variants";\n// Disable exhaustive-deps: customComponents only needs to update when menuFooter changes\n\nimport { Checkbox } from "../checkbox";\nimport { ADPIcon, iconList, type TIconType } from "../adpIcon";\nimport { reactSelectCustomStyles } from "./utils";\nimport { selectVariants } from "./selectVariants";\n\nimport { ESelectSizes, type TSelectSize, type IOption } from "./types";\n\n/**\n * Props for the Select component\n */\nexport interface ISelectProps<\n IsMulti extends boolean = false,\n Group extends GroupBase<IOption> = GroupBase<IOption>\n> extends Omit<ReactSelectProps<IOption, IsMulti, Group>, "options" | "value" | "className"> {\n /** Unique id for the select input */\n id?: string;\n /** Custom className for wrapper */\n className?: string;\n /** Label displayed above select */\n label?: ReactNode;\n /** Select size variant */\n size?: TSelectSize;\n /** Options or groups to render */\n options?: ReactSelectProps<IOption, IsMulti, Group>["options"];\n /** Current value of the select */\n value?: ReactSelectProps<IOption, IsMulti, Group>["value"];\n /** Helper text displayed below */\n helperText?: ReactNode;\n /** Display error styling */\n error?: boolean;\n /** Disable the select */\n disabled?: boolean;\n /** Enable creatable select */\n creatable?: boolean;\n /** Field touched state */\n touched?: boolean;\n /** Max number of visible values in multi */\n maxVisibleValues?: number;\n /** Label for truncated values */\n maxVisibleValuesMsg?: string;\n /** Custom footer in menu */\n menuFooter?: ReactNode;\n /** Prefix icon for the control */\n prefixIcon?: TIconType | ReactNode;\n /** Suffix icon for the control */\n suffixIcon?: TIconType | ReactNode;\n}\n\n/**\n * Select component for single or multi selection\n */\nexport function Select<\n IsMulti extends boolean = false,\n Group extends GroupBase<IOption> = GroupBase<IOption>\n>(\n selectProps: ISelectProps<IsMulti, Group>\n): ReactElement {\n const {\n id,\n className,\n label,\n size = ESelectSizes.MD,\n options = [],\n value,\n helperText,\n error = false,\n disabled = false,\n creatable = false,\n maxVisibleValues = 3,\n maxVisibleValuesMsg,\n menuFooter,\n prefixIcon,\n suffixIcon,\n ...restProps\n } = selectProps;\n\n // Dropdown indicator icon\n const DropdownIndicator = (\n props: DropdownIndicatorProps<IOption, IsMulti, Group>\n ) => {\n const {\n selectProps: { menuIsOpen }\n } = props;\n return (\n <ADPIcon\n size="xs"\n icon={menuIsOpen ? iconList.upArrow : iconList.downArrow}\n />\n );\n };\n\n // Custom option rendering (handles select-all and checkboxes)\n const OptionComponent = ( props: OptionProps<IOption, IsMulti, Group> ) => {\n const optionStyles = props.getStyles( "option", props );\n const baseClassName = props.cx(\n {\n option: true,\n "option--is-disabled": props.isDisabled,\n "option--is-focused": props.isFocused,\n "option--is-selected": props.isSelected\n },\n props.className\n );\n const selectedValues = props.getValue();\n const areAllSelected =\n selectedValues.filter(( o ) => o.value !== "select-all" ).length ===\n props.options.length - 1;\n\n const handleClick: React.MouseEventHandler<HTMLDivElement> = ( e ) => {\n e.stopPropagation();\n if ( props.isDisabled || props.data.isFixed ) {\n return;\n }\n if ( props.data.value === "select-all" ) {\n const others = props.options.filter(\n ( o ) => "value" in o && o.value !== "select-all"\n ) as IOption[];\n if ( areAllSelected ) {\n props.clearValue();\n } else {\n props.setValue( others as any, "select-option" );\n }\n } else {\n props.selectOption( props.data );\n }\n };\n\n const handleChange: React.ChangeEventHandler<HTMLInputElement> = ( e ) => {\n e.stopPropagation();\n if ( props.isDisabled || props.data.isFixed ) {\n return;\n }\n if ( props.data.value === "select-all" ) {\n const others = props.options.filter(\n ( o ) => "value" in o && o.value !== "select-all"\n ) as IOption[];\n if ( areAllSelected ) {\n const fixed = props.options.filter(\n ( o ) => "value" in o && o.isFixed\n ) as IOption[];\n props.setValue( fixed as any, "select-option" );\n } else {\n props.setValue( others as any, "select-option" );\n }\n } else {\n props.selectOption( props.data );\n }\n };\n\n return (\n <div\n style={optionStyles as React.CSSProperties}\n className={cx(\n baseClassName,\n "select__custom-option",\n ( props.isSelected || props.isFocused ) &&\n "dark:!bg-secondary-800",\n props.isMulti && "select__custom-option--is-multi"\n )}\n >\n {restProps.isMulti && (\n <Checkbox\n className="me-2"\n onChange={handleChange}\n checked={areAllSelected || props.isSelected}\n disabled={props.isDisabled || props.data.isFixed}\n />\n )}\n <components.Option\n {...props}\n className="!p-0"\n innerProps={{\n ...props.innerProps,\n ...( restProps.isMulti ? { onClick: handleClick } : {})\n }}\n />\n </div>\n );\n };\n\n // Custom value container to handle prefix/suffix icons and truncation\n const ValueContainer = (\n { children, ...props }: ValueContainerProps<IOption, IsMulti, Group>\n ) => {\n const values = props.getValue();\n const withData = React.Children.toArray( children ).filter(\n ( c ) => ( c as ReactElement ).props.data\n );\n const inputChild = React.Children.toArray( children ).filter(\n ( c ) => !( c as ReactElement ).props.data\n );\n return (\n <>\n {prefixIcon &&\n ( typeof prefixIcon === "string" ? (\n <ADPIcon icon={prefixIcon as TIconType} size="xs" />\n ) : (\n prefixIcon\n ))}\n <components.ValueContainer {...props}>\n {withData}\n {values.length > maxVisibleValues && (\n <div\n className={cx(\n "select__truncated-multi-value",\n disabled && "select__truncated-multi-value--is-disabled"\n )}\n >\n +{values.length - maxVisibleValues}{" "}\n {maxVisibleValuesMsg ?? "More"}\n </div>\n )}\n {inputChild}\n </components.ValueContainer>\n {suffixIcon &&\n ( typeof suffixIcon === "string" ? (\n <ADPIcon icon={suffixIcon as TIconType} size="xs" />\n ) : (\n suffixIcon\n ))}\n </>\n );\n };\n\n // Hide extra MultiValue components\n const MultiValue = ( props: MultiValueProps<IOption> ) => {\n if ( props.index !== undefined && props.index >= maxVisibleValues ) {\n return null;\n }\n return <components.MultiValue {...props} />;\n };\n\n // Custom input to prevent backspace removing truncated values\n const Input = ( props: InputProps<IOption, IsMulti, Group> ) => {\n const handleKeyDown = ( e: React.KeyboardEvent<HTMLInputElement> ) => {\n if ( e.key === "Backspace" ) {\n const vals = props.getValue();\n if ( vals.length > maxVisibleValues ) {\n e.stopPropagation();\n e.preventDefault();\n }\n }\n };\n return <components.Input {...props} onKeyDown={handleKeyDown} />;\n };\n\n // Footer for menu if provided\n const Footer = ( props: MenuProps<IOption, IsMulti, Group> ) => {\n const { children, innerProps, innerRef } = props;\n return (\n <components.Menu {...props} innerProps={innerProps} innerRef={innerRef}>\n {children}\n {menuFooter}\n </components.Menu>\n );\n };\n\n const customComponents = {\n IndicatorSeparator: () => null,\n DropdownIndicator,\n Option: OptionComponent,\n ValueContainer,\n MultiValue,\n Input,\n ...( menuFooter ? { Menu: Footer } : {})\n };\n\n const inputId = useRef( useId());\n // unify component type for creatable and default select\n type TRSComponent = ComponentType<ReactSelectProps<IOption, IsMulti, Group>>;\n const SelectType: TRSComponent = creatable\n ? ( CreatableSelect as TRSComponent )\n : ( ReactSelect as TRSComponent );\n\n const { base, label: labelClass, helperText: helperTextClass } = selectVariants({ error });\n\n return (\n <div className={base({ class: className })}>\n {label !== undefined && (\n <label\n htmlFor={id || inputId.current}\n className={labelClass()}\n >\n {label}\n </label>\n )}\n <SelectType\n classNamePrefix="select"\n inputId={id || inputId.current}\n options={\n restProps.isMulti && ( options as IOption[]).length\n ? ([{ value: "select-all", label: "Select All" }] as IOption[]).concat(\n options as IOption[]\n )\n : options\n }\n value={value}\n hideSelectedOptions={false}\n menuPlacement="auto"\n components={customComponents}\n className={cx( "select", `select-${size}` )}\n styles={\n reactSelectCustomStyles<IsMulti, Group>(\n error,\n size,\n restProps.isMulti as boolean\n )\n }\n isDisabled={disabled}\n closeMenuOnSelect={!restProps.isMulti}\n {...restProps}\n />\n {helperText && (\n <div className={helperTextClass()}>\n {helperText}\n </div>\n )}\n </div>\n );\n}\n\nSelect.displayName = "Select";\n// Exported types SelectProps and Option are declared above and can be re-exported via index.ts\n'
|
|
3074
2956
|
},
|
|
3075
2957
|
{
|
|
3076
2958
|
"name": "index.ts",
|
|
@@ -3087,8 +2969,9 @@ export const scrollbarThumbXVariants = cva(
|
|
|
3087
2969
|
"name": "tabs",
|
|
3088
2970
|
"description": "The Tabs component provides a tabbed interface for organizing content into separate views, showing one panel at a time. It's a controlled component that manages active tab state. **When to use:** - Organizing related content into separate views - Settings and configuration panels with multiple sections - Dashboard views with different data perspectives - Multi-step forms or workflows - Navigation between related pages or sections **Component Architecture:** - Controlled component (requires active tab state management) - Styled with Tailwind CSS and class-variance-authority (cva) - Keyboard navigation support (Arrow keys, Tab) - Supports both horizontal layouts - Accessible with proper ARIA attributes",
|
|
3089
2971
|
"dependencies": [
|
|
3090
|
-
"
|
|
3091
|
-
"react"
|
|
2972
|
+
"tailwind-variants",
|
|
2973
|
+
"react",
|
|
2974
|
+
"tailwind-merge"
|
|
3092
2975
|
],
|
|
3093
2976
|
"internalDependencies": [
|
|
3094
2977
|
"adpIcon"
|
|
@@ -3096,15 +2979,15 @@ export const scrollbarThumbXVariants = cva(
|
|
|
3096
2979
|
"files": [
|
|
3097
2980
|
{
|
|
3098
2981
|
"name": "tabsVariants.ts",
|
|
3099
|
-
"content": 'import {
|
|
2982
|
+
"content": 'import { tv } from "tailwind-variants";\n\nconst scrollbarStyles = [\n "scrollbar-width-thin",\n "[&::-webkit-scrollbar-thumb]:rounded-[5px]",\n "[&::-webkit-scrollbar]:w-[0.2rem]",\n "md:[&::-webkit-scrollbar]:w-[0rem]",\n "[&::-webkit-scrollbar]:h-[0rem]",\n "md:[&::-webkit-scrollbar]:h-[0.3em]",\n "[&::-webkit-scrollbar]:translate-y-2"\n];\n\nexport const tabsVariants = tv({\n slots: {\n root: "w-full",\n list: [\n "w-full list-none overflow-x-auto overflow-y-hidden whitespace-nowrap",\n "grid grid-flow-col justify-start z-10",\n "text-secondary-500",\n ...scrollbarStyles\n ],\n listItem: "flex items-center justify-center cursor-pointer rounded-none focus:outline-none disabled:cursor-not-allowed",\n content: "dark:text-white"\n },\n variants: {\n size: {\n xs: { listItem: "p-2 text-xs" },\n sm: { listItem: "px-1.5 py-1 text-sm" },\n md: { listItem: "px-2.5 py-2 text-md" },\n lg: { listItem: "p-3.5 text-lg" },\n xl: { listItem: "p-[18px] text-xl" }\n },\n variant: {\n text: {\n list: "mb-[-2px]",\n listItem: [\n "px-4",\n "border-0",\n "dark:text-white",\n "hover:text-primary-500",\n "dark:hover:text-primary-400",\n "dark:disabled:hover:text-secondary-300",\n "disabled:text-secondary-300",\n "dark:disabled:text-secondary-300",\n "disabled:hover:bg-transparent"\n ]\n },\n brick: {\n list: "mb-[-2px]",\n listItem: [\n "first:rounded-l last:rounded-r",\n "first:rtl:rounded-l-none first:rtl:rounded-r",\n "last:rtl:rounded-r-none last:rtl:rounded-l",\n "first:border-l-2",\n "first:rtl:border-l-0",\n "last:rtl:border-l-2",\n "px-4",\n "border-y-2",\n "border-r-2",\n "border-secondary-100",\n "hover:text-primary-500",\n "dark:hover:text-primary-400",\n "dark:disabled:hover:text-secondary-300",\n "dark:first:border-l-0 dark:last:border-r-0 dark:border-t-0 dark:border-b-0",\n "dark:last:rtl:border-r-2",\n "dark:bg-secondary-50",\n "disabled:text-secondary-300",\n "disabled:hover:bg-secondary-50"\n ]\n },\n filled: {\n listItem: "first:rounded-l last:rounded-r first:rtl:rounded-l-none first:rtl:rounded-r last:rtl:rounded-r-none last:rtl:rounded-l"\n }\n },\n color: {\n primary: {},\n secondary: {}\n },\n active: {\n true: {},\n false: {}\n }\n },\n compoundVariants: [\n // List variants\n {\n variant: "brick",\n color: "primary",\n class: { list: "border-gray-200" }\n },\n {\n variant: "filled",\n color: "primary",\n class: { list: "border-primary-500" }\n },\n {\n variant: "filled",\n color: "secondary",\n class: { list: "divide-gray-200 divide-x dark:divide-secondary-500" }\n },\n // Non-active filled variant styles\n {\n variant: "filled",\n color: "primary",\n active: false,\n class: {\n listItem: [\n "text-white",\n "bg-primary-500",\n "hover:bg-primary-700",\n "focus-visible:bg-primary-400",\n "active:bg-primary-500",\n "disabled:bg-primary-200",\n "disabled:text-white",\n "focus-visible:ring",\n "focus-visible:ring-[#99CBFA]",\n "focus-visible:active:bg-primary-500"\n ]\n }\n },\n {\n variant: "filled",\n color: "secondary",\n active: false,\n class: {\n listItem: [\n "bg-gray-100",\n "hover:bg-gray-300",\n "focus-visible:bg-gray-200",\n "active:bg-gray-100",\n "disabled:bg-gray-100",\n "disabled:text-secondary-300",\n "focus-visible:ring",\n "focus-visible:ring-gray-100",\n "focus-visible:active:bg-gray-100"\n ]\n }\n },\n // Active variant styles\n {\n variant: "text",\n active: true,\n class: {\n listItem: [\n "border-b-2 font-regular border-primary-500 dark:border-secondary-100",\n "text-primary-500 dark:hover:text-white dark:text-white bg-primary-50 dark:bg-primary-400"\n ]\n }\n },\n {\n variant: "brick",\n active: true,\n class: {\n listItem: [\n "text-primary-500 dark:text-white bg-primary-50",\n "dark:hover:text-white dark:text-white dark:bg-primary-400",\n "first:rtl:border-r-primary-50 first:rtl:dark:border-r-primary-400",\n "last:rtl:border-l-primary-50 last:rtl:dark:border-l-primary-400"\n ]\n }\n },\n {\n variant: "filled",\n color: "primary",\n active: true,\n class: { listItem: "border-primary-200 dark:text-white font-regular !bg-primary-700 text-white" }\n },\n {\n variant: "filled",\n color: "secondary",\n active: true,\n class: { listItem: "border-gray-200 font-regular !bg-primary-500 dark:!bg-primary-400 text-secondary-50 dark:text-white" }\n }\n ],\n defaultVariants: {\n size: "md",\n variant: "text",\n color: "secondary",\n active: false\n }\n});\n'
|
|
3100
2983
|
},
|
|
3101
2984
|
{
|
|
3102
2985
|
"name": "tabs.tsx",
|
|
3103
|
-
"content": 'import * as React from "react";\nimport {\n Children,\n createRef,\n useEffect,\n useRef,\n useState,\n isValidElement,\n type ReactElement,\n type RefObject\n} from "react";\nimport { cn } from "@utils";\nimport {\n tabsVariants,\n tabsListVariants,\n tabsListItemVariants,\n tabsContentVariants\n} from "./tabsVariants";\nimport { keyboardKeys } from "@constants";\n\nexport enum ETabSizes {\n XS = "xs",\n SM = "sm",\n MD = "md",\n LG = "lg"\n}\nexport type TTabSize = `${ETabSizes}`;\n\n/**\n * Props for the Tab component.\n */\nexport interface ITabProps {\n /**\n * Id of the tab.\n */\n id?: string;\n /**\n * Name of the tab.\n */\n title: React.ReactNode;\n /**\n * Function to call when the tab is displayed.\n */\n onEnter?: () => void;\n /**\n * Function to call when the tab is exited.\n */\n onExit?: () => void;\n /**\n * Dynamically updates the className applied to the component.\n */\n className?: string;\n /**\n * Content to be displayed within the tab.\n */\n children: React.ReactNode;\n /**\n * Indicates whether the tab is disabled.\n */\n disabled?: boolean;\n /**\n * Icon to be displayed before the title of the Tab.\n * @description\n * This prop accepts either an IconType (string) or a ReactNode.\n * - If an IconType is provided, it will be rendered as-is.\n * - If a ReactNode is provided, it will be rendered as-is.\n */\n prefixIcon?: React.ReactNode;\n /**\n * Icon to be displayed after the title of the Tab.\n * @description\n * This prop accepts either an IconType (string) or a ReactNode.\n * - If an IconType is provided, it will be rendered as-is.\n * - If a ReactNode is provided, it will be rendered as-is.\n */\n suffixIcon?: React.ReactNode;\n}\n\nexport const Tab = React.forwardRef<HTMLDivElement, ITabProps>(\n ({ children, className }, ref ) => (\n <div ref={ref} className={className}>\n {children}\n </div>\n )\n);\nTab.displayName = "Tab";\n\n/**\n * Props for the Tabs component.\n */\nexport interface ITabsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {\n /**\n * Active tab index. Tab index starts from 0.\n */\n activeIndex?: number;\n /**\n * Default active tab index, to be used with the uncontrolled version of the Tabs component.\n * @default 0\n */\n defaultActiveIndex?: number;\n /**\n * Dynamically updates the className applied to the component.\n */\n className?: string;\n /**\n * Renders all the tabs at once and switches between them using CSS.\n * @default false\n */\n switchUsingCss?: boolean;\n /**\n * Called on click of a tab header.\n * @param selectedTabIndex {number} Selected tab index.\n */\n onSelect?: ( selectedTabIndex: number ) => void;\n /**\n * Array of items to be displayed in the Tabs,\n * each item should be wrapped in Tabs.Tab component\n * @required\n * @example\n * ```tsx\n * <Tabs>\n * <Tabs.Tab title="Tab 1">Content of Tab 1</Tabs.Tab>\n * </Tabs>\n * ```\n */\n children:\n | ReactElement<ITabProps>\n | Array<ReactElement<ITabProps> | boolean>\n | boolean;\n /**\n * Size of the tab.\n * @default md\n */\n size?: TTabSize;\n /**\n * Variant of the tab.\n * @default text\n */\n variant?: "filled" | "text" | "brick";\n /**\n * Color of the tab. Only applicable for filled variant.\n */\n color?: "primary" | "secondary";\n}\n\nconst filterChildren = (\n children: ITabsProps["children"]\n): Array<ReactElement<ITabProps>> => {\n return Children.toArray( children ).filter(\n ( child ) =>\n isValidElement( child ) &&\n child.type === Tab &&\n child.props.title !== undefined\n ) as Array<ReactElement<ITabProps>>;\n};\n\nconst TabsComponent = React.forwardRef<HTMLDivElement, ITabsProps>(\n (\n {\n className,\n children,\n activeIndex,\n defaultActiveIndex = 0,\n switchUsingCss = false,\n onSelect,\n size = "md",\n variant = "text",\n color = "secondary",\n ...props\n },\n ref\n ) => {\n const [ activeIdx, setActiveIdx ] = useState<number>(() => {\n const childArray = Children.toArray( children );\n if (\n ( childArray[defaultActiveIndex] as ReactElement<ITabProps> )?.props\n .disabled\n ) {\n const nextIndex = childArray.findIndex(\n ( child, idx ) =>\n ( child as ReactElement<ITabProps> ).props.disabled === false &&\n idx > defaultActiveIndex\n );\n return nextIndex !== -1 ? nextIndex : 0;\n }\n return defaultActiveIndex;\n });\n const [ filteredChildren, setFilteredChildren ] = useState<\n Array<ReactElement<ITabProps>>\n >([]);\n const evalActiveIndex = activeIndex ?? activeIdx;\n const noOfChildren = filteredChildren.length;\n const activeTabProps = filteredChildren[evalActiveIndex]?.props;\n const tabRefs = useRef<Array<RefObject<HTMLButtonElement>>>([]);\n\n if ( tabRefs.current.length !== noOfChildren ) {\n tabRefs.current = filteredChildren.map(\n ( _, idx ) =>\n tabRefs.current[idx] || createRef<HTMLButtonElement>()\n );\n }\n\n const disabledTabIndexes = Children.toArray( children )\n .map(( child, idx ) =>\n ( child as ReactElement<ITabProps> ).props.disabled ? idx : null\n )\n .filter(( idx ) => idx !== null ) as number[];\n\n useEffect(() => {\n setFilteredChildren( filterChildren( children ));\n }, [children]);\n\n const handleSelect = ( tabProps: ITabProps, index: number ) => () => {\n if ( index === evalActiveIndex ) {\n return;\n }\n activeTabProps?.onExit?.();\n if ( activeIndex !== undefined ) {\n onSelect?.( index );\n } else {\n setActiveIdx( index );\n }\n tabProps.onEnter?.();\n };\n\n const handleKeyDown = ( event: React.KeyboardEvent<HTMLButtonElement> ) => {\n if ( noOfChildren <= 1 ) {\n return;\n }\n const { key } = event;\n let reqIndex = evalActiveIndex;\n switch ( key ) {\n case keyboardKeys.end:\n reqIndex = noOfChildren - 1;\n while ( disabledTabIndexes.includes( reqIndex ) && reqIndex > 0 ) {\n reqIndex--;\n }\n break;\n case keyboardKeys.home:\n reqIndex = 0;\n while (\n disabledTabIndexes.includes( reqIndex ) &&\n reqIndex < noOfChildren - 1\n ) {\n reqIndex++;\n }\n break;\n case keyboardKeys.arrowUp:\n case keyboardKeys.arrowLeft:\n event.preventDefault();\n do{\n reqIndex = ( reqIndex - 1 + noOfChildren ) % noOfChildren;\n } while ( disabledTabIndexes.includes( reqIndex ));\n break;\n case keyboardKeys.arrowDown:\n case keyboardKeys.arrowRight:\n event.preventDefault();\n do{\n reqIndex = ( reqIndex + 1 ) % noOfChildren;\n } while ( disabledTabIndexes.includes( reqIndex ));\n break;\n default:\n break;\n }\n activeTabProps?.onExit?.();\n if ( activeIndex !== undefined ) {\n onSelect?.( reqIndex );\n } else {\n setActiveIdx( reqIndex );\n }\n filteredChildren[reqIndex]?.props.onEnter?.();\n tabRefs.current[reqIndex]?.current?.focus();\n };\n\n if ( noOfChildren === 0 ) {\n return null;\n }\n\n return (\n <div ref={ref} className={cn( tabsVariants(), className )} {...props}>\n <div\n className={cn( variant === "text" && "border-b-2 border-gray-200", "mb-4" )}\n >\n <nav\n role="tablist"\n className={cn( tabsListVariants({ variant, color }))}\n >\n {filteredChildren.map(( tab, index ) => {\n const isActive = index === evalActiveIndex;\n const btnId = tab.props.id ? `${tab.props.id}-btn` : `tab-${index}-btn`;\n const ariaControls = tab.props.id || `tab-${index}`;\n return (\n <button\n key={index}\n type="button"\n role="tab"\n ref={tabRefs.current[index]}\n disabled={tab.props.disabled}\n id={btnId}\n aria-controls={ariaControls}\n aria-selected={isActive}\n onClick={handleSelect( tab.props, index )}\n onKeyDown={handleKeyDown}\n className={cn(\n tabsListItemVariants({ size, variant, color, active: isActive })\n )}\n >\n {tab.props.prefixIcon && (\n <span className="mr-2 rtl:mr-0 rtl:ml-2">{tab.props.prefixIcon}</span>\n )}\n {tab.props.title}\n {tab.props.suffixIcon && (\n <span className="ml-2 rtl:ml-0 rtl:mr-2">{tab.props.suffixIcon}</span>\n )}\n </button>\n );\n })}\n </nav>\n </div>\n {switchUsingCss\n ? filteredChildren.map(( tab, index ) => (\n <div\n key={index}\n id={tab.props.id || `tab-${index}`}\n role="tabpanel"\n aria-labelledby={\n tab.props.id ? `${tab.props.id}-btn` : `tab-${index}-btn`\n }\n tabIndex={0}\n className={cn(\n tabsContentVariants(),\n index === evalActiveIndex\n ? activeTabProps?.className\n : "hidden"\n )}\n >\n {tab.props.children}\n </div>\n ))\n : (\n <div\n id={\n activeTabProps?.id || `tab-${evalActiveIndex}`\n }\n role="tabpanel"\n aria-labelledby={\n activeTabProps?.id\n ? `${activeTabProps.id}-btn`\n : `tab-${evalActiveIndex}-btn`\n }\n tabIndex={0}\n className={cn(\n tabsContentVariants(),\n activeTabProps?.className\n )}\n >\n {activeTabProps?.children}\n </div>\n )}\n </div>\n );\n }\n);\n\nTabsComponent.displayName = "Tabs";\n\n// Create composite component to attach Tab as static member\ntype TTabsType = typeof TabsComponent & { Tab: typeof Tab };\nconst Tabs = TabsComponent as TTabsType;\nTabs.Tab = Tab;\nexport { Tabs };\n'
|
|
2986
|
+
"content": 'import * as React from "react";\nimport {\n Children,\n createRef,\n useEffect,\n useRef,\n useState,\n isValidElement,\n type ReactElement,\n type RefObject\n} from "react";\n\nimport { tabsVariants } from "./tabsVariants";\nimport { keyboardKeys } from "@constants";\n\nexport enum ETabSizes {\n XS = "xs",\n SM = "sm",\n MD = "md",\n LG = "lg"\n}\nexport type TTabSize = `${ETabSizes}`;\n\n/**\n * Props for the Tab component.\n */\nexport interface ITabProps {\n /**\n * Id of the tab.\n */\n id?: string;\n /**\n * Name of the tab.\n */\n title: React.ReactNode;\n /**\n * Function to call when the tab is displayed.\n */\n onEnter?: () => void;\n /**\n * Function to call when the tab is exited.\n */\n onExit?: () => void;\n /**\n * Dynamically updates the className applied to the component.\n */\n className?: string;\n /**\n * Content to be displayed within the tab.\n */\n children: React.ReactNode;\n /**\n * Indicates whether the tab is disabled.\n */\n disabled?: boolean;\n /**\n * Icon to be displayed before the title of the Tab.\n * @description\n * This prop accepts either an IconType (string) or a ReactNode.\n * - If an IconType is provided, it will be rendered as-is.\n * - If a ReactNode is provided, it will be rendered as-is.\n */\n prefixIcon?: React.ReactNode;\n /**\n * Icon to be displayed after the title of the Tab.\n * @description\n * This prop accepts either an IconType (string) or a ReactNode.\n * - If an IconType is provided, it will be rendered as-is.\n * - If a ReactNode is provided, it will be rendered as-is.\n */\n suffixIcon?: React.ReactNode;\n}\n\nexport const Tab = React.forwardRef<HTMLDivElement, ITabProps>(\n ({ children, className }, ref ) => (\n <div ref={ref} className={className}>\n {children}\n </div>\n )\n);\nTab.displayName = "Tab";\n\n/**\n * Props for the Tabs component.\n */\nexport interface ITabsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {\n /**\n * Active tab index. Tab index starts from 0.\n */\n activeIndex?: number;\n /**\n * Default active tab index, to be used with the uncontrolled version of the Tabs component.\n * @default 0\n */\n defaultActiveIndex?: number;\n /**\n * Dynamically updates the className applied to the component.\n */\n className?: string;\n /**\n * Renders all the tabs at once and switches between them using CSS.\n * @default false\n */\n switchUsingCss?: boolean;\n /**\n * Called on click of a tab header.\n * @param selectedTabIndex {number} Selected tab index.\n */\n onSelect?: ( selectedTabIndex: number ) => void;\n /**\n * Array of items to be displayed in the Tabs,\n * each item should be wrapped in Tabs.Tab component\n * @required\n * @example\n * ```tsx\n * <Tabs>\n * <Tabs.Tab title="Tab 1">Content of Tab 1</Tabs.Tab>\n * </Tabs>\n * ```\n */\n children:\n | ReactElement<ITabProps>\n | Array<ReactElement<ITabProps> | boolean>\n | boolean;\n /**\n * Size of the tab.\n * @default md\n */\n size?: TTabSize;\n /**\n * Variant of the tab.\n * @default text\n */\n variant?: "filled" | "text" | "brick";\n /**\n * Color of the tab. Only applicable for filled variant.\n */\n color?: "primary" | "secondary";\n}\n\nconst filterChildren = (\n children: ITabsProps["children"]\n): Array<ReactElement<ITabProps>> => {\n return Children.toArray( children ).filter(\n ( child ) =>\n isValidElement( child ) &&\n child.type === Tab &&\n child.props.title !== undefined\n ) as Array<ReactElement<ITabProps>>;\n};\n\nconst TabsComponent = React.forwardRef<HTMLDivElement, ITabsProps>(\n (\n {\n className,\n children,\n activeIndex,\n defaultActiveIndex = 0,\n switchUsingCss = false,\n onSelect,\n size = "md",\n variant = "text",\n color = "secondary",\n ...props\n },\n ref\n ) => {\n const [ activeIdx, setActiveIdx ] = useState<number>(() => {\n const childArray = Children.toArray( children );\n if (\n ( childArray[defaultActiveIndex] as ReactElement<ITabProps> )?.props\n .disabled\n ) {\n const nextIndex = childArray.findIndex(\n ( child, idx ) =>\n ( child as ReactElement<ITabProps> ).props.disabled === false &&\n idx > defaultActiveIndex\n );\n return nextIndex !== -1 ? nextIndex : 0;\n }\n return defaultActiveIndex;\n });\n const [ filteredChildren, setFilteredChildren ] = useState<\n Array<ReactElement<ITabProps>>\n >([]);\n const evalActiveIndex = activeIndex ?? activeIdx;\n const noOfChildren = filteredChildren.length;\n const activeTabProps = filteredChildren[evalActiveIndex]?.props;\n const tabRefs = useRef<Array<RefObject<HTMLButtonElement>>>([]);\n\n if ( tabRefs.current.length !== noOfChildren ) {\n tabRefs.current = filteredChildren.map(\n ( _, idx ) =>\n tabRefs.current[idx] || createRef<HTMLButtonElement>()\n );\n }\n\n const disabledTabIndexes = Children.toArray( children )\n .map(( child, idx ) =>\n ( child as ReactElement<ITabProps> ).props.disabled ? idx : null\n )\n .filter(( idx ) => idx !== null ) as number[];\n\n useEffect(() => {\n setFilteredChildren( filterChildren( children ));\n }, [children]);\n\n const handleSelect = ( tabProps: ITabProps, index: number ) => () => {\n if ( index === evalActiveIndex ) {\n return;\n }\n activeTabProps?.onExit?.();\n if ( activeIndex !== undefined ) {\n onSelect?.( index );\n } else {\n setActiveIdx( index );\n }\n tabProps.onEnter?.();\n };\n\n const handleKeyDown = ( event: React.KeyboardEvent<HTMLButtonElement> ) => {\n if ( noOfChildren <= 1 ) {\n return;\n }\n const { key } = event;\n let reqIndex = evalActiveIndex;\n switch ( key ) {\n case keyboardKeys.end:\n reqIndex = noOfChildren - 1;\n while ( disabledTabIndexes.includes( reqIndex ) && reqIndex > 0 ) {\n reqIndex--;\n }\n break;\n case keyboardKeys.home:\n reqIndex = 0;\n while (\n disabledTabIndexes.includes( reqIndex ) &&\n reqIndex < noOfChildren - 1\n ) {\n reqIndex++;\n }\n break;\n case keyboardKeys.arrowUp:\n case keyboardKeys.arrowLeft:\n event.preventDefault();\n do{\n reqIndex = ( reqIndex - 1 + noOfChildren ) % noOfChildren;\n } while ( disabledTabIndexes.includes( reqIndex ));\n break;\n case keyboardKeys.arrowDown:\n case keyboardKeys.arrowRight:\n event.preventDefault();\n do{\n reqIndex = ( reqIndex + 1 ) % noOfChildren;\n } while ( disabledTabIndexes.includes( reqIndex ));\n break;\n default:\n break;\n }\n activeTabProps?.onExit?.();\n if ( activeIndex !== undefined ) {\n onSelect?.( reqIndex );\n } else {\n setActiveIdx( reqIndex );\n }\n filteredChildren[reqIndex]?.props.onEnter?.();\n tabRefs.current[reqIndex]?.current?.focus();\n };\n\n if ( noOfChildren === 0 ) {\n return null;\n }\n\n const { root, list, content } = tabsVariants({ size, variant, color });\n\n return (\n <div ref={ref} className={root({ class: className })} {...props}>\n <div\n className={[ variant === "text" ? "border-b-2 border-gray-200" : "", "mb-4" ].filter( Boolean ).join( " " )}\n >\n <nav\n role="tablist"\n className={list()}\n >\n {filteredChildren.map(( tab, index ) => {\n const isActive = index === evalActiveIndex;\n const btnId = tab.props.id ? `${tab.props.id}-btn` : `tab-${index}-btn`;\n const ariaControls = tab.props.id || `tab-${index}`;\n const { listItem } = tabsVariants({ size, variant, color, active: isActive });\n return (\n <button\n key={index}\n type="button"\n role="tab"\n ref={tabRefs.current[index]}\n disabled={tab.props.disabled}\n id={btnId}\n aria-controls={ariaControls}\n aria-selected={isActive}\n onClick={handleSelect( tab.props, index )}\n onKeyDown={handleKeyDown}\n className={listItem()}\n >\n {tab.props.prefixIcon && (\n <span className="mr-2 rtl:mr-0 rtl:ml-2">{tab.props.prefixIcon}</span>\n )}\n {tab.props.title}\n {tab.props.suffixIcon && (\n <span className="ml-2 rtl:ml-0 rtl:mr-2">{tab.props.suffixIcon}</span>\n )}\n </button>\n );\n })}\n </nav>\n </div>\n {switchUsingCss\n ? filteredChildren.map(( tab, index ) => (\n <div\n key={index}\n id={tab.props.id || `tab-${index}`}\n role="tabpanel"\n aria-labelledby={\n tab.props.id ? `${tab.props.id}-btn` : `tab-${index}-btn`\n }\n tabIndex={0}\n className={content({ class: index === evalActiveIndex\n ? activeTabProps?.className\n : "hidden" })}\n >\n {tab.props.children}\n </div>\n ))\n : (\n <div\n id={\n activeTabProps?.id || `tab-${evalActiveIndex}`\n }\n role="tabpanel"\n aria-labelledby={\n activeTabProps?.id\n ? `${activeTabProps.id}-btn`\n : `tab-${evalActiveIndex}-btn`\n }\n tabIndex={0}\n className={content({ class: activeTabProps?.className })}\n >\n {activeTabProps?.children}\n </div>\n )}\n </div>\n );\n }\n);\n\nTabsComponent.displayName = "Tabs";\n\n// Create composite component to attach Tab as static member\ntype TTabsType = typeof TabsComponent & { Tab: typeof Tab };\nconst Tabs = TabsComponent as TTabsType;\nTabs.Tab = Tab;\nexport { Tabs };\n'
|
|
3104
2987
|
},
|
|
3105
2988
|
{
|
|
3106
2989
|
"name": "index.ts",
|
|
3107
|
-
"content": 'export { Tabs, type ITabsProps, Tab, type ITabProps } from "./tabs";\nexport { tabsVariants
|
|
2990
|
+
"content": 'export { Tabs, type ITabsProps, Tab, type ITabProps } from "./tabs";\nexport { tabsVariants } from "./tabsVariants";'
|
|
3108
2991
|
},
|
|
3109
2992
|
{
|
|
3110
2993
|
"name": "README.md",
|
|
@@ -3117,8 +3000,9 @@ export const scrollbarThumbXVariants = cva(
|
|
|
3117
3000
|
"name": "textCopy",
|
|
3118
3001
|
"description": "The TextCopy component displays text with a copy-to-clipboard button. **When to use:** - API keys and tokens - Code snippets - URLs and links - Reference numbers - Installation commands **Component Architecture:** - Styled with Tailwind CSS and cva - One-click copy functionality - Copy confirmation feedback - Truncation support",
|
|
3119
3002
|
"dependencies": [
|
|
3120
|
-
"
|
|
3121
|
-
"react"
|
|
3003
|
+
"tailwind-variants",
|
|
3004
|
+
"react",
|
|
3005
|
+
"tailwind-merge"
|
|
3122
3006
|
],
|
|
3123
3007
|
"internalDependencies": [
|
|
3124
3008
|
"adpIcon",
|
|
@@ -3127,25 +3011,11 @@ export const scrollbarThumbXVariants = cva(
|
|
|
3127
3011
|
"files": [
|
|
3128
3012
|
{
|
|
3129
3013
|
"name": "textCopyVariants.ts",
|
|
3130
|
-
"content":
|
|
3131
|
-
|
|
3132
|
-
export const textCopyVariants = cva(
|
|
3133
|
-
"inline-block dark:text-secondary-200",
|
|
3134
|
-
{
|
|
3135
|
-
variants: {
|
|
3136
|
-
displayOnHover: {
|
|
3137
|
-
true: "" // We'll handle this with conditional rendering in the component
|
|
3138
|
-
}
|
|
3139
|
-
},
|
|
3140
|
-
defaultVariants: {
|
|
3141
|
-
displayOnHover: false
|
|
3142
|
-
}
|
|
3143
|
-
}
|
|
3144
|
-
);`
|
|
3014
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const textCopyVariants = tv({\n base: "inline-block dark:text-secondary-200",\n variants: {\n displayOnHover: {\n true: ""\n }\n },\n defaultVariants: {\n displayOnHover: false\n }\n});\n'
|
|
3145
3015
|
},
|
|
3146
3016
|
{
|
|
3147
3017
|
"name": "textCopy.tsx",
|
|
3148
|
-
"content": 'import * as React from "react";\nimport { cn } from "
|
|
3018
|
+
"content": 'import * as React from "react";\nimport { cn } from "tailwind-variants";\nimport { textCopyVariants } from "./textCopyVariants";\n\nimport { ADPIcon, iconList } from "../adpIcon";\nimport { Button } from "../button";\n\n/**\n * TextCopy component for copying text to clipboard\n */\nexport interface ITextCopyProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Pass the text prop if you want to copy something other than children string\n */\n text?: string;\n /**\n * Pass the children to display the text or element to be copied.\n * When passing children as non-string, make sure to pass text prop as well\n */\n children?: React.ReactElement | string;\n /**\n * To display icon on hover of the text\n */\n displayOnHover?: boolean;\n /**\n * Title for the copy button\n */\n copyButtonTitle?: string;\n /**\n * Confirmation message to be shown after the text gets copied\n */\n confirmationMessage?: string;\n}\n\n/**\n * TextCopy component for copying text to clipboard with visual confirmation\n */\nexport const TextCopy = React.forwardRef<HTMLDivElement, ITextCopyProps>(\n ({\n className,\n text = "",\n children,\n displayOnHover = false,\n copyButtonTitle,\n confirmationMessage = "Copied",\n ...props\n }, ref ) => {\n const [ copied, setCopied ] = React.useState<boolean>( false );\n\n const copyText = async (): Promise<void> => {\n setCopied( true );\n // Copy text, if not exists copy children if children is string or empty string\n const textToCopy = text || ( typeof children === "string" ? children : "" );\n await navigator.clipboard.writeText( textToCopy );\n setTimeout(() => {\n setCopied( false );\n }, 1500 );\n };\n\n const textCopyClasses = textCopyVariants({ displayOnHover, class: [ displayOnHover && "group", className ] });\n\n return (\n <div\n ref={ref}\n data-testid="copyComponent"\n className={textCopyClasses}\n {...props}\n >\n {children}\n {copied ? (\n <div\n className="inline-flex align-bottom items-center gap-1 text-success-500 ms-2"\n data-testid="textCopied"\n aria-live="polite"\n >\n <ADPIcon size="xs" fixedWidth icon={iconList.checkCircle} />\n <span>{confirmationMessage}</span>\n </div>\n ) : (\n <Button\n variant="text"\n onClick={copyText}\n title={copyButtonTitle || "Copy Text"}\n className={cn(\n "cursor-pointer transition-opacity ease-in-out inline-flex align-text-top ms-2",\n displayOnHover && "opacity-0 group-hover:opacity-100 group-hover:delay-100 delay-300"\n )}\n data-testid="copyButton"\n >\n <ADPIcon size="xs" fixedWidth icon={iconList.clone} />\n </Button>\n )}\n </div>\n );\n }\n);\n\nTextCopy.displayName = "TextCopy";\n'
|
|
3149
3019
|
},
|
|
3150
3020
|
{
|
|
3151
3021
|
"name": "index.ts",
|
|
@@ -3162,8 +3032,9 @@ export const textCopyVariants = cva(
|
|
|
3162
3032
|
"name": "textField",
|
|
3163
3033
|
"description": "The TextField component provides a text input field with label, error handling, and various input types. **When to use:** - Form text inputs - Search fields - Email, password, number inputs - Any single-line text entry **Component Architecture:** - Styled with Tailwind CSS and cva - Support for prefix/suffix icons - Built-in error states - Multiple input types",
|
|
3164
3034
|
"dependencies": [
|
|
3165
|
-
"
|
|
3166
|
-
"react"
|
|
3035
|
+
"tailwind-variants",
|
|
3036
|
+
"react",
|
|
3037
|
+
"tailwind-merge"
|
|
3167
3038
|
],
|
|
3168
3039
|
"internalDependencies": [
|
|
3169
3040
|
"adpIcon"
|
|
@@ -3171,11 +3042,11 @@ export const textCopyVariants = cva(
|
|
|
3171
3042
|
"files": [
|
|
3172
3043
|
{
|
|
3173
3044
|
"name": "textFieldVariants.ts",
|
|
3174
|
-
"content": 'import {
|
|
3045
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const textFieldVariants = tv({\n slots: {\n container: "inline-flex flex-col gap-2 relative",\n label: "text-base font-medium text-secondary-400 dark:text-secondary-300",\n input: [\n "w-full rounded border border-gray-200 text-secondary-400",\n "placeholder:text-secondary-200 dark:placeholder:text-secondary-400",\n "outline-none focus:border-primary-500 focus:shadow-primary-2px",\n "disabled:bg-gray-50 disabled:text-secondary-200",\n "dark:border-secondary-500 dark:bg-iridium dark:disabled:bg-secondary-800 dark:disabled:text-secondary-500",\n "dark:focus:border-primary-400 dark:text-secondary-200"\n ],\n icon: "w-5 absolute top-1/2 -translate-y-1/2 text-gray-800 dark:text-gray-800",\n helperText: "text-xs font-normal"\n },\n variants: {\n size: {\n md: {\n input: "ps-3 pe-3 py-2.5 text-base"\n },\n lg: {\n input: "ps-4 pe-4 py-3 text-md"\n }\n },\n error: {\n true: {\n input: "border-error-500 focus:shadow-error-2px dark:border-error-400",\n helperText: "text-error-500 dark:text-error-400"\n },\n false: {\n helperText: "text-secondary-400 dark:text-secondary-200"\n }\n },\n withPrefix: {\n true: { input: "ps-8" },\n false: {}\n },\n withSuffix: {\n true: { input: "pe-8" },\n false: {}\n },\n position: {\n prefix: {},\n suffix: {}\n }\n },\n compoundVariants: [\n { size: "md", position: "prefix", class: { icon: "start-3" } },\n { size: "md", position: "suffix", class: { icon: "end-3" } },\n { size: "lg", position: "prefix", class: { icon: "start-4" } },\n { size: "lg", position: "suffix", class: { icon: "end-4" } }\n ],\n defaultVariants: {\n size: "md",\n error: false,\n withPrefix: false,\n withSuffix: false,\n position: "prefix"\n }\n});\n'
|
|
3175
3046
|
},
|
|
3176
3047
|
{
|
|
3177
3048
|
"name": "textField.tsx",
|
|
3178
|
-
"content": 'import * as React from "react";\nimport { type ReactNode, useId, useRef, forwardRef } from "react";\nimport { ADPIcon, type TIconType } from "../adpIcon";\nimport {
|
|
3049
|
+
"content": 'import * as React from "react";\nimport { type ReactNode, useId, useRef, forwardRef } from "react";\n\nimport { ADPIcon, type TIconType } from "../adpIcon";\nimport { textFieldVariants } from "./textFieldVariants";\n\ntype TSize = "md" | "lg";\n\nexport interface ITextFieldProps\n extends Omit<React.ComponentPropsWithoutRef<"input">, "css" | "size"> {\n /**\n * Regular static label.\n */\n label?: string | ReactNode;\n /**\n * Size of the Textfield.\n * @default md\n */\n size?: TSize;\n /**\n * Pass a ref to the inner input element\n */\n ref?: React.Ref<HTMLInputElement>;\n /**\n * Additional information or guidance displayed below the text field.\n */\n helperText?: ReactNode;\n /**\n * Pass a boolean value to indicate if the text field is in an error state.\n */\n error?: boolean;\n /**\n * Icon to be displayed before the input.\n * @description\n * This prop accepts either an IconType (string) or a ReactNode.\n * - If an IconType is provided, it will render an ADPIcon component.\n * - If a ReactNode is provided, it will be rendered as-is.\n */\n prefixIcon?: TIconType | ReactNode;\n /**\n * Icon to be displayed after the input.\n * @description\n * This prop accepts either an IconType (string) or a ReactNode.\n * - If an IconType is provided, it will render an ADPIcon component.\n * - If a ReactNode is provided, it will be rendered as-is.\n */\n suffixIcon?: TIconType | ReactNode;\n}\n\n/**\n * TextField component for collecting text input from users.\n */\nexport const TextField = forwardRef<HTMLInputElement, ITextFieldProps>(\n function TextField(\n {\n label,\n size = "md",\n helperText,\n error = false,\n prefixIcon,\n suffixIcon,\n className,\n ...props\n }: ITextFieldProps,\n ref\n ) {\n const inputId = useRef( useId());\n const { id, disabled } = props;\n\n const { container, label: labelClass, input, helperText: helperTextClass } = textFieldVariants({\n size,\n error,\n withPrefix: Boolean( prefixIcon ),\n withSuffix: Boolean( suffixIcon )\n });\n const { icon: prefixIconClass } = textFieldVariants({ size, position: "prefix" });\n const { icon: suffixIconClass } = textFieldVariants({ size, position: "suffix" });\n\n return (\n <div className={container({ class: className })}>\n {label !== undefined && (\n <label\n htmlFor={id || inputId.current}\n className={labelClass()}\n >\n {label}\n </label>\n )}\n <div className="relative">\n {prefixIcon && ( typeof prefixIcon === "string"\n ? (\n <ADPIcon\n icon={prefixIcon as TIconType}\n size={size}\n fixedWidth\n className={prefixIconClass()}\n />\n )\n : (\n <div className={prefixIconClass()}>\n {prefixIcon}\n </div>\n )\n )}\n <input\n id={id || inputId.current}\n className={input()}\n disabled={disabled}\n ref={ref}\n {...props}\n />\n {suffixIcon && ( typeof suffixIcon === "string"\n ? (\n <ADPIcon\n icon={suffixIcon as TIconType}\n size={size}\n fixedWidth\n className={suffixIconClass()}\n />\n )\n : (\n <div className={suffixIconClass()}>\n {suffixIcon}\n </div>\n )\n )}\n </div>\n {helperText && (\n <div className={helperTextClass()}>\n {helperText}\n </div>\n )}\n </div>\n );\n }\n);\n\nTextField.displayName = "TextField";\n'
|
|
3179
3050
|
},
|
|
3180
3051
|
{
|
|
3181
3052
|
"name": "index.ts",
|
|
@@ -3192,8 +3063,9 @@ export const textCopyVariants = cva(
|
|
|
3192
3063
|
"name": "textarea",
|
|
3193
3064
|
"description": "The Textarea component provides a multi-line text input field with label and error handling. **When to use:** - Long-form text entry - Comments and feedback - Descriptions and notes - Multi-line content **Component Architecture:** - Styled with Tailwind CSS and cva - Resizable or fixed height - Built-in error states - Character count support",
|
|
3194
3065
|
"dependencies": [
|
|
3195
|
-
"
|
|
3196
|
-
"react"
|
|
3066
|
+
"tailwind-variants",
|
|
3067
|
+
"react",
|
|
3068
|
+
"tailwind-merge"
|
|
3197
3069
|
],
|
|
3198
3070
|
"internalDependencies": [
|
|
3199
3071
|
"adpIcon"
|
|
@@ -3201,15 +3073,15 @@ export const textCopyVariants = cva(
|
|
|
3201
3073
|
"files": [
|
|
3202
3074
|
{
|
|
3203
3075
|
"name": "textareaVariants.ts",
|
|
3204
|
-
"content": 'import {
|
|
3076
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const textareaVariants = tv({\n slots: {\n container: "flex flex-col rounded border border-gray-200 bg-white w-full",\n textarea: "resize-y outline-none border-none text-secondary-500 rounded-t px-4 pt-3 w-full placeholder:text-secondary-200",\n helperText: "w-full text-xs font-normal"\n },\n variants: {\n state: {\n default: {\n container: "focus-within:outline-none focus-within:border-primary-500 focus-within:shadow-primary-2px",\n textarea: "focus:outline-none",\n helperText: "text-secondary-400"\n },\n error: {\n container: "!border-error-500 !focus-within:border-error-500 !focus-within:shadow-error-2px",\n textarea: "",\n helperText: "text-error-500"\n },\n disabled: {\n container: "bg-gray-50 focus-within:outline-none focus-within:border-gray-200 focus-within:shadow-none",\n textarea: "bg-gray-50 text-secondary-200",\n helperText: ""\n }\n }\n },\n defaultVariants: {\n state: "default"\n }\n});\n'
|
|
3205
3077
|
},
|
|
3206
3078
|
{
|
|
3207
3079
|
"name": "textarea.tsx",
|
|
3208
|
-
"content": 'import * as React from "react";\nimport { forwardRef, useRef, useId } from "react";\nimport { cn } from "
|
|
3080
|
+
"content": 'import * as React from "react";\nimport { forwardRef, useRef, useId } from "react";\nimport { cn } from "tailwind-variants";\nimport { textareaVariants } from "./textareaVariants";\n\n/**\n * Textarea component for multiline text input fields.\n */\nexport interface ITextareaProps extends Omit<React.ComponentPropsWithoutRef<"textarea">, "label"> {\n /**\n * Regular static label.\n */\n label?: React.ReactNode;\n /**\n * Pass a ref to the inner input element\n */\n ref?: React.Ref<HTMLTextAreaElement>;\n /**\n * Additional information or guidance displayed below the textarea.\n */\n helperText?: React.ReactNode;\n /**\n * Additional information shown inside the textarea.\n */\n infoText?: string;\n /**\n * Pass a boolean value to indicate if the textarea is in an error state.\n */\n error?: boolean;\n}\n\n/**\n * Textarea component for multiline text input fields.\n */\nexport const Textarea = forwardRef<HTMLTextAreaElement, ITextareaProps>((\n {\n label,\n helperText,\n error = false,\n infoText,\n className,\n ...restProps\n }: ITextareaProps,\n ref\n): React.ReactElement => {\n const { id, disabled, ...rest } = restProps;\n const inputId = useRef( useId());\n\n // Determine the state based on disabled and error props\n const state = disabled ? "disabled" : error ? "error" : "default";\n const { container, textarea: textareaClass, helperText: helperTextClass } = textareaVariants({ state });\n\n return (\n <div className={cn( "inline-flex flex-col gap-2", className )}>\n {\n label && (\n <label\n htmlFor={id || inputId.current}\n className="text-base text-secondary-400 font-medium"\n >\n {label}\n </label>\n )\n }\n <div className={container()}>\n <textarea\n className={textareaClass({ class: typeof infoText === "undefined" && "pb-3 rounded-b" })}\n rows={3}\n disabled={disabled}\n id={id || inputId.current}\n ref={ref}\n {...rest}\n />\n {\n typeof infoText !== "undefined" && (\n <div className="text-xxs text-secondary-400 self-end px-4 py-3">\n {infoText}\n </div>\n )\n }\n </div>\n {typeof helperText !== "undefined" && (\n <div className={helperTextClass()}>\n {helperText}\n </div>\n )}\n </div>\n );\n});\n\nTextarea.displayName = "Textarea";\n'
|
|
3209
3081
|
},
|
|
3210
3082
|
{
|
|
3211
3083
|
"name": "index.ts",
|
|
3212
|
-
"content": 'export { Textarea, type ITextareaProps } from "./textarea";\nexport { textareaVariants
|
|
3084
|
+
"content": 'export { Textarea, type ITextareaProps } from "./textarea";\nexport { textareaVariants } from "./textareaVariants";'
|
|
3213
3085
|
},
|
|
3214
3086
|
{
|
|
3215
3087
|
"name": "README.md",
|
|
@@ -3222,8 +3094,9 @@ export const textCopyVariants = cva(
|
|
|
3222
3094
|
"name": "toast",
|
|
3223
3095
|
"description": "The Toast component displays temporary notification messages that appear and auto-dismiss. **When to use:** - Success confirmations - Error notifications - System messages - Action feedback **Component Architecture:** - Uses toast library for management - Styled with Tailwind CSS and cva - Auto-dismiss with configurable duration - Multiple color variants - Positioned at various screen locations",
|
|
3224
3096
|
"dependencies": [
|
|
3225
|
-
"
|
|
3226
|
-
"react"
|
|
3097
|
+
"tailwind-variants",
|
|
3098
|
+
"react",
|
|
3099
|
+
"tailwind-merge"
|
|
3227
3100
|
],
|
|
3228
3101
|
"internalDependencies": [
|
|
3229
3102
|
"adpIcon",
|
|
@@ -3232,15 +3105,15 @@ export const textCopyVariants = cva(
|
|
|
3232
3105
|
"files": [
|
|
3233
3106
|
{
|
|
3234
3107
|
"name": "toastVariants.ts",
|
|
3235
|
-
"content": 'import {
|
|
3108
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const toastVariants = tv({\n slots: {\n root: "flex w-full md:w-[40vw] rounded-s z-[2060]",\n iconWrapper: "rounded-s flex justify-center items-center min-w-2 gap-1 text-white flex-shrink-0",\n contentWrapper: "flex gap-4 p-4 w-full items-center",\n content: "text-secondary-500 dark:text-secondary-50 line-clamp-5 flex-1",\n closeButton: "text-gray-800 ms-auto me-0 flex-shrink-0 flex-none"\n },\n variants: {\n variant: {\n info: {\n root: "bg-primary-50 dark:bg-secondary-600",\n iconWrapper: "bg-primary-500"\n },\n success: {\n root: "bg-success-50 dark:bg-secondary-600",\n iconWrapper: "bg-success-500"\n },\n warning: {\n root: "bg-warning-50 dark:bg-secondary-600",\n iconWrapper: "bg-warning-500"\n },\n error: {\n root: "bg-error-50 dark:bg-secondary-600",\n iconWrapper: "bg-error-500"\n }\n },\n condensed: {\n true: { iconWrapper: "px-3" },\n false: {}\n }\n },\n defaultVariants: {\n variant: "info",\n condensed: true\n }\n});\n'
|
|
3236
3109
|
},
|
|
3237
3110
|
{
|
|
3238
3111
|
"name": "toast.tsx",
|
|
3239
|
-
"content": 'import * as React from "react";\nimport {
|
|
3112
|
+
"content": 'import * as React from "react";\n\nimport { toastVariants } from "./toastVariants";\nimport { ADPIcon, iconList, type TIconType } from "../adpIcon";\nimport { Button } from "../button";\n\n/**\n * Toast component for displaying notifications or alerts.\n */\nexport interface IToastProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Accepts a variant value\n * @default info\n */\n variant?: "success" | "info" | "warning" | "error";\n /**\n * Show/hide the toast component\n * @default true\n */\n display?: boolean;\n /**\n * Accepts children of ReactNode type\n */\n children?: React.ReactNode;\n /**\n * Hide the Toast components automatically in x milliseconds\n */\n autoHideDelay?: number;\n /**\n * Hide the close button on the toast\n * @default false\n */\n hideCloseButton?: boolean;\n /**\n * A callback that is fired once the Toast closes\n */\n onClose?: () => void;\n /**\n * Show a condensed form of the Toast\n * @default true\n */\n condensed?: boolean;\n}\n\nconst variantMapping: Record<NonNullable<IToastProps["variant"]>, TIconType> = {\n info: iconList.infoFilled,\n success: iconList.checkCircleFilled,\n warning: iconList.alertFilled,\n error: iconList.timesCircleFilled\n};\n\n/**\n * Toast component for displaying notifications or alerts.\n */\nexport const Toast = React.forwardRef<HTMLDivElement, IToastProps>(\n ({\n variant = "info",\n display = true,\n children,\n autoHideDelay,\n hideCloseButton = false,\n onClose,\n condensed = true,\n className,\n ...props\n }, ref ) => {\n const [ show, setShow ] = React.useState( display );\n\n const hideToast = React.useCallback(() => {\n onClose?.();\n setShow( false );\n }, [onClose]);\n\n React.useEffect(() => {\n setShow( display );\n }, [display]);\n\n React.useEffect(() => {\n let autoHide: ReturnType<typeof setTimeout>;\n if ( typeof autoHideDelay === "number" ) {\n autoHide = setTimeout( hideToast, Math.round( autoHideDelay ));\n }\n return () => {\n if ( autoHide ) {\n clearTimeout( autoHide );\n }\n };\n }, [ autoHideDelay, hideToast ]);\n\n const { root, iconWrapper, contentWrapper, content, closeButton } = toastVariants({ variant, condensed });\n\n return show ? (\n <div\n ref={ref}\n className={root({ class: className })}\n role="alert"\n aria-live="assertive"\n aria-atomic="true"\n {...props}\n >\n <div className={iconWrapper()}>\n {condensed && <ADPIcon icon={variantMapping[variant]} size="md" />}\n </div>\n <div className={contentWrapper()}>\n {children && <div className={content()}>{children}</div>}\n {!hideCloseButton && (\n <Button\n prefixIcon="cross"\n onClick={hideToast}\n variant="text"\n color="secondary"\n className={closeButton()}\n size="xl"\n />\n )}\n </div>\n </div>\n ) : null;\n }\n);\n\nToast.displayName = "Toast";\n'
|
|
3240
3113
|
},
|
|
3241
3114
|
{
|
|
3242
3115
|
"name": "index.ts",
|
|
3243
|
-
"content": 'export { Toast } from "./toast";\n// eslint-disable-next-line no-duplicate-imports\nexport type { IToastProps } from "./toast";\nexport {
|
|
3116
|
+
"content": 'export { Toast } from "./toast";\n// eslint-disable-next-line no-duplicate-imports\nexport type { IToastProps } from "./toast";\nexport { toastVariants } from "./toastVariants";'
|
|
3244
3117
|
},
|
|
3245
3118
|
{
|
|
3246
3119
|
"name": "README.md",
|
|
@@ -3253,8 +3126,9 @@ export const textCopyVariants = cva(
|
|
|
3253
3126
|
"name": "toggle",
|
|
3254
3127
|
"description": "The Toggle component provides an on/off switch for binary settings. **When to use:** - Feature toggles - Settings enable/disable - Preference switches - Mode selection (light/dark) **Component Architecture:** - Styled with Tailwind CSS and cva - Accessible switch implementation - Smooth transition animations",
|
|
3255
3128
|
"dependencies": [
|
|
3256
|
-
"
|
|
3257
|
-
"react"
|
|
3129
|
+
"tailwind-variants",
|
|
3130
|
+
"react",
|
|
3131
|
+
"tailwind-merge"
|
|
3258
3132
|
],
|
|
3259
3133
|
"internalDependencies": [
|
|
3260
3134
|
"adpIcon"
|
|
@@ -3262,15 +3136,15 @@ export const textCopyVariants = cva(
|
|
|
3262
3136
|
"files": [
|
|
3263
3137
|
{
|
|
3264
3138
|
"name": "toggleVariants.ts",
|
|
3265
|
-
"content": 'import {
|
|
3139
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const toggleVariants = tv({\n slots: {\n wrapper: "inline-flex items-center gap-3 text-secondary-500",\n container: "relative bg-gray-200 rounded-full duration-300 ease-in-out dark:bg-secondary-600 cursor-pointer",\n button: "absolute top-1/2 start-0.5 -translate-y-1/2 bg-white rounded-full duration-300 ease-in-out",\n icon: "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-secondary-800",\n label: "text-inherit dark:text-secondary-100"\n },\n variants: {\n size: {\n sm: {\n container: "w-[36px] h-5",\n button: "w-4 h-4",\n label: "text-base"\n },\n md: {\n container: "w-[44px] h-6",\n button: "w-5 h-5",\n label: "text-md"\n }\n },\n toggled: {\n true: {\n container: "bg-success-500 dark:bg-success-400",\n button: "translate-x-full -translate-y-1/2 rtl:-translate-x-full"\n },\n false: {}\n },\n disabled: {\n true: {\n container: "bg-gray-200 dark:bg-secondary-600 cursor-not-allowed",\n button: "bg-secondary-50 dark:bg-gray-900",\n label: "text-secondary-300 dark:text-secondary-500 cursor-not-allowed"\n },\n false: {}\n }\n },\n compoundVariants: [\n {\n toggled: true,\n disabled: true,\n class: {\n container: "bg-success-500/50 dark:bg-success-400/50"\n }\n },\n {\n toggled: false,\n disabled: false,\n class: {\n container: "focus-within:shadow-success-2px focus-within:ring-1 focus-within:ring-success-100 focus-within:transition-all focus-within:duration-300"\n }\n },\n {\n toggled: true,\n disabled: false,\n class: {\n container: "focus-within:shadow-success-2px"\n }\n }\n ],\n defaultVariants: {\n size: "md",\n toggled: false,\n disabled: false\n }\n});\n'
|
|
3266
3140
|
},
|
|
3267
3141
|
{
|
|
3268
3142
|
"name": "toggle.tsx",
|
|
3269
|
-
"content": 'import * as React from "react";\nimport { forwardRef, type RefObject, useRef, useId } from "react";\nimport {
|
|
3143
|
+
"content": 'import * as React from "react";\nimport { forwardRef, type RefObject, useRef, useId } from "react";\nimport { ADPIcon, type TIconType } from "../adpIcon";\nimport { toggleVariants } from "./toggleVariants";\n\n/**\n * Toggle component props\n */\nexport interface IToggleProps\n extends Omit<React.HTMLProps<HTMLInputElement>, "label" | "size"> {\n /**\n * id for the Toggle\n */\n id?: string;\n /**\n * Overwrite the styles by passing space separated class names\n */\n className?: string;\n /**\n * Size of the Toggle.\n * @default md\n */\n size?: "sm" | "md";\n /**\n * Regular static label.\n */\n label?: React.ReactNode;\n /**\n * Passing boolean value to check or uncheck Toggle.\n * @default false\n */\n toggled?: boolean;\n /**\n * Provide an optional icon for the toggled state of the component.\n */\n toggledIcon?: TIconType;\n /**\n * Provide an optional icon for the un-toggled state of the component.\n */\n untoggledIcon?: TIconType;\n /**\n * Pass a ref to the inner input element\n */\n ref?: RefObject<HTMLInputElement>;\n}\n\n/**\n * Toggle component for toggling boolean states\n */\nexport const Toggle = forwardRef<HTMLInputElement, IToggleProps>(\n (\n {\n id,\n className,\n size = "md",\n label,\n toggled,\n toggledIcon,\n untoggledIcon,\n disabled,\n ...restProps\n }: IToggleProps,\n ref\n ): React.ReactElement => {\n const inputId = useRef( useId());\n\n const icon = toggled ? toggledIcon : untoggledIcon;\n\n const { wrapper, container, button, icon: iconClass, label: labelClass } = toggleVariants({\n size,\n disabled: Boolean( disabled ),\n toggled: Boolean( toggled )\n });\n\n return (\n <label className={wrapper({ class: className })}>\n <input\n {...restProps}\n type="checkbox"\n id={id || inputId.current}\n className="absolute invisible w-0 h-0 outline-none"\n checked={toggled}\n disabled={disabled}\n ref={ref}\n />\n <div className={container()}>\n <span className={button()}>\n {icon && (\n <ADPIcon\n className={iconClass()}\n icon={icon}\n height={10}\n width={10}\n />\n )}\n </span>\n </div>\n {typeof label !== "undefined" && (\n <span className={labelClass()}>\n {label}\n </span>\n )}\n </label>\n );\n }\n);\n\nToggle.displayName = "Toggle";\n'
|
|
3270
3144
|
},
|
|
3271
3145
|
{
|
|
3272
3146
|
"name": "index.ts",
|
|
3273
|
-
"content": 'export { Toggle, type IToggleProps } from "./toggle";\nexport {
|
|
3147
|
+
"content": 'export { Toggle, type IToggleProps } from "./toggle";\nexport { toggleVariants } from "./toggleVariants";'
|
|
3274
3148
|
},
|
|
3275
3149
|
{
|
|
3276
3150
|
"name": "README.md",
|
|
@@ -3283,9 +3157,10 @@ export const textCopyVariants = cva(
|
|
|
3283
3157
|
"name": "tooltip",
|
|
3284
3158
|
"description": "The Tooltip component displays informative text when hovering over an element. **When to use:** - Additional context for UI elements - Icon explanations - Truncated text full content - Help text - Keyboard shortcuts **Component Architecture:** - Built with @floating-ui for positioning - Styled with Tailwind CSS and cva - Multiple placement options - Keyboard accessible",
|
|
3285
3159
|
"dependencies": [
|
|
3286
|
-
"
|
|
3160
|
+
"tailwind-variants",
|
|
3287
3161
|
"react",
|
|
3288
|
-
"@floating-ui/react"
|
|
3162
|
+
"@floating-ui/react",
|
|
3163
|
+
"tailwind-merge"
|
|
3289
3164
|
],
|
|
3290
3165
|
"internalDependencies": [
|
|
3291
3166
|
"adpIcon"
|
|
@@ -3293,11 +3168,11 @@ export const textCopyVariants = cva(
|
|
|
3293
3168
|
"files": [
|
|
3294
3169
|
{
|
|
3295
3170
|
"name": "tooltipVariants.ts",
|
|
3296
|
-
"content": 'import {
|
|
3171
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const tooltipVariants = tv({\n slots: {\n container: "border border-gray-100 bg-white rounded shadow-xl max-w-[308px] z-[2040] dark:bg-secondary-800 dark:border-secondary-500 w-auto",\n content: "flex flex-col gap-1 relative z-10 break-words dark:text-secondary-50 text-secondary-300",\n title: "font-medium text-black dark:text-white",\n arrow: "absolute aspect-square w-4 h-4 rounded-tl-sm bg-inherit border-1.5 border-r-0 border-b-0 border-gray-100 dark:border-secondary-500",\n trigger: "relative text-secondary-400 dark:text-secondary-50 max-w-max"\n },\n variants: {\n size: {\n sm: {\n container: "p-2",\n content: "text-xs",\n title: "text-xs"\n },\n md: {\n container: "p-3",\n content: "text-sm",\n title: "text-base"\n }\n },\n disabled: {\n true: {\n trigger: "!text-secondary-200 cursor-not-allowed"\n }\n }\n },\n defaultVariants: {\n size: "md"\n }\n});\n'
|
|
3297
3172
|
},
|
|
3298
3173
|
{
|
|
3299
3174
|
"name": "tooltip.tsx",
|
|
3300
|
-
"content": 'import * as React from "react";\nimport {\n FloatingPortal,\n type Placement,\n type Side,\n arrow as arrowMiddleware,\n offset,\n flip,\n shift,\n useClick,\n useDismiss,\n useFloating,\n useFocus,\n useHover,\n useInteractions,\n useRole,\n autoUpdate\n} from "@floating-ui/react";\nimport {
|
|
3175
|
+
"content": 'import * as React from "react";\nimport {\n FloatingPortal,\n type Placement,\n type Side,\n arrow as arrowMiddleware,\n offset,\n flip,\n shift,\n useClick,\n useDismiss,\n useFloating,\n useFocus,\n useHover,\n useInteractions,\n useRole,\n autoUpdate\n} from "@floating-ui/react";\n\nimport { tooltipVariants } from "./tooltipVariants";\n\ntype TTooltipSizes = "sm" | "md";\n\nexport interface ITooltipProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Title of the tooltip */\n title?: string;\n /** Content of the tooltip */\n children?: React.ReactNode;\n /** The reference element that triggers the tooltip */\n trigger: React.ReactNode;\n /** Size of the tooltip content container @default "md" */\n size?: TTooltipSizes;\n /** Placement of the tooltip @default "bottom-end" */\n placement?: Placement;\n /** Renders the tooltip on click @default false */\n clickable?: boolean;\n /** Override tooltip container class names */\n className?: string;\n /** Override trigger element class names */\n triggerElementClassName?: string;\n /** Callback executed on trigger click */\n onTriggerClick?: () => void;\n /** Render tooltip outside portal @default false */\n renderOutsidePortal?: boolean;\n /** aria-label for the trigger element */\n triggerAriaLabel?: string;\n /** Disable tooltip interactions @default false */\n disabled?: boolean;\n}\n\nexport const Tooltip = React.forwardRef<HTMLDivElement, ITooltipProps>(\n (\n {\n title,\n children,\n trigger,\n size = "md",\n placement = "bottom-end",\n clickable = false,\n className,\n triggerElementClassName,\n onTriggerClick,\n renderOutsidePortal = false,\n triggerAriaLabel,\n disabled = false,\n ...props\n },\n ref\n ) => {\n const [ open, setOpen ] = React.useState( false );\n const arrowRef = React.useRef<HTMLDivElement | null>( null );\n\n const {\n x,\n y,\n refs,\n strategy,\n context,\n middlewareData: { arrow: { x: arrowX, y: arrowY } = {} },\n placement: hookPlacement\n } = useFloating({\n open,\n onOpenChange: setOpen,\n placement,\n whileElementsMounted: autoUpdate,\n middleware: [ offset( 10 ), flip(), shift(), arrowMiddleware({ element: arrowRef, padding: 14 }) ]\n });\n\n const hover = useHover( context, { move: false, enabled: !clickable, delay: 200 });\n const focus = useFocus( context );\n const click = useClick( context, { enabled: clickable && !disabled });\n const dismiss = useDismiss( context );\n const role = useRole( context, { role: "tooltip" });\n\n const { getReferenceProps, getFloatingProps } = useInteractions([\n hover,\n focus,\n click,\n dismiss,\n role\n ]);\n\n const invalidTrigger = isNodeInvalid( trigger );\n const invalidChild = isNodeInvalid( children );\n\n const referenceProps = getReferenceProps({\n onClick: () => {\n if ( !disabled ) {\n onTriggerClick?.();\n }\n }\n });\n\n const { container, content, title: titleClass, arrow, trigger: triggerClass } =\n tooltipVariants({ size, disabled: Boolean( disabled ) });\n\n const popupContent = (\n <div\n role="tooltip"\n ref={( node ) => {\n refs.setFloating( node );\n if ( ref && typeof ref === "function" ) {\n ref( node );\n } else if ( ref && "current" in ref ) {\n ( ref as React.MutableRefObject<HTMLDivElement | null> ).current = node;\n }\n }}\n style={{ position: strategy, top: y ?? 0, left: x ?? 0 }}\n className={container({ class: [ open ? "visible" : "invisible", className ] })}\n {...getFloatingProps()}\n {...props}\n >\n <div\n data-testid="tooltip-content"\n className={content()}\n >\n {title && (\n <h1 className={titleClass()}>\n {title}\n </h1>\n )}\n {!invalidChild && children}\n </div>\n <div\n ref={arrowRef}\n style={{ left: arrowX, top: arrowY, [getCSSPropertyForPlacement( hookPlacement )]: "-9px" }}\n className={arrow({ class: getArrowPlacementClass( hookPlacement ) })}\n />\n </div>\n );\n\n if ( invalidTrigger ) {\n return null;\n }\n\n return (\n <>\n <span\n data-testid="tooltip-trigger"\n role="button"\n ref={refs.setReference}\n tabIndex={0}\n aria-label={triggerAriaLabel}\n className={triggerClass({ class: triggerElementClassName })}\n {...referenceProps}\n >\n {trigger}\n </span>\n {open && ( renderOutsidePortal ? popupContent : <FloatingPortal>{popupContent}</FloatingPortal> )}\n </>\n );\n }\n);\n\nTooltip.displayName = "Tooltip";\n\n/**\n * Determines the CSS property for positioning the arrow based on placement\n */\nconst getCSSPropertyForPlacement = ( placementSide: Placement ): Side => {\n const [staticPosition] = placementSide.split( "-" );\n const mapping: Record<string, Side> = {\n top: "bottom",\n right: "left",\n bottom: "top",\n left: "right"\n };\n return mapping[staticPosition];\n};\n\n/**\n * Checks if the node is an invalid ReactNode (empty object)\n */\nconst isNodeInvalid = ( node: any ): boolean =>\n typeof node === "object" && !Array.isArray( node ) && Object.keys( node ).length === 0;\n\n/**\n * Returns the rotation class for the arrow based on placement\n */\nfunction getArrowPlacementClass( placementSide: Placement ): string {\n const [staticPosition] = placementSide.split( "-" );\n switch ( staticPosition ) {\n case "top":\n return "-rotate-[135deg]";\n case "right":\n return "-rotate-45";\n case "left":\n return "rotate-[135deg]";\n case "bottom":\n default:\n return "rotate-45";\n }\n}\n'
|
|
3301
3176
|
},
|
|
3302
3177
|
{
|
|
3303
3178
|
"name": "index.ts",
|
|
@@ -3314,8 +3189,9 @@ export const textCopyVariants = cva(
|
|
|
3314
3189
|
"name": "truncate",
|
|
3315
3190
|
"description": "The Truncate component limits text length with ellipsis and optional expand functionality. **When to use:** - Long descriptions - User-generated content - List items with variable text - Table cells with overflow - Preview text **Component Architecture:** - Styled with Tailwind CSS and cva - Multiple truncation modes - Expand/collapse support - Line clamping",
|
|
3316
3191
|
"dependencies": [
|
|
3317
|
-
"
|
|
3318
|
-
"react"
|
|
3192
|
+
"tailwind-variants",
|
|
3193
|
+
"react",
|
|
3194
|
+
"tailwind-merge"
|
|
3319
3195
|
],
|
|
3320
3196
|
"internalDependencies": [
|
|
3321
3197
|
"button",
|
|
@@ -3324,11 +3200,11 @@ export const textCopyVariants = cva(
|
|
|
3324
3200
|
"files": [
|
|
3325
3201
|
{
|
|
3326
3202
|
"name": "truncateVariants.ts",
|
|
3327
|
-
"content": 'import {
|
|
3203
|
+
"content": 'import { tv } from "tailwind-variants";\n\nexport const truncateVariants = tv({\n base: "relative",\n variants: {\n truncated: {\n true: "text-secondary-500 dark:text-secondary-50",\n false: ""\n }\n },\n defaultVariants: {\n truncated: false\n }\n});\n'
|
|
3328
3204
|
},
|
|
3329
3205
|
{
|
|
3330
3206
|
"name": "truncate.tsx",
|
|
3331
|
-
"content": 'import * as React from "react";\nimport {
|
|
3207
|
+
"content": 'import * as React from "react";\nimport { truncateVariants } from "./truncateVariants";\nimport { Button } from "../button";\nimport { Modal } from "../modal";\nimport { ModalBody } from "../modal/modal";\n\n// Import the type for void elements\ntype TVoidElements =\n | "area"\n | "base"\n | "br"\n | "col"\n | "embed"\n | "hr"\n | "img"\n | "input"\n | "link"\n | "meta"\n | "param"\n | "source"\n | "track"\n | "wbr";\n\n/**\n * Truncate component for displaying limited text with a "Show more" button\n */\nexport interface ITruncateProps extends React.HTMLAttributes<HTMLElement> {\n /**\n * The type of tag to render for the scroll container, the default value is "div".\n * @default div\n */\n as?: keyof Omit<JSX.IntrinsicElements, TVoidElements>;\n /**\n * The text content to be truncated\n */\n children?: string;\n /**\n * Maximum number of characters to display before truncating.\n * If the text exceeds this limit, it will be truncated with an ellipsis,\n * and a "Show more" button will be displayed.\n * The full text can be viewed in a modal by clicking the "Show more" button.\n * @default 100\n */\n limit?: number;\n /**\n * Overwrite the styles for the Truncate component by passing space separated class names\n */\n className?: string;\n}\n\n/**\n * Truncate component for displaying limited text with a "Show more" button\n */\nexport const Truncate = React.forwardRef<HTMLElement, ITruncateProps>(\n ({ as = "div", children, limit = 100, className, ...props }, ref ) => {\n const [ showModal, setShowModal ] = React.useState<boolean>( false );\n\n // Convert the HTML tag string to an ElementType\n const Tag = as as React.ElementType;\n\n // Check if truncation is needed\n const isTruncated = typeof children === "string" && limit > 0 && children.length > limit;\n\n // If children is not a string or limit is invalid, return null\n if ( typeof children !== "string" || limit <= 0 ) {\n return null;\n }\n\n const truncateClasses = truncateVariants({ truncated: isTruncated, class: className });\n\n return isTruncated ? (\n <>\n <Tag\n ref={ref as any}\n className={truncateClasses}\n {...props}\n >\n {children.substring( 0, limit )}...{" "}\n <Button\n variant="text"\n onClick={() => setShowModal( true )}\n className="inline-block text-inherit"\n style={{ fontSize: "inherit" }}\n >\n Show more\n </Button>\n </Tag>\n {showModal && (\n <Modal showModal={showModal} onHide={() => setShowModal( false )}>\n <ModalBody className="max-w-3xl">{children}</ModalBody>\n </Modal>\n )}\n </>\n ) : (\n <Tag\n ref={ref as any}\n className={truncateClasses}\n {...props}\n >\n {children}\n </Tag>\n );\n }\n);\n\nTruncate.displayName = "Truncate";\n'
|
|
3332
3208
|
},
|
|
3333
3209
|
{
|
|
3334
3210
|
"name": "index.ts",
|
|
@@ -3360,14 +3236,13 @@ var library_deps_default = {
|
|
|
3360
3236
|
"@floating-ui/react": "^0.26.28",
|
|
3361
3237
|
"@tanstack/react-table": "^8.20.5",
|
|
3362
3238
|
"@tanstack/react-virtual": "^3.10.9",
|
|
3363
|
-
"class-variance-authority": "^0.7.1",
|
|
3364
|
-
clsx: "^1.2.1",
|
|
3365
3239
|
global: "^4.4.0",
|
|
3366
3240
|
"react-datepicker": "^7.5.0",
|
|
3367
3241
|
"react-focus-on": "^3.9.4",
|
|
3368
3242
|
"react-merge-refs": "^1.1.0",
|
|
3369
3243
|
"react-select": "^5.8.0",
|
|
3370
3244
|
"tailwind-merge": "^3.2.0",
|
|
3245
|
+
"tailwind-variants": "^3.2.2",
|
|
3371
3246
|
react: "^18.3.1",
|
|
3372
3247
|
"react-dom": "^18.3.1",
|
|
3373
3248
|
tailwindcss: "^3.4.17",
|