forked from cerc-io/snowballtools-base
merge
This commit is contained in:
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
// eslint extension options
|
||||
"eslint.enable": true,
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"css.customData": [".vscode/tailwind.json"],
|
||||
// prettier extension setting
|
||||
"editor.formatOnSave": true,
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.rulers": [80],
|
||||
"editor.codeActionsOnSave": [
|
||||
"source.addMissingImports",
|
||||
"source.fixAll",
|
||||
"source.organizeImports"
|
||||
],
|
||||
// Show in vscode "Problems" tab when there are errors
|
||||
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
|
||||
// Use absolute import for typescript files
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
// IntelliSense for taiwind variants
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
]
|
||||
}
|
||||
@@ -3,8 +3,17 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.0.16",
|
||||
"@material-tailwind/react": "^2.1.7",
|
||||
"@tanstack/react-query": "^5.22.2",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
@@ -16,13 +25,15 @@
|
||||
"@web3modal/wagmi": "^4.0.5",
|
||||
"assert": "^2.1.0",
|
||||
"axios": "^1.6.7",
|
||||
"date-fns": "^3.0.1",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"downshift": "^8.2.3",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"gql-client": "^1.0.0",
|
||||
"luxon": "^3.4.4",
|
||||
"octokit": "^3.1.2",
|
||||
"react": "^18.2.0",
|
||||
"react-calendar": "^4.8.0",
|
||||
"react-code-blocks": "^0.1.6",
|
||||
"react-day-picker": "^8.9.1",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -34,6 +45,7 @@
|
||||
"react-scripts": "5.0.1",
|
||||
"react-timer-hook": "^3.0.7",
|
||||
"siwe": "^2.1.4",
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"typescript": "^4.9.5",
|
||||
"usehooks-ts": "^2.10.0",
|
||||
"vertical-stepper-nav": "^1.0.2",
|
||||
@@ -78,6 +90,6 @@
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"prettier": "^3.1.0",
|
||||
"tailwindcss": "^3.3.6"
|
||||
"tailwindcss": "^3.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,9 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Collapse, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { Environment, EnvironmentVariable } from 'gql-client/dist/src/types';
|
||||
|
||||
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
|
||||
import { Environment, EnvironmentVariable } from 'gql-client';
|
||||
|
||||
interface DisplayEnvironmentVariablesProps {
|
||||
environment: Environment;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const avatarTheme = tv(
|
||||
{
|
||||
base: ['relative', 'block', 'rounded-full', 'overflow-hidden'],
|
||||
slots: {
|
||||
image: [
|
||||
'h-full',
|
||||
'w-full',
|
||||
'rounded-[inherit]',
|
||||
'object-cover',
|
||||
'object-center',
|
||||
],
|
||||
fallback: [
|
||||
'grid',
|
||||
'select-none',
|
||||
'place-content-center',
|
||||
'h-full',
|
||||
'w-full',
|
||||
'rounded-[inherit]',
|
||||
'font-medium',
|
||||
],
|
||||
},
|
||||
variants: {
|
||||
type: {
|
||||
gray: {
|
||||
fallback: ['text-elements-highEm', 'bg-base-bg-emphasized'],
|
||||
},
|
||||
orange: {
|
||||
fallback: ['text-elements-warning', 'bg-base-bg-emphasized-warning'],
|
||||
},
|
||||
blue: {
|
||||
fallback: ['text-elements-info', 'bg-base-bg-emphasized-info'],
|
||||
},
|
||||
},
|
||||
size: {
|
||||
18: {
|
||||
base: ['rounded-md', 'h-[18px]', 'w-[18px]', 'text-[0.625rem]'],
|
||||
},
|
||||
20: {
|
||||
base: ['rounded-md', 'h-5', 'w-5', 'text-[0.625rem]'],
|
||||
},
|
||||
24: {
|
||||
base: ['rounded-md', 'h-6', 'w-6', 'text-[0.625rem]'],
|
||||
},
|
||||
28: {
|
||||
base: ['rounded-lg', 'h-[28px]', 'w-[28px]', 'text-[0.625rem]'],
|
||||
},
|
||||
32: {
|
||||
base: ['rounded-lg', 'h-8', 'w-8', 'text-xs'],
|
||||
},
|
||||
36: {
|
||||
base: ['rounded-xl', 'h-[36px]', 'w-[36px]', 'text-xs'],
|
||||
},
|
||||
40: {
|
||||
base: ['rounded-xl', 'h-10', 'w-10', 'text-sm'],
|
||||
},
|
||||
44: {
|
||||
base: ['rounded-xl', 'h-[44px]', 'w-[44px]', 'text-sm'],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 24,
|
||||
type: 'gray',
|
||||
},
|
||||
},
|
||||
{ responsiveVariants: true },
|
||||
);
|
||||
|
||||
export type AvatarVariants = VariantProps<typeof avatarTheme>;
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { type ComponentPropsWithoutRef, type ComponentProps } from 'react';
|
||||
import { avatarTheme, type AvatarVariants } from './Avatar.theme';
|
||||
import * as PrimitiveAvatar from '@radix-ui/react-avatar';
|
||||
|
||||
export type AvatarProps = ComponentPropsWithoutRef<'div'> & {
|
||||
imageSrc?: string | null;
|
||||
initials?: string;
|
||||
imageProps?: ComponentProps<typeof PrimitiveAvatar.Image>;
|
||||
fallbackProps?: ComponentProps<typeof PrimitiveAvatar.Fallback>;
|
||||
} & AvatarVariants;
|
||||
|
||||
export const Avatar = ({
|
||||
className,
|
||||
size,
|
||||
type,
|
||||
imageSrc,
|
||||
imageProps,
|
||||
fallbackProps,
|
||||
initials,
|
||||
}: AvatarProps) => {
|
||||
const { base, image, fallback } = avatarTheme({ size, type });
|
||||
|
||||
return (
|
||||
<PrimitiveAvatar.Root className={base({ className })}>
|
||||
{imageSrc && (
|
||||
<PrimitiveAvatar.Image
|
||||
{...imageProps}
|
||||
className={image({ className: imageProps?.className })}
|
||||
src={imageSrc}
|
||||
/>
|
||||
)}
|
||||
<PrimitiveAvatar.Fallback asChild {...fallbackProps}>
|
||||
<div className={fallback({ className: fallbackProps?.className })}>
|
||||
{initials}
|
||||
</div>
|
||||
</PrimitiveAvatar.Fallback>
|
||||
</PrimitiveAvatar.Root>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Avatar';
|
||||
export * from './Avatar.theme';
|
||||
@@ -0,0 +1,43 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const badgeTheme = tv({
|
||||
slots: {
|
||||
wrapper: ['rounded-full', 'grid', 'place-content-center'],
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
primary: {
|
||||
wrapper: ['bg-controls-primary', 'text-elements-on-primary'],
|
||||
},
|
||||
secondary: {
|
||||
wrapper: ['bg-controls-secondary', 'text-elements-on-secondary'],
|
||||
},
|
||||
tertiary: {
|
||||
wrapper: [
|
||||
'bg-controls-tertiary',
|
||||
'border',
|
||||
'border-border-interactive/10',
|
||||
'text-elements-high-em',
|
||||
'shadow-button',
|
||||
],
|
||||
},
|
||||
inset: {
|
||||
wrapper: ['bg-controls-inset', 'text-elements-high-em'],
|
||||
},
|
||||
},
|
||||
size: {
|
||||
sm: {
|
||||
wrapper: ['h-5', 'w-5', 'text-xs'],
|
||||
},
|
||||
xs: {
|
||||
wrapper: ['h-4', 'w-4', 'text-2xs', 'font-medium'],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
size: 'sm',
|
||||
},
|
||||
});
|
||||
|
||||
export type BadgeTheme = VariantProps<typeof badgeTheme>;
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
import { BadgeTheme, badgeTheme } from './Badge.theme';
|
||||
|
||||
export interface BadgeProps
|
||||
extends ComponentPropsWithoutRef<'div'>,
|
||||
BadgeTheme {}
|
||||
|
||||
/**
|
||||
* A badge is a small status descriptor for UI elements.
|
||||
* It can be used to indicate a status, a count, or a category.
|
||||
* It is typically used in lists, tables, or navigation elements.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Badge variant="primary" size="sm">1</Badge
|
||||
* ```
|
||||
*/
|
||||
export const Badge = ({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: BadgeProps) => {
|
||||
const { wrapper } = badgeTheme();
|
||||
|
||||
return (
|
||||
<div {...props} className={wrapper({ className, variant, size })}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Badge';
|
||||
@@ -0,0 +1,185 @@
|
||||
import { tv } from 'tailwind-variants';
|
||||
import type { VariantProps } from 'tailwind-variants';
|
||||
|
||||
/**
|
||||
* Defines the theme for a button component.
|
||||
*/
|
||||
export const buttonTheme = tv(
|
||||
{
|
||||
base: [
|
||||
'h-fit',
|
||||
'inline-flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'whitespace-nowrap',
|
||||
'focus-ring',
|
||||
'disabled:cursor-not-allowed',
|
||||
],
|
||||
variants: {
|
||||
size: {
|
||||
lg: ['gap-2', 'py-3.5', 'px-5', 'text-base', 'tracking-[-0.011em]'],
|
||||
md: ['gap-2', 'py-3.25', 'px-5', 'text-sm', 'tracking-[-0.006em]'],
|
||||
sm: ['gap-1', 'py-2', 'px-3', 'text-xs'],
|
||||
xs: ['gap-1', 'py-1', 'px-2', 'text-xs'],
|
||||
},
|
||||
fullWidth: {
|
||||
true: 'w-full',
|
||||
},
|
||||
shape: {
|
||||
default: '',
|
||||
rounded: 'rounded-full',
|
||||
},
|
||||
iconOnly: {
|
||||
true: '',
|
||||
},
|
||||
variant: {
|
||||
primary: [
|
||||
'text-elements-on-primary',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'bg-controls-primary',
|
||||
'shadow-button',
|
||||
'hover:bg-controls-primary-hovered',
|
||||
'focus-visible:bg-controls-primary-hovered',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
secondary: [
|
||||
'text-elements-on-secondary',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'bg-controls-secondary',
|
||||
'hover:bg-controls-secondary-hovered',
|
||||
'focus-visible:bg-controls-secondary-hovered',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
tertiary: [
|
||||
'text-elements-on-tertiary',
|
||||
'border',
|
||||
'border-border-interactive/10',
|
||||
'bg-controls-tertiary',
|
||||
'hover:bg-controls-tertiary-hovered',
|
||||
'hover:border-border-interactive-hovered',
|
||||
'hover:border-border-interactive-hovered/[0.14]',
|
||||
'focus-visible:bg-controls-tertiary-hovered',
|
||||
'focus-visible:border-border-interactive-hovered',
|
||||
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
ghost: [
|
||||
'text-elements-on-tertiary',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'bg-transparent',
|
||||
'hover:bg-controls-tertiary-hovered',
|
||||
'hover:border-border-interactive-hovered',
|
||||
'hover:border-border-interactive-hovered/[0.14]',
|
||||
'focus-visible:bg-controls-tertiary-hovered',
|
||||
'focus-visible:border-border-interactive-hovered',
|
||||
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
danger: [
|
||||
'text-elements-on-danger',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'bg-border-danger',
|
||||
'hover:bg-controls-danger-hovered',
|
||||
'focus-visible:bg-controls-danger-hovered',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
'danger-ghost': [
|
||||
'text-elements-danger',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'bg-transparent',
|
||||
'hover:bg-controls-tertiary-hovered',
|
||||
'hover:border-border-interactive-hovered',
|
||||
'hover:border-border-interactive-hovered/[0.14]',
|
||||
'focus-visible:bg-controls-tertiary-hovered',
|
||||
'focus-visible:border-border-interactive-hovered',
|
||||
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
link: [
|
||||
'p-0',
|
||||
'gap-1.5',
|
||||
'text-elements-link',
|
||||
'rounded',
|
||||
'focus-ring',
|
||||
'hover:underline',
|
||||
'hover:text-elements-link-hovered',
|
||||
'disabled:text-controls-disabled',
|
||||
'disabled:hover:no-underline',
|
||||
],
|
||||
'link-emphasized': [
|
||||
'p-0',
|
||||
'gap-1.5',
|
||||
'text-elements-high-em',
|
||||
'rounded',
|
||||
'underline',
|
||||
'focus-ring',
|
||||
'hover:text-elements-link-hovered',
|
||||
'disabled:text-controls-disabled',
|
||||
'disabled:hover:no-underline',
|
||||
'dark:text-elements-on-high-contrast',
|
||||
],
|
||||
unstyled: [],
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
size: 'lg',
|
||||
iconOnly: true,
|
||||
class: ['py-3.5', 'px-3.5'],
|
||||
},
|
||||
{
|
||||
size: 'md',
|
||||
iconOnly: true,
|
||||
class: ['py-3.25', 'px-3.25'],
|
||||
},
|
||||
{
|
||||
size: 'sm',
|
||||
iconOnly: true,
|
||||
class: ['py-2', 'px-2'],
|
||||
},
|
||||
{
|
||||
size: 'xs',
|
||||
iconOnly: true,
|
||||
class: ['py-1', 'px-1'],
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
variant: 'primary',
|
||||
fullWidth: false,
|
||||
iconOnly: false,
|
||||
shape: 'rounded',
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: true,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Represents the type of a button theme, which is derived from the `buttonTheme` variant props.
|
||||
*/
|
||||
export type ButtonTheme = VariantProps<typeof buttonTheme>;
|
||||
@@ -0,0 +1,186 @@
|
||||
import React, { forwardRef, useCallback } from 'react';
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
|
||||
import { buttonTheme } from './Button.theme';
|
||||
import type { ButtonTheme } from './Button.theme';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { cloneIcon } from 'utils/cloneIcon';
|
||||
|
||||
/**
|
||||
* Represents the properties of a base button component.
|
||||
*/
|
||||
export interface ButtonBaseProps {
|
||||
/**
|
||||
* The optional left icon element for a component.
|
||||
* @type {ReactNode}
|
||||
*/
|
||||
leftIcon?: ReactNode;
|
||||
/**
|
||||
* The optional right icon element to display.
|
||||
* @type {ReactNode}
|
||||
*/
|
||||
rightIcon?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the props of a button link component.
|
||||
*/
|
||||
export interface ButtonLinkProps
|
||||
extends Omit<ComponentPropsWithoutRef<'a'>, 'color'> {
|
||||
/**
|
||||
* Specifies the optional property `as` with a value of `'a'`.
|
||||
* @type {'a'}
|
||||
*/
|
||||
as?: 'a';
|
||||
/**
|
||||
* Indicates whether the item is external or not.
|
||||
* @type {boolean}
|
||||
*/
|
||||
external?: boolean;
|
||||
/**
|
||||
* The URL of a web page or resource.
|
||||
* @type {string}
|
||||
*/
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface ButtonProps
|
||||
extends Omit<ComponentPropsWithoutRef<'button'>, 'color'> {
|
||||
/**
|
||||
* Specifies the optional property `as` with a value of `'button'`.
|
||||
* @type {'button'}
|
||||
*/
|
||||
as?: 'button';
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface representing the props for a button component.
|
||||
* Extends the ComponentPropsWithoutRef<'button'> and ButtonTheme interfaces.
|
||||
*/
|
||||
export type ButtonOrLinkProps = (ButtonLinkProps | ButtonProps) &
|
||||
ButtonBaseProps &
|
||||
ButtonTheme;
|
||||
|
||||
/**
|
||||
* A custom button component that can be used in React applications.
|
||||
*/
|
||||
const Button = forwardRef<
|
||||
HTMLButtonElement | HTMLAnchorElement,
|
||||
ButtonOrLinkProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
fullWidth,
|
||||
iconOnly,
|
||||
shape,
|
||||
variant,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
// Conditionally render between <NextLink>, <a> or <button> depending on props
|
||||
// useCallback to prevent unnecessary re-rendering
|
||||
const Component = useCallback(
|
||||
({ children: _children, ..._props }: ButtonOrLinkProps) => {
|
||||
if (_props.as === 'a') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { external, href, as, ...baseLinkProps } = _props;
|
||||
|
||||
// External link
|
||||
if (external) {
|
||||
const externalLinkProps = {
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
href,
|
||||
...baseLinkProps,
|
||||
};
|
||||
return (
|
||||
// @ts-expect-error - ref
|
||||
<a ref={ref} {...externalLinkProps}>
|
||||
{_children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// Internal link
|
||||
return (
|
||||
// @ts-expect-error - ref
|
||||
<Link ref={ref} {...baseLinkProps} to={href}>
|
||||
{_children}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
const { ...buttonProps } = _props;
|
||||
return (
|
||||
// @ts-expect-error - as prop is not a valid prop for button elements
|
||||
<button ref={ref} {...buttonProps}>
|
||||
{_children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Extracts specific style properties from the given props object and returns them as a new object.
|
||||
*/
|
||||
const styleProps = (({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
fullWidth = false,
|
||||
iconOnly = false,
|
||||
shape = 'rounded',
|
||||
as,
|
||||
}) => ({
|
||||
variant,
|
||||
size,
|
||||
fullWidth,
|
||||
iconOnly,
|
||||
shape,
|
||||
as,
|
||||
}))({ ...props, fullWidth, iconOnly, shape, variant });
|
||||
|
||||
/**
|
||||
* Validates that a button component has either children or an aria-label prop.
|
||||
*/
|
||||
if (typeof children === 'undefined' && !props['aria-label']) {
|
||||
throw new Error(
|
||||
'Button components must have either children or an aria-label prop',
|
||||
);
|
||||
}
|
||||
|
||||
const iconSize = useCallback(() => {
|
||||
switch (styleProps.size) {
|
||||
case 'lg':
|
||||
return { width: 20, height: 20 };
|
||||
case 'sm':
|
||||
case 'xs':
|
||||
return { width: 16, height: 16 };
|
||||
case 'md':
|
||||
default: {
|
||||
return { width: 18, height: 18 };
|
||||
}
|
||||
}
|
||||
}, [styleProps.size])();
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
className={buttonTheme({ ...styleProps, class: className })}
|
||||
>
|
||||
{cloneIcon(leftIcon, { ...iconSize })}
|
||||
{children}
|
||||
{cloneIcon(rightIcon, { ...iconSize })}
|
||||
</Component>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button };
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Button';
|
||||
export * from './Button.theme';
|
||||
@@ -0,0 +1,128 @@
|
||||
/* React Calendar */
|
||||
.react-calendar {
|
||||
@apply border-none font-sans;
|
||||
}
|
||||
|
||||
/* Weekdays -- START */
|
||||
.react-calendar__month-view__weekdays {
|
||||
@apply p-0 flex items-center justify-center;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays__weekday {
|
||||
@apply h-8 w-12 flex items-center justify-center p-0 font-medium text-xs text-elements-disabled mb-2;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
text-decoration: none;
|
||||
}
|
||||
/* Weekdays -- END */
|
||||
|
||||
/* Days -- START */
|
||||
.react-calendar__month-view__days {
|
||||
@apply p-0 gap-0;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth {
|
||||
@apply !text-elements-disabled;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth:hover {
|
||||
@apply !text-elements-disabled !bg-transparent;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth {
|
||||
@apply !text-elements-disabled !bg-transparent;
|
||||
}
|
||||
|
||||
/* For weekend days */
|
||||
.react-calendar__month-view__days__day--weekend {
|
||||
/* color: ${colors.grey[950]} !important; */
|
||||
}
|
||||
|
||||
.react-calendar__tile {
|
||||
@apply h-12 w-12 text-elements-high-em;
|
||||
}
|
||||
|
||||
.react-calendar__tile:hover {
|
||||
@apply bg-base-bg-emphasized rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile:focus-visible {
|
||||
@apply bg-base-bg-emphasized rounded-lg focus-ring z-10;
|
||||
}
|
||||
|
||||
.react-calendar__tile--now {
|
||||
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--now:hover {
|
||||
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--now:focus-visible {
|
||||
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg focus-ring;
|
||||
}
|
||||
|
||||
.react-calendar__tile--active {
|
||||
@apply bg-controls-primary text-elements-on-primary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--active:hover {
|
||||
@apply bg-controls-primary-hovered;
|
||||
}
|
||||
|
||||
.react-calendar__tile--active:focus-visible {
|
||||
@apply bg-controls-primary-hovered focus-ring;
|
||||
}
|
||||
|
||||
/* Range -- START */
|
||||
.react-calendar__tile--range {
|
||||
@apply bg-controls-secondary text-elements-on-secondary rounded-none;
|
||||
}
|
||||
|
||||
.react-calendar__tile--range:hover {
|
||||
@apply bg-controls-secondary-hovered text-elements-on-secondary rounded-none;
|
||||
}
|
||||
|
||||
.react-calendar__tile--range:focus-visible {
|
||||
@apply bg-controls-secondary-hovered text-elements-on-secondary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeStart {
|
||||
@apply bg-controls-primary text-elements-on-primary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeStart:hover {
|
||||
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeStart:focus-visible {
|
||||
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeEnd {
|
||||
@apply bg-controls-primary text-elements-on-primary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeEnd:hover {
|
||||
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeEnd:focus-visible {
|
||||
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring;
|
||||
}
|
||||
/* Range -- END */
|
||||
/* Days -- END */
|
||||
|
||||
/* Months -- START */
|
||||
.react-calendar__tile--hasActive {
|
||||
@apply bg-controls-primary text-elements-on-primary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--hasActive:hover {
|
||||
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--hasActive:focus-visible {
|
||||
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const calendarTheme = tv({
|
||||
slots: {
|
||||
wrapper: [
|
||||
'max-w-[352px]',
|
||||
'bg-surface-floating',
|
||||
'shadow-calendar',
|
||||
'rounded-xl',
|
||||
],
|
||||
calendar: ['flex', 'flex-col', 'py-2', 'px-2', 'gap-2'],
|
||||
navigation: [
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-between',
|
||||
'gap-3',
|
||||
'py-2.5',
|
||||
'px-1',
|
||||
],
|
||||
actions: ['flex', 'items-center', 'justify-center', 'gap-1.5', 'flex-1'],
|
||||
button: [
|
||||
'flex',
|
||||
'items-center',
|
||||
'gap-2',
|
||||
'px-2',
|
||||
'py-2',
|
||||
'rounded-lg',
|
||||
'border',
|
||||
'border-border-interactive',
|
||||
'text-elements-high-em',
|
||||
'shadow-field',
|
||||
'bg-white',
|
||||
'hover:bg-base-bg-alternate',
|
||||
'focus-visible:bg-base-bg-alternate',
|
||||
],
|
||||
footer: [
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-end',
|
||||
'py-3',
|
||||
'px-2',
|
||||
'gap-3',
|
||||
'border-t',
|
||||
'border-border-separator',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export type CalendarTheme = VariantProps<typeof calendarTheme>;
|
||||
@@ -0,0 +1,299 @@
|
||||
import React, {
|
||||
ComponentPropsWithRef,
|
||||
MouseEvent,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
Calendar as ReactCalendar,
|
||||
CalendarProps as ReactCalendarProps,
|
||||
} from 'react-calendar';
|
||||
import { CalendarTheme, calendarTheme } from './Calendar.theme';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
ChevronGrabberHorizontal,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from 'components/shared/CustomIcon';
|
||||
|
||||
import './Calendar.css';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
type ValuePiece = Date | null;
|
||||
export type Value = ValuePiece | [ValuePiece, ValuePiece];
|
||||
const CALENDAR_VIEW = ['month', 'year', 'decade'] as const;
|
||||
export type CalendarView = (typeof CALENDAR_VIEW)[number];
|
||||
|
||||
/**
|
||||
* Defines a custom set of props for a React calendar component by excluding specific props
|
||||
* from the original ReactCalendarProps type.
|
||||
* @type {CustomReactCalendarProps}
|
||||
*/
|
||||
type CustomReactCalendarProps = Omit<
|
||||
ReactCalendarProps,
|
||||
'view' | 'showNavigation' | 'onClickMonth' | 'onClickYear'
|
||||
>;
|
||||
|
||||
export interface CalendarProps extends CustomReactCalendarProps, CalendarTheme {
|
||||
/**
|
||||
* Optional props for wrapping a component with a div element.
|
||||
*/
|
||||
wrapperProps?: ComponentPropsWithRef<'div'>;
|
||||
/**
|
||||
* Props for the calendar wrapper component.
|
||||
*/
|
||||
calendarWrapperProps?: ComponentPropsWithRef<'div'>;
|
||||
/**
|
||||
* Optional props for the footer component.
|
||||
*/
|
||||
footerProps?: ComponentPropsWithRef<'div'>;
|
||||
/**
|
||||
* Optional custom actions to be rendered.
|
||||
*/
|
||||
actions?: ReactNode;
|
||||
/**
|
||||
* Optional callback function that is called when a value is selected.
|
||||
* @param {Value} value - The selected value
|
||||
* @returns None
|
||||
*/
|
||||
onSelect?: (value: Value) => void;
|
||||
/**
|
||||
* Optional callback function that is called when a cancel action is triggered.
|
||||
* @returns None
|
||||
*/
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calendar component that allows users to select dates and navigate through months and years.
|
||||
* @param {Object} CalendarProps - Props for the Calendar component.
|
||||
* @returns {JSX.Element} A calendar component with navigation, date selection, and actions.
|
||||
*/
|
||||
export const Calendar = ({
|
||||
selectRange,
|
||||
activeStartDate: activeStartDateProp,
|
||||
value: valueProp,
|
||||
wrapperProps,
|
||||
calendarWrapperProps,
|
||||
footerProps,
|
||||
actions,
|
||||
onSelect,
|
||||
onCancel,
|
||||
onChange: onChangeProp,
|
||||
...props
|
||||
}: CalendarProps): JSX.Element => {
|
||||
const {
|
||||
wrapper,
|
||||
calendar,
|
||||
navigation,
|
||||
actions: actionsClass,
|
||||
button,
|
||||
footer,
|
||||
} = calendarTheme();
|
||||
|
||||
const today = new Date();
|
||||
const currentMonth = format(today, 'MMM');
|
||||
const currentYear = format(today, 'yyyy');
|
||||
|
||||
const [view, setView] = useState<CalendarView>('month');
|
||||
const [activeDate, setActiveDate] = useState<Date>(
|
||||
activeStartDateProp ?? today,
|
||||
);
|
||||
const [value, setValue] = useState<Value>(valueProp as Value);
|
||||
const [month, setMonth] = useState(currentMonth);
|
||||
const [year, setYear] = useState(currentYear);
|
||||
|
||||
/**
|
||||
* Update the navigation label based on the active date
|
||||
*/
|
||||
const changeNavigationLabel = useCallback(
|
||||
(date: Date) => {
|
||||
setMonth(format(date, 'MMM'));
|
||||
setYear(format(date, 'yyyy'));
|
||||
},
|
||||
[setMonth, setYear],
|
||||
);
|
||||
|
||||
/**
|
||||
* Change the active date base on the action and range
|
||||
*/
|
||||
const handleNavigate = useCallback(
|
||||
(action: 'previous' | 'next', view: CalendarView) => {
|
||||
setActiveDate((date) => {
|
||||
const newDate = new Date(date);
|
||||
switch (view) {
|
||||
case 'month':
|
||||
newDate.setMonth(
|
||||
action === 'previous' ? date.getMonth() - 1 : date.getMonth() + 1,
|
||||
);
|
||||
break;
|
||||
case 'year':
|
||||
newDate.setFullYear(
|
||||
action === 'previous'
|
||||
? date.getFullYear() - 1
|
||||
: date.getFullYear() + 1,
|
||||
);
|
||||
break;
|
||||
case 'decade':
|
||||
newDate.setFullYear(
|
||||
action === 'previous'
|
||||
? date.getFullYear() - 10
|
||||
: date.getFullYear() + 10,
|
||||
);
|
||||
break;
|
||||
}
|
||||
changeNavigationLabel(newDate);
|
||||
return newDate;
|
||||
});
|
||||
},
|
||||
[setActiveDate, changeNavigationLabel],
|
||||
);
|
||||
|
||||
/**
|
||||
* Change the view of the calendar
|
||||
*/
|
||||
const handleChangeView = useCallback(
|
||||
(view: CalendarView) => {
|
||||
setView(view);
|
||||
},
|
||||
[setView],
|
||||
);
|
||||
|
||||
/**
|
||||
* Change the active date and set the view to the selected type
|
||||
* and also update the navigation label
|
||||
*/
|
||||
const handleChangeNavigation = useCallback(
|
||||
(view: 'month' | 'year', date: Date) => {
|
||||
setActiveDate(date);
|
||||
changeNavigationLabel(date);
|
||||
setView(view);
|
||||
},
|
||||
[setActiveDate, changeNavigationLabel, setView],
|
||||
);
|
||||
|
||||
const handlePrevious = useCallback(() => {
|
||||
switch (view) {
|
||||
case 'month':
|
||||
return handleNavigate('previous', 'month');
|
||||
case 'year':
|
||||
return handleNavigate('previous', 'year');
|
||||
case 'decade':
|
||||
return handleNavigate('previous', 'decade');
|
||||
}
|
||||
}, [view]);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
switch (view) {
|
||||
case 'month':
|
||||
return handleNavigate('next', 'month');
|
||||
case 'year':
|
||||
return handleNavigate('next', 'year');
|
||||
case 'decade':
|
||||
return handleNavigate('next', 'decade');
|
||||
}
|
||||
}, [view]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValue: Value, event: MouseEvent<HTMLButtonElement>) => {
|
||||
setValue(newValue);
|
||||
|
||||
// Call the onChange prop if it exists
|
||||
onChangeProp?.(newValue, event);
|
||||
|
||||
/**
|
||||
* Update the active date and navigation label
|
||||
*
|
||||
* NOTE:
|
||||
* For range selection, the active date is not updated
|
||||
* The user only can select multiple dates within the same month
|
||||
*/
|
||||
if (!selectRange) {
|
||||
setActiveDate(newValue as Date);
|
||||
changeNavigationLabel(newValue as Date);
|
||||
}
|
||||
},
|
||||
[setValue, setActiveDate, changeNavigationLabel, selectRange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...wrapperProps}
|
||||
className={wrapper({ className: wrapperProps?.className })}
|
||||
>
|
||||
{/* Calendar wrapper */}
|
||||
<div
|
||||
{...calendarWrapperProps}
|
||||
className={calendar({ className: calendarWrapperProps?.className })}
|
||||
>
|
||||
{/* Navigation */}
|
||||
<div className={navigation()}>
|
||||
<Button iconOnly size="sm" variant="ghost" onClick={handlePrevious}>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
<div className={actionsClass()}>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className={button()}
|
||||
rightIcon={
|
||||
<ChevronGrabberHorizontal className="text-elements-low-em" />
|
||||
}
|
||||
onClick={() => handleChangeView('year')}
|
||||
>
|
||||
{month}
|
||||
</Button>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className={button()}
|
||||
rightIcon={
|
||||
<ChevronGrabberHorizontal className="text-elements-low-em" />
|
||||
}
|
||||
onClick={() => handleChangeView('decade')}
|
||||
>
|
||||
{year}
|
||||
</Button>
|
||||
</div>
|
||||
<Button iconOnly size="sm" variant="ghost" onClick={handleNext}>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Calendar */}
|
||||
<ReactCalendar
|
||||
{...props}
|
||||
activeStartDate={activeDate}
|
||||
view={view}
|
||||
value={value}
|
||||
showNavigation={false}
|
||||
selectRange={selectRange}
|
||||
onChange={handleChange}
|
||||
onClickMonth={(date) => handleChangeNavigation('month', date)}
|
||||
onClickYear={(date) => handleChangeNavigation('year', date)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer or CTA */}
|
||||
<div
|
||||
{...footerProps}
|
||||
className={footer({ className: footerProps?.className })}
|
||||
>
|
||||
{actions ? (
|
||||
actions
|
||||
) : (
|
||||
<>
|
||||
<Button variant="tertiary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!value}
|
||||
onClick={() => (value ? onSelect?.(value) : null)}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Calendar';
|
||||
@@ -0,0 +1,68 @@
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const getCheckboxVariant = tv({
|
||||
slots: {
|
||||
wrapper: ['group', 'flex', 'gap-3'],
|
||||
indicator: [
|
||||
'grid',
|
||||
'place-content-center',
|
||||
'text-transparent',
|
||||
'group-hover:text-controls-disabled',
|
||||
'focus-visible:text-controls-disabled',
|
||||
'group-focus-visible:text-controls-disabled',
|
||||
'data-[state=checked]:text-elements-on-primary',
|
||||
'data-[state=checked]:group-focus-visible:text-elements-on-primary',
|
||||
'data-[state=indeterminate]:text-elements-on-primary',
|
||||
'data-[state=checked]:data-[disabled]:text-elements-on-disabled-active',
|
||||
],
|
||||
icon: ['w-3', 'h-3', 'stroke-current', 'text-current'],
|
||||
input: [
|
||||
'h-5',
|
||||
'w-5',
|
||||
'group',
|
||||
'border',
|
||||
'border-border-interactive/10',
|
||||
'bg-controls-tertiary',
|
||||
'rounded-md',
|
||||
'transition-all',
|
||||
'duration-150',
|
||||
'focus-ring',
|
||||
'shadow-button',
|
||||
'group-hover:border-border-interactive/[0.14]',
|
||||
'group-hover:bg-controls-tertiary',
|
||||
'data-[state=checked]:bg-controls-primary',
|
||||
'data-[state=checked]:hover:bg-controls-primary-hovered',
|
||||
'data-[state=checked]:focus-visible:bg-controls-primary-hovered',
|
||||
'data-[disabled]:bg-controls-disabled',
|
||||
'data-[disabled]:shadow-none',
|
||||
'data-[disabled]:hover:border-border-interactive/10',
|
||||
'data-[disabled]:cursor-not-allowed',
|
||||
'data-[state=checked]:data-[disabled]:bg-controls-disabled-active',
|
||||
],
|
||||
label: [
|
||||
'text-sm',
|
||||
'tracking-[-0.006em]',
|
||||
'text-elements-high-em',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-1',
|
||||
'px-1',
|
||||
],
|
||||
description: ['text-xs', 'text-elements-low-em'],
|
||||
},
|
||||
variants: {
|
||||
disabled: {
|
||||
true: {
|
||||
wrapper: ['cursor-not-allowed'],
|
||||
indicator: ['group-hover:text-transparent'],
|
||||
input: [
|
||||
'group-hover:border-border-interactive/[0.14]',
|
||||
'group-hover:bg-controls-disabled',
|
||||
],
|
||||
label: ['cursor-not-allowed'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type CheckboxVariants = VariantProps<typeof getCheckboxVariant>;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import * as CheckboxRadix from '@radix-ui/react-checkbox';
|
||||
import { type CheckboxProps as CheckboxRadixProps } from '@radix-ui/react-checkbox';
|
||||
|
||||
import { getCheckboxVariant, type CheckboxVariants } from './Checkbox.theme';
|
||||
import { CheckIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
interface CheckBoxProps extends CheckboxRadixProps, CheckboxVariants {
|
||||
/**
|
||||
* The label of the checkbox.
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* The description of the checkbox.
|
||||
*/
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox component is used to allow users to select one or more items from a set.
|
||||
* It is a wrapper around the `@radix-ui/react-checkbox` component.
|
||||
*
|
||||
* It accepts all the props from `@radix-ui/react-checkbox` component and the variants from the theme.
|
||||
*
|
||||
* It also accepts `label` and `description` props to display the label and description of the checkbox.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Checkbox
|
||||
* id="checkbox"
|
||||
* label="Checkbox"
|
||||
* description="This is a checkbox"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const Checkbox = ({
|
||||
id,
|
||||
className,
|
||||
label,
|
||||
description,
|
||||
onCheckedChange,
|
||||
...props
|
||||
}: CheckBoxProps) => {
|
||||
const {
|
||||
wrapper: wrapperStyle,
|
||||
indicator: indicatorStyle,
|
||||
icon: iconStyle,
|
||||
input: inputStyle,
|
||||
label: labelStyle,
|
||||
description: descriptionStyle,
|
||||
} = getCheckboxVariant({
|
||||
disabled: props?.disabled,
|
||||
});
|
||||
return (
|
||||
<div className={wrapperStyle()}>
|
||||
<CheckboxRadix.Root
|
||||
{...props}
|
||||
className={inputStyle({ className })}
|
||||
id={id}
|
||||
onCheckedChange={onCheckedChange}
|
||||
>
|
||||
<CheckboxRadix.Indicator forceMount className={indicatorStyle()}>
|
||||
<CheckIcon className={iconStyle()} />
|
||||
</CheckboxRadix.Indicator>
|
||||
</CheckboxRadix.Root>
|
||||
{label && (
|
||||
<label className={labelStyle()} htmlFor={id}>
|
||||
{label}
|
||||
{description && (
|
||||
<span className={descriptionStyle()}>{description}</span>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Checkbox';
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CalendarIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="19"
|
||||
viewBox="0 0 18 19"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5 0C5.55228 0 6 0.447715 6 1V2H12V1C12 0.447715 12.4477 0 13 0C13.5523 0 14 0.447715 14 1V2H16C17.1046 2 18 2.89543 18 4V17C18 18.1046 17.1046 19 16 19H2C0.89543 19 0 18.1046 0 17V4C0 2.89543 0.895431 2 2 2H4V1C4 0.447715 4.44772 0 5 0ZM2 9V17H16V9H2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CheckIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M1.5 7.5L4.64706 10L10.5 2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CheckRoundFilledIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM15.774 10.1333C16.1237 9.70582 16.0607 9.0758 15.6332 8.72607C15.2058 8.37635 14.5758 8.43935 14.226 8.86679L10.4258 13.5116L9.20711 12.2929C8.81658 11.9024 8.18342 11.9024 7.79289 12.2929C7.40237 12.6834 7.40237 13.3166 7.79289 13.7071L9.79289 15.7071C9.99267 15.9069 10.2676 16.0129 10.5498 15.9988C10.832 15.9847 11.095 15.8519 11.274 15.6333L15.774 10.1333Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ChevronGrabberHorizontal = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M6.66666 12.5L9.99999 15.8333L13.3333 12.5M6.66666 7.5L9.99999 4.16666L13.3333 7.5"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ChevronLeft = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.7071 2.66666L5.37378 8.00001L10.7071 13.3333L10 14.0404L3.95956 8.00001L10 1.95956L10.7071 2.66666Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ChevronRight = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.00001 1.95956L12.0405 7.99998L6.00001 14.0404L5.29291 13.3333L10.6262 7.99999L5.29291 2.66666L6.00001 1.95956Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CrossIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M5 5L19 19M19 5L5 19"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
|
||||
export interface CustomIconProps extends ComponentPropsWithoutRef<'svg'> {
|
||||
size?: number | string; // width and height will both be set as the same value
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export const CustomIcon = ({
|
||||
children,
|
||||
width = 24,
|
||||
height = 24,
|
||||
size,
|
||||
viewBox = '0 0 24 24',
|
||||
name,
|
||||
...rest
|
||||
}: CustomIconProps) => {
|
||||
return (
|
||||
<svg
|
||||
aria-labelledby={name}
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox={viewBox}
|
||||
width={size || width}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const GlobeIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M10 17.9167C14.3723 17.9167 17.9167 14.3723 17.9167 10C17.9167 5.62776 14.3723 2.08334 10 2.08334M10 17.9167C5.62776 17.9167 2.08334 14.3723 2.08334 10C2.08334 5.62776 5.62776 2.08334 10 2.08334M10 17.9167C8.15906 17.9167 6.66668 14.3723 6.66668 10C6.66668 5.62776 8.15906 2.08334 10 2.08334M10 17.9167C11.841 17.9167 13.3333 14.3723 13.3333 10C13.3333 5.62776 11.841 2.08334 10 2.08334M17.5 10H2.50001"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const InfoRoundFilledIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM10 11C10 10.4477 10.4477 10 11 10H12C12.5523 10 13 10.4477 13 11V16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16V12C10.4477 12 10 11.5523 10 11ZM12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9C12.5523 9 13 8.55228 13 8C13 7.44772 12.5523 7 12 7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const InfoSquareIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.96786 2.5C5.52858 2.49999 5.14962 2.49997 4.83748 2.52548C4.50802 2.55239 4.18034 2.61182 3.86503 2.77249C3.39462 3.01217 3.01217 3.39462 2.77249 3.86503C2.61182 4.18034 2.55239 4.50802 2.52548 4.83748C2.49997 5.14962 2.49999 5.52857 2.5 5.96785V14.0321C2.49999 14.4714 2.49997 14.8504 2.52548 15.1625C2.55239 15.492 2.61182 15.8197 2.77249 16.135C3.01217 16.6054 3.39462 16.9878 3.86503 17.2275C4.18034 17.3882 4.50802 17.4476 4.83748 17.4745C5.14962 17.5 5.52858 17.5 5.96786 17.5H14.0321C14.4714 17.5 14.8504 17.5 15.1625 17.4745C15.492 17.4476 15.8197 17.3882 16.135 17.2275C16.6054 16.9878 16.9878 16.6054 17.2275 16.135C17.3882 15.8197 17.4476 15.492 17.4745 15.1625C17.5 14.8504 17.5 14.4714 17.5 14.0321V5.96786C17.5 5.52858 17.5 5.14962 17.4745 4.83748C17.4476 4.50802 17.3882 4.18034 17.2275 3.86503C16.9878 3.39462 16.6054 3.01217 16.135 2.77249C15.8197 2.61182 15.492 2.55239 15.1625 2.52548C14.8504 2.49997 14.4714 2.49999 14.0322 2.5H5.96786ZM8.33333 9.16667C8.33333 8.70643 8.70643 8.33333 9.16667 8.33333H10C10.4602 8.33333 10.8333 8.70643 10.8333 9.16667V13.3333C10.8333 13.7936 10.4602 14.1667 10 14.1667C9.53976 14.1667 9.16667 13.7936 9.16667 13.3333V10C8.70643 10 8.33333 9.6269 8.33333 9.16667ZM10 5.83333C9.53976 5.83333 9.16667 6.20643 9.16667 6.66667C9.16667 7.1269 9.53976 7.5 10 7.5C10.4602 7.5 10.8333 7.1269 10.8333 6.66667C10.8333 6.20643 10.4602 5.83333 10 5.83333Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const LoadingIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.0002 1.66669C10.4605 1.66669 10.8336 2.03978 10.8336 2.50002V5.00002C10.8336 5.46026 10.4605 5.83335 10.0002 5.83335C9.54 5.83335 9.1669 5.46026 9.1669 5.00002V2.50002C9.1669 2.03978 9.54 1.66669 10.0002 1.66669ZM15.8928 4.10746C16.2182 4.4329 16.2182 4.96054 15.8928 5.28597L14.125 7.05374C13.7996 7.37918 13.272 7.37918 12.9465 7.05374C12.6211 6.7283 12.6211 6.20067 12.9465 5.87523L14.7143 4.10746C15.0397 3.78203 15.5674 3.78203 15.8928 4.10746ZM4.10768 4.10746C4.43312 3.78203 4.96076 3.78203 5.28619 4.10746L7.05396 5.87523C7.3794 6.20067 7.3794 6.7283 7.05396 7.05374C6.72852 7.37918 6.20088 7.37918 5.87545 7.05374L4.10768 5.28597C3.78224 4.96054 3.78224 4.4329 4.10768 4.10746ZM1.66666 10.0006C1.66666 9.54035 2.03975 9.16725 2.49999 9.16725H4.99999C5.46023 9.16725 5.83332 9.54035 5.83332 10.0006C5.83332 10.4608 5.46023 10.8339 4.99999 10.8339H2.49999C2.03975 10.8339 1.66666 10.4608 1.66666 10.0006ZM14.1667 10.0006C14.1667 9.54035 14.5398 9.16725 15 9.16725H17.5C17.9602 9.16725 18.3333 9.54035 18.3333 10.0006C18.3333 10.4608 17.9602 10.8339 17.5 10.8339H15C14.5398 10.8339 14.1667 10.4608 14.1667 10.0006ZM7.05396 12.9463C7.3794 13.2717 7.3794 13.7994 7.05396 14.1248L5.28619 15.8926C4.96076 16.218 4.43312 16.218 4.10768 15.8926C3.78224 15.5671 3.78224 15.0395 4.10768 14.7141L5.87545 12.9463C6.20088 12.6209 6.72852 12.6209 7.05396 12.9463ZM12.9465 12.9463C13.272 12.6209 13.7996 12.6209 14.125 12.9463L15.8928 14.7141C16.2182 15.0395 16.2182 15.5671 15.8928 15.8926C15.5674 16.218 15.0397 16.218 14.7143 15.8926L12.9465 14.1248C12.6211 13.7994 12.6211 13.2717 12.9465 12.9463ZM10.0002 14.1667C10.4605 14.1667 10.8336 14.5398 10.8336 15V17.5C10.8336 17.9603 10.4605 18.3334 10.0002 18.3334C9.54 18.3334 9.1669 17.9603 9.1669 17.5V15C9.1669 14.5398 9.54 14.1667 10.0002 14.1667Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const PlusIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.66666 9.99999C1.66666 5.39762 5.39762 1.66666 9.99999 1.66666C14.6024 1.66666 18.3333 5.39762 18.3333 9.99999C18.3333 14.6024 14.6024 18.3333 9.99999 18.3333C5.39762 18.3333 1.66666 14.6024 1.66666 9.99999ZM10.625 6.46483C10.625 6.11966 10.3452 5.83983 9.99999 5.83983C9.65481 5.83983 9.37499 6.11966 9.37499 6.46483V9.37537H6.46446C6.11928 9.37537 5.83946 9.65519 5.83946 10.0004C5.83946 10.3455 6.11928 10.6254 6.46446 10.6254H9.37499V13.5359C9.37499 13.8811 9.65481 14.1609 9.99999 14.1609C10.3452 14.1609 10.625 13.8811 10.625 13.5359V10.6254H13.5355C13.8807 10.6254 14.1605 10.3455 14.1605 10.0004C14.1605 9.65519 13.8807 9.37537 13.5355 9.37537H10.625V6.46483Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
# 1. What icons are compatible with this component?
|
||||
|
||||
- Viewbox "0 0 24 24": From where you're exporting from, please make sure the icon is using viewBox="0 0 24 24" before downloading/exporting. Not doing so will result in incorrect icon scaling
|
||||
|
||||
# 2. How to add a new icon?
|
||||
|
||||
**2.1 Sanitising the icon**
|
||||
|
||||
1. Duplicate a current icon e.g. CrossIcon and rename it accordingly.
|
||||
2. Rename the function inside the new file you duplicated too
|
||||
3. Replace the markup with your SVG markup (make sure it complies with the above section's rule)
|
||||
4. Depending on the svg you pasted...
|
||||
A. If the `<svg>` has only 1 child, remove the `<svg>` parent entirely so you only have the path left
|
||||
B. If your component has more than 1 paths, rename `<svg>` tag with the `<g>` tag. Then, remove all attributes of this `<g>` tag so that it's just `<g>`
|
||||
5. Usually, icons are single colored. If that's the case, replace all fill/stroke color with `currentColor`. E.g. <path d="..." fill="currentColor">. Leave the other attributes without removing them.
|
||||
6. If your icon has more than one colour, then it's up to you to decide whether we want to use tailwind to help set the fill and stroke colors
|
||||
7. Lastly, export your icon in `index.ts` by following what was done for CrossIcon
|
||||
8. Make sure to provide a name to the `<CustomIcon>` component for accessibility sake
|
||||
9. Done!
|
||||
|
||||
**2.3 Use your newly imported icon**
|
||||
|
||||
1. You can change simply use `<BellIcon size="32" />` to quickly change both width and height with the same value (square). For custom viewBox, width and height, simply provide all three props.
|
||||
2. Coloring the icon: Simply add a className with text color. E.g. `<BellIcon className="text-gray-500" />`
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const SearchIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M20 20L16.05 16.05M18 11C18 14.866 14.866 18 11 18C7.13401 18 4 14.866 4 11C4 7.13401 7.13401 4 11 4C14.866 4 18 7.13401 18 11Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const WarningIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.4902 2.84406C11.1661 1.69 12.8343 1.69 13.5103 2.84406L22.0156 17.3654C22.699 18.5321 21.8576 19.9999 20.5056 19.9999H3.49483C2.14281 19.9999 1.30147 18.5321 1.98479 17.3654L10.4902 2.84406ZM12 9C12.4142 9 12.75 9.33579 12.75 9.75V13.25C12.75 13.6642 12.4142 14 12 14C11.5858 14 11.25 13.6642 11.25 13.25V9.75C11.25 9.33579 11.5858 9 12 9ZM13 15.75C13 16.3023 12.5523 16.75 12 16.75C11.4477 16.75 11 16.3023 11 15.75C11 15.1977 11.4477 14.75 12 14.75C12.5523 14.75 13 15.1977 13 15.75Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
export * from './PlusIcon';
|
||||
export * from './CustomIcon';
|
||||
export * from './CheckIcon';
|
||||
export * from './ChevronGrabberHorizontal';
|
||||
export * from './ChevronLeft';
|
||||
export * from './ChevronRight';
|
||||
export * from './InfoSquareIcon';
|
||||
export * from './WarningIcon';
|
||||
export * from './SearchIcon';
|
||||
export * from './CrossIcon';
|
||||
export * from './GlobeIcon';
|
||||
export * from './CalendarIcon';
|
||||
export * from './CheckRoundFilledIcon';
|
||||
export * from './InfoRoundFilledIcon';
|
||||
export * from './LoadingIcon';
|
||||
@@ -0,0 +1,9 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const datePickerTheme = tv({
|
||||
slots: {
|
||||
input: [],
|
||||
},
|
||||
});
|
||||
|
||||
export type DatePickerTheme = VariantProps<typeof datePickerTheme>;
|
||||
@@ -0,0 +1,100 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Input, InputProps } from 'components/shared/Input';
|
||||
import * as Popover from '@radix-ui/react-popover';
|
||||
import { datePickerTheme } from './DatePicker.theme';
|
||||
import { Calendar, CalendarProps } from 'components/shared/Calendar';
|
||||
import { CalendarIcon } from 'components/shared/CustomIcon';
|
||||
import { Value } from 'react-calendar/dist/cjs/shared/types';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export interface DatePickerProps
|
||||
extends Omit<InputProps, 'onChange' | 'value'> {
|
||||
/**
|
||||
* The props for the calendar component.
|
||||
*/
|
||||
calendarProps?: CalendarProps;
|
||||
/**
|
||||
* Optional callback function that is called when the value of the input changes.
|
||||
* @param {string} value - The new value of the input.
|
||||
* @returns None
|
||||
*/
|
||||
onChange?: (value: Value) => void;
|
||||
/**
|
||||
* The value of the input.
|
||||
*/
|
||||
value?: Value;
|
||||
/**
|
||||
* Whether to allow the selection of a date range.
|
||||
*/
|
||||
selectRange?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A date picker component that allows users to select a date from a calendar.
|
||||
* @param {DatePickerProps} props - The props for the date picker component.
|
||||
* @returns The rendered date picker component.
|
||||
*/
|
||||
export const DatePicker = ({
|
||||
className,
|
||||
calendarProps,
|
||||
value,
|
||||
onChange,
|
||||
selectRange = false,
|
||||
...props
|
||||
}: DatePickerProps) => {
|
||||
const { input } = datePickerTheme();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
/**
|
||||
* Renders the value of the date based on the current state of `props.value`.
|
||||
* @returns {string | undefined} - The formatted date value or `undefined` if `props.value` is falsy.
|
||||
*/
|
||||
const renderValue = useCallback(() => {
|
||||
if (!value) return undefined;
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((date) => format(date as Date, 'dd/MM/yyyy'))
|
||||
.join(' - ');
|
||||
}
|
||||
return format(value, 'dd/MM/yyyy');
|
||||
}, [value]);
|
||||
|
||||
/**
|
||||
* Handles the selection of a date from the calendar.
|
||||
*/
|
||||
const handleSelect = useCallback(
|
||||
(date: Value) => {
|
||||
setOpen(false);
|
||||
onChange?.(date);
|
||||
},
|
||||
[setOpen, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover.Root open={open}>
|
||||
<Popover.Trigger>
|
||||
<Input
|
||||
{...props}
|
||||
rightIcon={<CalendarIcon onClick={() => setOpen(true)} />}
|
||||
readOnly
|
||||
placeholder="Select a date..."
|
||||
value={renderValue()}
|
||||
className={input({ className })}
|
||||
onClick={() => setOpen(true)}
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content onInteractOutside={() => setOpen(false)}>
|
||||
<Calendar
|
||||
{...calendarProps}
|
||||
selectRange={selectRange}
|
||||
value={value}
|
||||
onCancel={() => setOpen(false)}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './DatePicker';
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const inlineNotificationTheme = tv({
|
||||
slots: {
|
||||
wrapper: ['rounded-xl', 'flex', 'gap-2', 'items-start', 'w-full', 'border'],
|
||||
content: ['flex', 'flex-col', 'gap-1'],
|
||||
title: [],
|
||||
description: [],
|
||||
icon: ['flex', 'items-start'],
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
info: {
|
||||
wrapper: ['border-border-info-light', 'bg-base-bg-emphasized-info'],
|
||||
title: ['text-elements-on-emphasized-info'],
|
||||
description: ['text-elements-on-emphasized-info'],
|
||||
icon: ['text-elements-info'],
|
||||
},
|
||||
danger: {
|
||||
wrapper: ['border-border-danger-light', 'bg-base-bg-emphasized-danger'],
|
||||
title: ['text-elements-on-emphasized-danger'],
|
||||
description: ['text-elements-on-emphasized-danger'],
|
||||
icon: ['text-elements-danger'],
|
||||
},
|
||||
warning: {
|
||||
wrapper: [
|
||||
'border-border-warning-light',
|
||||
'bg-base-bg-emphasized-warning',
|
||||
],
|
||||
title: ['text-elements-on-emphasized-warning'],
|
||||
description: ['text-elements-on-emphasized-warning'],
|
||||
icon: ['text-elements-warning'],
|
||||
},
|
||||
success: {
|
||||
wrapper: [
|
||||
'border-border-success-light',
|
||||
'bg-base-bg-emphasized-success',
|
||||
],
|
||||
title: ['text-elements-on-emphasized-success'],
|
||||
description: ['text-elements-on-emphasized-success'],
|
||||
icon: ['text-elements-success'],
|
||||
},
|
||||
generic: {
|
||||
wrapper: ['border-border-separator', 'bg-base-bg-emphasized'],
|
||||
title: ['text-elements-high-em'],
|
||||
description: ['text-elements-on-emphasized-info'],
|
||||
icon: ['text-elements-high-em'],
|
||||
},
|
||||
},
|
||||
size: {
|
||||
sm: {
|
||||
wrapper: ['px-2', 'py-2'],
|
||||
title: ['leading-4', 'text-xs'],
|
||||
description: ['leading-4', 'text-xs'],
|
||||
icon: ['h-4', 'w-4'],
|
||||
},
|
||||
md: {
|
||||
wrapper: ['px-3', 'py-3'],
|
||||
title: ['leading-5', 'tracking-[-0.006em]', 'text-sm'],
|
||||
description: ['leading-5', 'tracking-[-0.006em]', 'text-sm'],
|
||||
icon: ['h-5', 'w-5'],
|
||||
},
|
||||
},
|
||||
hasDescription: {
|
||||
true: {
|
||||
title: ['font-medium'],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'generic',
|
||||
size: 'md',
|
||||
},
|
||||
});
|
||||
|
||||
export type InlineNotificationTheme = VariantProps<
|
||||
typeof inlineNotificationTheme
|
||||
>;
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { ReactNode, useCallback } from 'react';
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
import {
|
||||
InlineNotificationTheme,
|
||||
inlineNotificationTheme,
|
||||
} from './InlineNotification.theme';
|
||||
import { InfoSquareIcon } from 'components/shared/CustomIcon';
|
||||
import { cloneIcon } from 'utils/cloneIcon';
|
||||
|
||||
export interface InlineNotificationProps
|
||||
extends ComponentPropsWithoutRef<'div'>,
|
||||
InlineNotificationTheme {
|
||||
/**
|
||||
* The title of the notification
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* The description of the notification
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* The icon to display in the notification
|
||||
* @default <InfoSquareIcon />
|
||||
*/
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A notification that is displayed inline with the content
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <InlineNotification title="Notification title goes here" />
|
||||
* ```
|
||||
*/
|
||||
export const InlineNotification = ({
|
||||
className,
|
||||
title,
|
||||
description,
|
||||
size,
|
||||
variant,
|
||||
icon,
|
||||
...props
|
||||
}: InlineNotificationProps) => {
|
||||
const {
|
||||
wrapper,
|
||||
content,
|
||||
title: titleClass,
|
||||
description: descriptionClass,
|
||||
icon: iconClass,
|
||||
} = inlineNotificationTheme({ size, variant, hasDescription: !!description });
|
||||
|
||||
// Render custom icon or default icon
|
||||
const renderIcon = useCallback(() => {
|
||||
if (!icon) return <InfoSquareIcon className={iconClass()} />;
|
||||
return cloneIcon(icon, { className: iconClass() });
|
||||
}, [icon]);
|
||||
|
||||
return (
|
||||
<div {...props} className={wrapper({ className })}>
|
||||
{renderIcon()}
|
||||
<div className={content()}>
|
||||
<p className={titleClass()}>{title}</p>
|
||||
{description && <p className={descriptionClass()}>{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './InlineNotification';
|
||||
@@ -0,0 +1,85 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const inputTheme = tv(
|
||||
{
|
||||
slots: {
|
||||
container: [
|
||||
'flex',
|
||||
'items-center',
|
||||
'rounded-lg',
|
||||
'relative',
|
||||
'placeholder:text-elements-disabled',
|
||||
'disabled:cursor-not-allowed',
|
||||
'disabled:bg-controls-disabled',
|
||||
],
|
||||
label: ['text-sm', 'text-elements-high-em'],
|
||||
description: ['text-xs', 'text-elements-low-em'],
|
||||
input: [
|
||||
'focus-ring',
|
||||
'block',
|
||||
'w-full',
|
||||
'h-full',
|
||||
'rounded-lg',
|
||||
'text-elements-mid-em',
|
||||
'shadow-sm',
|
||||
'border',
|
||||
'border-border-interactive',
|
||||
'disabled:shadow-none',
|
||||
'disabled:border-none',
|
||||
],
|
||||
icon: ['text-elements-mid-em'],
|
||||
iconContainer: [
|
||||
'absolute',
|
||||
'inset-y-0',
|
||||
'flex',
|
||||
'items-center',
|
||||
'z-10',
|
||||
'cursor-pointer',
|
||||
],
|
||||
helperIcon: [],
|
||||
helperText: ['flex', 'gap-2', 'items-center', 'text-elements-danger'],
|
||||
},
|
||||
variants: {
|
||||
state: {
|
||||
default: {
|
||||
input: '',
|
||||
},
|
||||
error: {
|
||||
input: [
|
||||
'outline',
|
||||
'outline-offset-0',
|
||||
'outline-border-danger',
|
||||
'shadow-none',
|
||||
'focus:outline-border-danger',
|
||||
],
|
||||
helperText: 'text-elements-danger',
|
||||
},
|
||||
},
|
||||
size: {
|
||||
md: {
|
||||
container: 'h-11',
|
||||
input: ['text-sm pl-4 pr-4'],
|
||||
icon: ['h-[18px] w-[18px]'],
|
||||
helperText: 'text-sm',
|
||||
helperIcon: ['h-5 w-5'],
|
||||
},
|
||||
sm: {
|
||||
container: 'h-8',
|
||||
input: ['text-xs pl-3 pr-3'],
|
||||
icon: ['h-4 w-4'],
|
||||
helperText: 'text-xs',
|
||||
helperIcon: ['h-4 w-4'],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
state: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: true,
|
||||
},
|
||||
);
|
||||
|
||||
export type InputTheme = VariantProps<typeof inputTheme>;
|
||||
@@ -0,0 +1,100 @@
|
||||
import React, { ReactNode, useMemo } from 'react';
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
import { InputTheme, inputTheme } from './Input.theme';
|
||||
import { WarningIcon } from 'components/shared/CustomIcon';
|
||||
import { cloneIcon } from 'utils/cloneIcon';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
export interface InputProps
|
||||
extends InputTheme,
|
||||
Omit<ComponentPropsWithoutRef<'input'>, 'size'> {
|
||||
label?: string;
|
||||
description?: string;
|
||||
leftIcon?: ReactNode;
|
||||
rightIcon?: ReactNode;
|
||||
helperText?: string;
|
||||
}
|
||||
|
||||
export const Input = ({
|
||||
className,
|
||||
label,
|
||||
description,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
helperText,
|
||||
size,
|
||||
state,
|
||||
...props
|
||||
}: InputProps) => {
|
||||
const styleProps = (({ size = 'md', state }) => ({
|
||||
size,
|
||||
state,
|
||||
}))({ size, state });
|
||||
|
||||
const {
|
||||
container: containerCls,
|
||||
label: labelCls,
|
||||
description: descriptionCls,
|
||||
input: inputCls,
|
||||
icon: iconCls,
|
||||
iconContainer: iconContainerCls,
|
||||
helperText: helperTextCls,
|
||||
helperIcon: helperIconCls,
|
||||
} = inputTheme({ ...styleProps });
|
||||
|
||||
const renderLabels = useMemo(
|
||||
() => (
|
||||
<div className="space-y-1">
|
||||
<p className={labelCls()}>{label}</p>
|
||||
<p className={descriptionCls()}>{description}</p>
|
||||
</div>
|
||||
),
|
||||
[labelCls, descriptionCls, label, description],
|
||||
);
|
||||
|
||||
const renderLeftIcon = useMemo(() => {
|
||||
return (
|
||||
<div className={iconContainerCls({ class: 'left-0 pl-4' })}>
|
||||
{cloneIcon(leftIcon, { className: iconCls(), 'aria-hidden': true })}
|
||||
</div>
|
||||
);
|
||||
}, [cloneIcon, iconCls, iconContainerCls, leftIcon]);
|
||||
|
||||
const renderRightIcon = useMemo(() => {
|
||||
return (
|
||||
<div className={iconContainerCls({ class: 'pr-4 right-0' })}>
|
||||
{cloneIcon(rightIcon, { className: iconCls(), 'aria-hidden': true })}
|
||||
</div>
|
||||
);
|
||||
}, [cloneIcon, iconCls, iconContainerCls, rightIcon]);
|
||||
|
||||
const renderHelperText = useMemo(
|
||||
() => (
|
||||
<div className={helperTextCls()}>
|
||||
{state &&
|
||||
cloneIcon(<WarningIcon className={helperIconCls()} />, {
|
||||
'aria-hidden': true,
|
||||
})}
|
||||
<p>{helperText}</p>
|
||||
</div>
|
||||
),
|
||||
[cloneIcon, state, helperIconCls, helperText, helperTextCls],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{renderLabels}
|
||||
<div className={containerCls({ class: className })}>
|
||||
{leftIcon && renderLeftIcon}
|
||||
<input
|
||||
className={cn(inputCls({ class: 'w-80' }), {
|
||||
'pl-10': leftIcon,
|
||||
})}
|
||||
{...props}
|
||||
/>
|
||||
{rightIcon && renderRightIcon}
|
||||
</div>
|
||||
{renderHelperText}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Input';
|
||||
export * from './Input.theme';
|
||||
@@ -0,0 +1,54 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const radioTheme = tv({
|
||||
slots: {
|
||||
root: ['flex', 'gap-3', 'flex-wrap'],
|
||||
wrapper: ['flex', 'items-center', 'gap-2', 'group'],
|
||||
label: ['text-sm', 'tracking-[-0.006em]', 'text-elements-high-em'],
|
||||
radio: [
|
||||
'w-5',
|
||||
'h-5',
|
||||
'rounded-full',
|
||||
'border',
|
||||
'group',
|
||||
'border-border-interactive/10',
|
||||
'shadow-button',
|
||||
'group-hover:border-border-interactive/[0.14]',
|
||||
'focus-ring',
|
||||
// Checked
|
||||
'data-[state=checked]:bg-controls-primary',
|
||||
'data-[state=checked]:group-hover:bg-controls-primary-hovered',
|
||||
],
|
||||
indicator: [
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'relative',
|
||||
'w-full',
|
||||
'h-full',
|
||||
'after:content-[""]',
|
||||
'after:block',
|
||||
'after:w-2.5',
|
||||
'after:h-2.5',
|
||||
'after:rounded-full',
|
||||
'after:bg-transparent',
|
||||
'after:group-hover:bg-controls-disabled',
|
||||
'after:group-focus-visible:bg-controls-disabled',
|
||||
// Checked
|
||||
'after:data-[state=checked]:bg-elements-on-primary',
|
||||
'after:data-[state=checked]:group-hover:bg-elements-on-primary',
|
||||
'after:data-[state=checked]:group-focus-visible:bg-elements-on-primary',
|
||||
],
|
||||
},
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: { root: ['flex-col'] },
|
||||
horizontal: { root: ['flex-row'] },
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: 'vertical',
|
||||
},
|
||||
});
|
||||
|
||||
export type RadioTheme = VariantProps<typeof radioTheme>;
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Root as RadixRoot,
|
||||
RadioGroupProps,
|
||||
} from '@radix-ui/react-radio-group';
|
||||
import { RadioTheme, radioTheme } from './Radio.theme';
|
||||
import { RadioItem, RadioItemProps } from './RadioItem';
|
||||
|
||||
export interface RadioOption extends RadioItemProps {
|
||||
/**
|
||||
* The label of the radio option.
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* The value of the radio option.
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface RadioProps extends RadioGroupProps, RadioTheme {
|
||||
/**
|
||||
* The options of the radio.
|
||||
* @default []
|
||||
* @example
|
||||
* ```tsx
|
||||
* const options = [
|
||||
* {
|
||||
* label: 'Label 1',
|
||||
* value: '1',
|
||||
* },
|
||||
* {
|
||||
* label: 'Label 2',
|
||||
* value: '2',
|
||||
* },
|
||||
* {
|
||||
* label: 'Label 3',
|
||||
* value: '3',
|
||||
* },
|
||||
* ];
|
||||
* ```
|
||||
*/
|
||||
options: RadioOption[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The Radio component is used to select one option from a list of options.
|
||||
*/
|
||||
export const Radio = ({
|
||||
className,
|
||||
options,
|
||||
orientation,
|
||||
...props
|
||||
}: RadioProps) => {
|
||||
const { root } = radioTheme({ orientation });
|
||||
|
||||
return (
|
||||
<RadixRoot {...props} className={root({ className })}>
|
||||
{options.map((option) => (
|
||||
<RadioItem key={option.value} {...option} />
|
||||
))}
|
||||
</RadixRoot>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
import {
|
||||
Item as RadixRadio,
|
||||
Indicator as RadixIndicator,
|
||||
RadioGroupItemProps,
|
||||
RadioGroupIndicatorProps,
|
||||
} from '@radix-ui/react-radio-group';
|
||||
import { radioTheme } from './Radio.theme';
|
||||
|
||||
export interface RadioItemProps extends RadioGroupItemProps {
|
||||
/**
|
||||
* The wrapper props of the radio item.
|
||||
* You can use this prop to customize the wrapper props.
|
||||
*/
|
||||
wrapperProps?: ComponentPropsWithoutRef<'div'>;
|
||||
/**
|
||||
* The label props of the radio item.
|
||||
* You can use this prop to customize the label props.
|
||||
*/
|
||||
labelProps?: ComponentPropsWithoutRef<'label'>;
|
||||
/**
|
||||
* The indicator props of the radio item.
|
||||
* You can use this prop to customize the indicator props.
|
||||
*/
|
||||
indicatorProps?: RadioGroupIndicatorProps;
|
||||
/**
|
||||
* The id of the radio item.
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* The label of the radio item.
|
||||
*/
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The RadioItem component is used to render a radio item.
|
||||
*/
|
||||
export const RadioItem = ({
|
||||
className,
|
||||
wrapperProps,
|
||||
labelProps,
|
||||
indicatorProps,
|
||||
label,
|
||||
id,
|
||||
...props
|
||||
}: RadioItemProps) => {
|
||||
const { wrapper, label: labelClass, radio, indicator } = radioTheme();
|
||||
|
||||
// Generate a unique id for the radio item from the label if the id is not provided
|
||||
const kebabCaseLabel = label?.toLowerCase().replace(/\s+/g, '-');
|
||||
const componentId = id ?? kebabCaseLabel;
|
||||
|
||||
return (
|
||||
<div className={wrapper({ className: wrapperProps?.className })}>
|
||||
<RadixRadio {...props} className={radio({ className })} id={componentId}>
|
||||
<RadixIndicator
|
||||
forceMount
|
||||
{...indicatorProps}
|
||||
className={indicator({ className: indicatorProps?.className })}
|
||||
/>
|
||||
</RadixRadio>
|
||||
{label && (
|
||||
<label
|
||||
{...labelProps}
|
||||
className={labelClass({ className: labelProps?.className })}
|
||||
htmlFor={componentId}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Radio';
|
||||
export * from './RadioItem';
|
||||
@@ -0,0 +1,77 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
type ComponentPropsWithoutRef,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
segmentedControlsTheme,
|
||||
type SegmentedControlsVariants,
|
||||
} from './SegmentedControls.theme';
|
||||
import { cloneIcon } from 'utils/cloneIcon';
|
||||
|
||||
/**
|
||||
* Interface for the props of a segmented control item component.
|
||||
*/
|
||||
export interface SegmentedControlItemProps
|
||||
extends Omit<ComponentPropsWithoutRef<'button'>, 'type' | 'children'>,
|
||||
SegmentedControlsVariants {
|
||||
/**
|
||||
* The optional left icon element for a component.
|
||||
*/
|
||||
leftIcon?: ReactNode;
|
||||
/**
|
||||
* The optional right icon element to display.
|
||||
*/
|
||||
rightIcon?: ReactNode;
|
||||
/**
|
||||
* Indicates whether the item is active or not.
|
||||
*/
|
||||
active?: boolean;
|
||||
/**
|
||||
* Optional prop that represents the children of a React component.
|
||||
*/
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A functional component that represents an item in a segmented control.
|
||||
* @returns The rendered segmented control item.
|
||||
*/
|
||||
const SegmentedControlItem = forwardRef<
|
||||
HTMLButtonElement,
|
||||
SegmentedControlItemProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
size,
|
||||
type,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
active = false,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { item, icon } = segmentedControlsTheme({ size, type });
|
||||
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={item({ className })}
|
||||
data-active={active}
|
||||
>
|
||||
{leftIcon && cloneIcon(leftIcon, { className: icon({ size }) })}
|
||||
{children}
|
||||
{rightIcon && cloneIcon(rightIcon, { className: icon({ size }) })}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SegmentedControlItem.displayName = 'SegmentedControlItem';
|
||||
|
||||
export { SegmentedControlItem };
|
||||
@@ -0,0 +1,76 @@
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
/**
|
||||
* Defines the theme for a segmented controls.
|
||||
*/
|
||||
export const segmentedControlsTheme = tv({
|
||||
slots: {
|
||||
parent: [
|
||||
'flex',
|
||||
'items-center',
|
||||
'bg-base-bg-emphasized',
|
||||
'gap-0.5',
|
||||
'rounded-lg',
|
||||
],
|
||||
item: [
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'gap-2',
|
||||
'text-elements-mid-em',
|
||||
'bg-transparent',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'cursor-default',
|
||||
'whitespace-nowrap',
|
||||
'rounded-lg',
|
||||
'focus-ring',
|
||||
'hover:bg-controls-tertiary-hovered',
|
||||
'focus-visible:z-20',
|
||||
'focus-visible:bg-controls-tertiary-hovered',
|
||||
'disabled:text-controls-disabled',
|
||||
'disabled:bg-transparent',
|
||||
'disabled:cursor-not-allowed',
|
||||
'disabled:border-transparent',
|
||||
'data-[active=true]:bg-controls-tertiary',
|
||||
'data-[active=true]:text-elements-high-em',
|
||||
'data-[active=true]:border-border-interactive/10',
|
||||
'data-[active=true]:shadow-field',
|
||||
'data-[active=true]:hover:bg-controls-tertiary-hovered',
|
||||
],
|
||||
icon: [],
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
sm: {
|
||||
item: ['px-3', 'py-2', 'text-xs'],
|
||||
icon: ['h-4', 'w-4'],
|
||||
},
|
||||
md: {
|
||||
item: ['px-4', 'py-3', 'text-sm', 'tracking-[-0.006em]'],
|
||||
icon: ['h-5', 'w-5'],
|
||||
},
|
||||
},
|
||||
type: {
|
||||
'fixed-width': {
|
||||
parent: ['w-fit'],
|
||||
item: ['w-fit'],
|
||||
},
|
||||
'full-width': {
|
||||
parent: ['w-full'],
|
||||
item: ['w-full'],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
type: 'fixed-width',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Defines the type for the variants of a segmented controls.
|
||||
*/
|
||||
export type SegmentedControlsVariants = VariantProps<
|
||||
typeof segmentedControlsTheme
|
||||
>;
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
type ComponentPropsWithoutRef,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
SegmentedControlItem,
|
||||
type SegmentedControlItemProps,
|
||||
} from './SegmentedControlItem';
|
||||
import {
|
||||
segmentedControlsTheme,
|
||||
type SegmentedControlsVariants,
|
||||
} from './SegmentedControls.theme';
|
||||
|
||||
/**
|
||||
* Represents an option for a segmented control.
|
||||
*/
|
||||
export interface SegmentedControlsOption
|
||||
extends Omit<SegmentedControlItemProps, 'children'> {
|
||||
/**
|
||||
* The label of the item.
|
||||
*/
|
||||
label: ReactNode;
|
||||
/**
|
||||
* The value of the item.
|
||||
*
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the props for the SegmentedControls component.
|
||||
*/
|
||||
export interface SegmentedControlsProps<T extends string = string>
|
||||
extends Omit<ComponentPropsWithoutRef<'div'>, 'onChange'>,
|
||||
SegmentedControlsVariants {
|
||||
/**
|
||||
* An array of options for a segmented control component.
|
||||
*/
|
||||
options: SegmentedControlsOption[];
|
||||
/**
|
||||
* An optional string value.
|
||||
*/
|
||||
value?: T;
|
||||
/**
|
||||
* Optional callback function to handle changes in state.
|
||||
*/
|
||||
onChange?: (v: T) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that renders segmented controls with customizable options.
|
||||
*/
|
||||
export function SegmentedControls<T extends string = string>({
|
||||
className,
|
||||
options,
|
||||
value,
|
||||
type,
|
||||
size,
|
||||
onChange,
|
||||
...props
|
||||
}: SegmentedControlsProps<T>) {
|
||||
const { parent } = segmentedControlsTheme({ size, type });
|
||||
|
||||
/**
|
||||
* Handles the change event for a given option.
|
||||
*/
|
||||
const handleChange = useCallback(
|
||||
(option: T) => {
|
||||
if (!option) return;
|
||||
onChange?.(option);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div {...props} className={parent({ className })}>
|
||||
{options.map((option, index) => (
|
||||
<SegmentedControlItem
|
||||
key={index}
|
||||
active={value === option.value}
|
||||
size={size}
|
||||
type={type}
|
||||
onClick={() => handleChange(option.value as T)}
|
||||
{...option}
|
||||
>
|
||||
{option.label}
|
||||
</SegmentedControlItem>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './SegmentedControlItem';
|
||||
export * from './SegmentedControls';
|
||||
@@ -0,0 +1,84 @@
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const switchTheme = tv({
|
||||
slots: {
|
||||
wrapper: ['flex', 'items-start', 'gap-4', 'w-[375px]'],
|
||||
switch: [
|
||||
'h-6',
|
||||
'w-12',
|
||||
'rounded-full',
|
||||
'transition-all',
|
||||
'duration-500',
|
||||
'relative',
|
||||
'cursor-default',
|
||||
'shadow-inset',
|
||||
'focus-ring',
|
||||
'outline-none',
|
||||
],
|
||||
thumb: [
|
||||
'block',
|
||||
'h-4',
|
||||
'w-4',
|
||||
'translate-x-1',
|
||||
'transition-transform',
|
||||
'duration-100',
|
||||
'will-change-transform',
|
||||
'rounded-full',
|
||||
'shadow-button',
|
||||
'data-[state=checked]:translate-x-7',
|
||||
'bg-controls-elevated',
|
||||
],
|
||||
label: [
|
||||
'flex',
|
||||
'flex-1',
|
||||
'flex-col',
|
||||
'px-1',
|
||||
'gap-1',
|
||||
'text-sm',
|
||||
'text-elements-high-em',
|
||||
'tracking-[-0.006em]',
|
||||
],
|
||||
description: ['text-xs', 'text-elements-low-em'],
|
||||
},
|
||||
variants: {
|
||||
checked: {
|
||||
true: {
|
||||
switch: [
|
||||
'bg-controls-primary',
|
||||
'hover:bg-controls-primary-hovered',
|
||||
'focus-visible:bg-controls-primary-hovered',
|
||||
],
|
||||
},
|
||||
false: {
|
||||
switch: [
|
||||
'bg-controls-inset',
|
||||
'hover:bg-controls-inset-hovered',
|
||||
'focus-visible:bg-controls-inset-hovered',
|
||||
],
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
true: {
|
||||
switch: ['bg-controls-disabled', 'cursor-not-allowed'],
|
||||
thumb: ['bg-elements-on-disabled'],
|
||||
},
|
||||
},
|
||||
fullWidth: {
|
||||
true: {
|
||||
wrapper: ['w-full', 'justify-between'],
|
||||
},
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
checked: true,
|
||||
disabled: true,
|
||||
class: {
|
||||
switch: ['bg-controls-disabled-active'],
|
||||
thumb: ['bg-snowball-900'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export type SwitchVariants = VariantProps<typeof switchTheme>;
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { type ComponentPropsWithoutRef } from 'react';
|
||||
import { type SwitchProps as SwitchRadixProps } from '@radix-ui/react-switch';
|
||||
import * as SwitchRadix from '@radix-ui/react-switch';
|
||||
|
||||
import { switchTheme, type SwitchVariants } from './Switch.theme';
|
||||
|
||||
interface SwitchProps
|
||||
extends Omit<SwitchRadixProps, 'checked'>,
|
||||
SwitchVariants {
|
||||
/**
|
||||
* The label of the switch.
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* The description of the switch.
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* Custom wrapper props for the switch.
|
||||
*/
|
||||
wrapperProps?: ComponentPropsWithoutRef<'div'>;
|
||||
/**
|
||||
* Function that is called when the checked state of the switch changes.
|
||||
* @param checked The new checked state of the switch.
|
||||
*/
|
||||
onCheckedChange?(checked: boolean): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A switch is a component used for toggling between two states.
|
||||
*/
|
||||
export const Switch = ({
|
||||
className,
|
||||
checked,
|
||||
label,
|
||||
description,
|
||||
disabled,
|
||||
name,
|
||||
wrapperProps,
|
||||
fullWidth,
|
||||
...props
|
||||
}: SwitchProps) => {
|
||||
const {
|
||||
wrapper,
|
||||
switch: switchClass,
|
||||
thumb,
|
||||
label: labelClass,
|
||||
description: descriptionClass,
|
||||
} = switchTheme({
|
||||
checked,
|
||||
disabled,
|
||||
fullWidth,
|
||||
});
|
||||
|
||||
const switchComponent = (
|
||||
<SwitchRadix.Root
|
||||
{...props}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
className={switchClass({ className })}
|
||||
>
|
||||
<SwitchRadix.Thumb className={thumb()} />
|
||||
</SwitchRadix.Root>
|
||||
);
|
||||
|
||||
// If a label is provided, wrap the switch in a label element.
|
||||
if (label) {
|
||||
return (
|
||||
<div
|
||||
{...wrapperProps}
|
||||
className={wrapper({ className: wrapperProps?.className })}
|
||||
>
|
||||
<label className={labelClass()} htmlFor={name}>
|
||||
{label}
|
||||
{description && (
|
||||
<span className={descriptionClass()}>{description}</span>
|
||||
)}
|
||||
</label>
|
||||
{switchComponent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return switchComponent;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Switch';
|
||||
@@ -0,0 +1,82 @@
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export type TabsVariants = VariantProps<typeof tabsTheme>;
|
||||
|
||||
export const tabsTheme = tv({
|
||||
slots: {
|
||||
root: ['flex', 'data-[orientation=horizontal]:w-full'],
|
||||
triggerWrapper: [
|
||||
// Horizontal – default
|
||||
'px-1',
|
||||
'pb-5',
|
||||
'cursor-default',
|
||||
'select-none',
|
||||
'text-elements-low-em',
|
||||
'border-b-2',
|
||||
'border-transparent',
|
||||
'hover:border-border-interactive/10',
|
||||
'hover:text-elements-mid-em',
|
||||
'focus-within:border-border-interactive/10',
|
||||
'data-[state=active]:font-medium',
|
||||
'data-[state=active]:text-elements-high-em',
|
||||
'data-[state=active]:border-elements-high-em',
|
||||
// Vertical
|
||||
'data-[orientation=vertical]:px-3',
|
||||
'data-[orientation=vertical]:py-3',
|
||||
'data-[orientation=vertical]:min-w-[240px]',
|
||||
'data-[orientation=vertical]:focus-ring',
|
||||
'data-[orientation=vertical]:rounded-xl',
|
||||
'data-[orientation=vertical]:border-transparent',
|
||||
'data-[orientation=vertical]:hover:bg-base-bg-emphasized',
|
||||
'data-[orientation=vertical]:hover:text-elements-mid-em',
|
||||
'data-[orientation=vertical]:hover:border-transparent',
|
||||
'data-[orientation=vertical]:focus-visible:border-transparent',
|
||||
'data-[orientation=vertical]:focus-visible:bg-base-bg-emphasized',
|
||||
'data-[orientation=vertical]:focus-visible:text-elements-mid-em',
|
||||
'data-[orientation=vertical]:data-[state=active]:font-normal',
|
||||
'data-[orientation=vertical]:data-[state=active]:bg-base-bg-emphasized',
|
||||
'data-[orientation=vertical]:data-[state=active]:border-transparent',
|
||||
'data-[orientation=vertical]:data-[state=active]:hover:text-elements-high-em',
|
||||
'data-[orientation=vertical]:data-[state=active]:focus-visible:text-elements-high-em',
|
||||
],
|
||||
trigger: [
|
||||
'flex',
|
||||
'gap-1.5',
|
||||
'cursor-default',
|
||||
'select-none',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'outline-none',
|
||||
'leading-none',
|
||||
'tracking-[-0.006em]',
|
||||
'rounded-md',
|
||||
// Horizontal – default
|
||||
'data-[orientation=horizontal]:focus-ring',
|
||||
// Vertical
|
||||
'data-[orientation=vertical]:gap-2',
|
||||
],
|
||||
triggerList: [
|
||||
'flex',
|
||||
'shrink-0',
|
||||
'gap-5',
|
||||
'border-b',
|
||||
'border-transparent',
|
||||
// Horizontal – default
|
||||
'data-[orientation=horizontal]:border-border-interactive/10',
|
||||
// Vertical
|
||||
'data-[orientation=vertical]:flex-col',
|
||||
'data-[orientation=vertical]:gap-0.5',
|
||||
],
|
||||
content: ['text-elements-high-em', 'grow', 'outline-none', 'tab-content'],
|
||||
},
|
||||
variants: {
|
||||
fillWidth: {
|
||||
true: {
|
||||
trigger: ['flex-1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
fillWidth: false,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { type ComponentPropsWithoutRef } from 'react';
|
||||
import { Root as TabsRoot } from '@radix-ui/react-tabs';
|
||||
|
||||
import { tabsTheme } from './Tabs.theme';
|
||||
import TabsContent from './TabsContent';
|
||||
import TabsList from './TabsList';
|
||||
import TabsTrigger from './TabsTrigger';
|
||||
import TabsProvider, { TabsProviderProps } from './TabsProvider';
|
||||
|
||||
export interface TabsProps extends ComponentPropsWithoutRef<typeof TabsRoot> {
|
||||
/**
|
||||
* The configuration for the tabs component.
|
||||
*/
|
||||
config?: TabsProviderProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that allows users to switch between different tabs.
|
||||
* @returns JSX element representing the tabs component.
|
||||
*/
|
||||
export const Tabs = ({
|
||||
config,
|
||||
className,
|
||||
orientation = 'horizontal',
|
||||
...props
|
||||
}: TabsProps) => {
|
||||
const { root } = tabsTheme(config);
|
||||
|
||||
return (
|
||||
<TabsProvider {...config} orientation={orientation}>
|
||||
<TabsRoot
|
||||
{...props}
|
||||
orientation={orientation}
|
||||
className={root({ className })}
|
||||
/>
|
||||
</TabsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assigns the TabsTrigger class to the Trigger property of the Tabs object.
|
||||
*/
|
||||
Tabs.Trigger = TabsTrigger;
|
||||
/**
|
||||
* Assigns the TabsList object to the List property of the Tabs object.
|
||||
*/
|
||||
Tabs.List = TabsList;
|
||||
/**
|
||||
* Assigns the TabsContent component to the Content property of the Tabs component.
|
||||
*/
|
||||
Tabs.Content = TabsContent;
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
type ComponentPropsWithoutRef,
|
||||
type ElementRef,
|
||||
} from 'react';
|
||||
import { Content } from '@radix-ui/react-tabs';
|
||||
|
||||
import { tabsTheme } from '../Tabs.theme';
|
||||
|
||||
export interface TabsContentProps
|
||||
extends ComponentPropsWithoutRef<typeof Content> {}
|
||||
|
||||
/**
|
||||
* A component that represents the content of the tabs component.
|
||||
*/
|
||||
const TabsContent = forwardRef<ElementRef<typeof Content>, TabsContentProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
const { content } = tabsTheme();
|
||||
return <Content ref={ref} className={content({ className })} {...props} />;
|
||||
},
|
||||
);
|
||||
|
||||
// Assigns the display name to the TabsContent component.
|
||||
TabsContent.displayName = 'TabsContent';
|
||||
|
||||
export { TabsContent };
|
||||
@@ -0,0 +1,3 @@
|
||||
import { TabsContent } from './TabsContent';
|
||||
|
||||
export default TabsContent;
|
||||
@@ -0,0 +1,25 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
type ComponentPropsWithoutRef,
|
||||
type ElementRef,
|
||||
} from 'react';
|
||||
import { List } from '@radix-ui/react-tabs';
|
||||
|
||||
import { tabsTheme } from 'components/shared/Tabs/Tabs.theme';
|
||||
|
||||
export interface TabsListProps extends ComponentPropsWithoutRef<typeof List> {}
|
||||
|
||||
/**
|
||||
* A component that represents the list of tabs.
|
||||
*/
|
||||
const TabsList = forwardRef<ElementRef<typeof List>, TabsListProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
const { triggerList } = tabsTheme({ className });
|
||||
return <List ref={ref} className={triggerList({ className })} {...props} />;
|
||||
},
|
||||
);
|
||||
|
||||
// Assigns the display name to the TabsList component.
|
||||
TabsList.displayName = 'TabsList';
|
||||
|
||||
export { TabsList };
|
||||
@@ -0,0 +1,3 @@
|
||||
import { TabsList } from './TabsList';
|
||||
|
||||
export default TabsList;
|
||||
@@ -0,0 +1,47 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
type PropsWithChildren,
|
||||
ComponentPropsWithoutRef,
|
||||
} from 'react';
|
||||
import { TabsVariants } from './Tabs.theme';
|
||||
import { Root as TabsRoot } from '@radix-ui/react-tabs';
|
||||
|
||||
export interface TabsProviderProps
|
||||
extends Partial<TabsVariants>,
|
||||
ComponentPropsWithoutRef<typeof TabsRoot> {}
|
||||
|
||||
type TabsProviderContext = ReturnType<typeof useTabsValues>;
|
||||
|
||||
const TabsContext = createContext<Partial<TabsProviderContext>>({});
|
||||
|
||||
// For inferring return type
|
||||
const useTabsValues = (props: TabsProviderProps) => {
|
||||
return props;
|
||||
};
|
||||
|
||||
/**
|
||||
* A provider component that allows users to switch between different tabs.
|
||||
* @returns JSX element representing the tabs provider component.
|
||||
*/
|
||||
export const TabsProvider = ({
|
||||
children,
|
||||
...props
|
||||
}: PropsWithChildren<TabsProviderProps>): JSX.Element => {
|
||||
const values = useTabsValues(props);
|
||||
return <TabsContext.Provider value={values}>{children}</TabsContext.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that returns the context of the tabs provider.
|
||||
* @returns The context of the tabs provider.
|
||||
*/
|
||||
export const useTabs = () => {
|
||||
const context = useContext(TabsContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTabs was used outside of its Provider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export default TabsProvider;
|
||||
@@ -0,0 +1,59 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
type ComponentPropsWithoutRef,
|
||||
type ElementRef,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { Trigger } from '@radix-ui/react-tabs';
|
||||
|
||||
import { tabsTheme } from 'components/shared/Tabs/Tabs.theme';
|
||||
import { useTabs } from 'components/shared/Tabs/TabsProvider';
|
||||
|
||||
export interface TabsTriggerProps
|
||||
extends ComponentPropsWithoutRef<typeof Trigger> {
|
||||
/**
|
||||
* The icon to display in the trigger.
|
||||
*/
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that represents the trigger for the tabs component.
|
||||
*/
|
||||
const TabsTrigger = forwardRef<
|
||||
ElementRef<typeof Trigger>,
|
||||
PropsWithChildren<TabsTriggerProps>
|
||||
>(({ className, icon, children, ...props }, ref) => {
|
||||
const config = useTabs();
|
||||
const { triggerWrapper, trigger } = tabsTheme(config);
|
||||
const orientation = config.orientation;
|
||||
|
||||
return (
|
||||
<Trigger
|
||||
ref={ref}
|
||||
// Disabled focus state for horizontal tabs
|
||||
tabIndex={orientation === 'horizontal' ? -1 : undefined}
|
||||
className={triggerWrapper({ className })}
|
||||
{...props}
|
||||
>
|
||||
{/* Need to add button in the trigger children because there's focus state inside the children */}
|
||||
<button
|
||||
data-orientation={orientation}
|
||||
// Disabled focus state for vertical tabs
|
||||
tabIndex={orientation === 'vertical' ? -1 : undefined}
|
||||
className={trigger()}
|
||||
>
|
||||
{/* Put the icon on the left of the text for veritcal tab */}
|
||||
{orientation === 'vertical' && icon}
|
||||
{children}
|
||||
{/* Put the icon on the right of the text for horizontal tab */}
|
||||
{orientation === 'horizontal' && icon}
|
||||
</button>
|
||||
</Trigger>
|
||||
);
|
||||
});
|
||||
|
||||
TabsTrigger.displayName = 'TabsTrigger';
|
||||
|
||||
export { TabsTrigger };
|
||||
@@ -0,0 +1,3 @@
|
||||
import { TabsTrigger } from './TabsTrigger';
|
||||
|
||||
export default TabsTrigger;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Tabs';
|
||||
@@ -0,0 +1,93 @@
|
||||
import { tv } from 'tailwind-variants';
|
||||
import type { VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const tagTheme = tv(
|
||||
{
|
||||
slots: {
|
||||
wrapper: ['flex', 'gap-1.5', 'rounded-lg', 'border'],
|
||||
icon: ['h-4', 'w-4'],
|
||||
label: ['font-inter', 'text-xs'],
|
||||
},
|
||||
variants: {
|
||||
type: {
|
||||
attention: {
|
||||
icon: ['text-elements-warning'],
|
||||
},
|
||||
negative: {
|
||||
icon: ['text-elements-danger'],
|
||||
},
|
||||
positive: {
|
||||
icon: ['text-elements-success'],
|
||||
},
|
||||
emphasized: {
|
||||
icon: ['text-elements-on-secondary'],
|
||||
},
|
||||
neutral: {
|
||||
icon: ['text-elements-mid-em'],
|
||||
},
|
||||
},
|
||||
style: {
|
||||
default: {},
|
||||
minimal: {
|
||||
wrapper: ['border-border-interactive', 'bg-controls-tertiary'],
|
||||
label: ['text-elements-high-em'],
|
||||
},
|
||||
},
|
||||
size: {
|
||||
sm: {
|
||||
wrapper: ['px-2', 'py-2'],
|
||||
},
|
||||
xs: {
|
||||
wrapper: ['px-2', 'py-1.5'],
|
||||
},
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
type: 'attention',
|
||||
style: 'default',
|
||||
class: {
|
||||
wrapper: ['border-orange-200', 'bg-orange-50'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'negative',
|
||||
style: 'default',
|
||||
class: {
|
||||
wrapper: ['border-rose-200', 'bg-rose-50'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'positive',
|
||||
style: 'default',
|
||||
class: {
|
||||
wrapper: ['border-emerald-200', 'bg-emerald-50'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'emphasized',
|
||||
style: 'default',
|
||||
class: {
|
||||
wrapper: ['border-snowball-200', 'bg-snowball-50'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'neutral',
|
||||
style: 'default',
|
||||
class: {
|
||||
wrapper: ['border-gray-200', 'bg-gray-50'],
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
type: 'attention',
|
||||
style: 'default',
|
||||
size: 'sm',
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: true,
|
||||
},
|
||||
);
|
||||
|
||||
export type TagTheme = VariantProps<typeof tagTheme>;
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, {
|
||||
type ReactNode,
|
||||
type ComponentPropsWithoutRef,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { tagTheme, type TagTheme } from './Tag.theme';
|
||||
import { cloneIcon } from 'utils/cloneIcon';
|
||||
|
||||
type TagProps = ComponentPropsWithoutRef<'div'> &
|
||||
TagTheme & {
|
||||
/**
|
||||
* The optional left icon element for a component.
|
||||
* @type {ReactNode}
|
||||
*/
|
||||
leftIcon?: ReactNode;
|
||||
/**
|
||||
* The optional right icon element to display.
|
||||
* @type {ReactNode}
|
||||
*/
|
||||
rightIcon?: ReactNode;
|
||||
};
|
||||
|
||||
export const Tag = ({
|
||||
children,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
type = 'attention',
|
||||
style = 'default',
|
||||
size = 'sm',
|
||||
}: TagProps) => {
|
||||
const {
|
||||
wrapper: wrapperCls,
|
||||
icon: iconCls,
|
||||
label: labelCls,
|
||||
} = tagTheme({
|
||||
type,
|
||||
style,
|
||||
size,
|
||||
});
|
||||
|
||||
const renderLeftIcon = useMemo(() => {
|
||||
if (!leftIcon) return null;
|
||||
return <div className={iconCls()}>{cloneIcon(leftIcon, { size: 16 })}</div>;
|
||||
}, [cloneIcon, iconCls, leftIcon]);
|
||||
|
||||
const renderRightIcon = useMemo(() => {
|
||||
if (!rightIcon) return null;
|
||||
return (
|
||||
<div className={iconCls()}>{cloneIcon(rightIcon, { size: 16 })}</div>
|
||||
);
|
||||
}, [cloneIcon, iconCls, rightIcon]);
|
||||
|
||||
return (
|
||||
<div className={wrapperCls()}>
|
||||
{renderLeftIcon}
|
||||
<p className={labelCls()}>{children}</p>
|
||||
{renderRightIcon}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Tag';
|
||||
export * from './Tag.theme';
|
||||
@@ -0,0 +1,58 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const simpleToastTheme = tv(
|
||||
{
|
||||
slots: {
|
||||
wrapper: [
|
||||
'flex',
|
||||
'items-center',
|
||||
'py-2',
|
||||
'pl-2',
|
||||
'pr-1.5',
|
||||
'gap-2',
|
||||
'rounded-full',
|
||||
'mx-auto',
|
||||
'mt-3',
|
||||
'w-fit',
|
||||
'overflow-hidden',
|
||||
'bg-surface-high-contrast',
|
||||
'shadow-sm',
|
||||
],
|
||||
icon: ['flex', 'items-center', 'justify-center', 'w-5', 'h-5'],
|
||||
closeIcon: [
|
||||
'cursor-pointer',
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'w-6',
|
||||
'h-6',
|
||||
'text-elements-on-high-contrast',
|
||||
],
|
||||
title: ['text-sm', 'text-elements-on-high-contrast'],
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
success: {
|
||||
icon: ['text-elements-success'],
|
||||
},
|
||||
error: {
|
||||
icon: ['text-elements-danger'],
|
||||
},
|
||||
warning: {
|
||||
icon: ['text-elements-warning'],
|
||||
},
|
||||
info: {
|
||||
icon: ['text-elements-info'],
|
||||
},
|
||||
loading: {
|
||||
icon: ['text-elements-info'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: true,
|
||||
},
|
||||
);
|
||||
|
||||
export type SimpleToastTheme = VariantProps<typeof simpleToastTheme>;
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import * as ToastPrimitive from '@radix-ui/react-toast';
|
||||
import { ToastProps } from '@radix-ui/react-toast';
|
||||
import { motion } from 'framer-motion';
|
||||
import { simpleToastTheme, type SimpleToastTheme } from './SimpleToast.theme';
|
||||
import {
|
||||
LoadingIcon,
|
||||
CheckRoundFilledIcon,
|
||||
CrossIcon,
|
||||
InfoRoundFilledIcon,
|
||||
WarningIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Button, type ButtonOrLinkProps } from 'components/shared/Button';
|
||||
import { cloneIcon } from 'utils/cloneIcon';
|
||||
|
||||
type CtaProps = ButtonOrLinkProps & {
|
||||
buttonLabel: string;
|
||||
};
|
||||
export interface SimpleToastProps extends ToastProps {
|
||||
id: string;
|
||||
title: string;
|
||||
variant?: SimpleToastTheme['variant'];
|
||||
cta?: CtaProps[];
|
||||
onDismiss: (toastId: string) => void;
|
||||
}
|
||||
|
||||
export const SimpleToast = ({
|
||||
id,
|
||||
className,
|
||||
title,
|
||||
variant = 'success',
|
||||
cta = [],
|
||||
onDismiss,
|
||||
...props
|
||||
}: SimpleToastProps) => {
|
||||
const hasCta = cta.length > 0;
|
||||
const {
|
||||
wrapper: wrapperCls,
|
||||
icon: iconCls,
|
||||
closeIcon: closeIconCls,
|
||||
title: titleCls,
|
||||
} = simpleToastTheme({ variant });
|
||||
|
||||
const Icon = useMemo(() => {
|
||||
if (variant === 'success') return <CheckRoundFilledIcon />;
|
||||
if (variant === 'error') return <WarningIcon />;
|
||||
if (variant === 'warning') return <WarningIcon />;
|
||||
if (variant === 'info') return <InfoRoundFilledIcon />;
|
||||
return <LoadingIcon />; // variant === 'loading'
|
||||
}, [variant]);
|
||||
|
||||
const renderCta = useMemo(() => {
|
||||
if (!hasCta) return null;
|
||||
return (
|
||||
<div className="flex gap-1.5 ml-2">
|
||||
{cta.map(({ buttonLabel, ...props }, index) => (
|
||||
<Button key={index} {...props}>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}, [cta]);
|
||||
|
||||
const renderCloseButton = useMemo(
|
||||
() => (
|
||||
<div onClick={() => onDismiss(id)} className={closeIconCls()}>
|
||||
<CrossIcon className="h-3 w-3" />
|
||||
</div>
|
||||
),
|
||||
[id],
|
||||
);
|
||||
|
||||
return (
|
||||
<ToastPrimitive.Root {...props} asChild>
|
||||
<motion.li
|
||||
animate={{
|
||||
y: 'var(--radix-toast-swipe-move-y, 0)',
|
||||
opacity: 1,
|
||||
}}
|
||||
className={wrapperCls({ class: className })}
|
||||
exit={{ y: '100%', opacity: 0 }}
|
||||
initial={{ y: '100%', opacity: 0 }}
|
||||
>
|
||||
{cloneIcon(Icon, { className: iconCls() })}
|
||||
<ToastPrimitive.Title asChild>
|
||||
<p className={titleCls()}>{title}</p>
|
||||
</ToastPrimitive.Title>
|
||||
{renderCta}
|
||||
{renderCloseButton}
|
||||
</motion.li>
|
||||
</ToastPrimitive.Root>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Provider,
|
||||
Viewport,
|
||||
type ToastProviderProps,
|
||||
} from '@radix-ui/react-toast';
|
||||
|
||||
export const ToastProvider = ({ children, ...props }: ToastProviderProps) => {
|
||||
return (
|
||||
<Provider {...props}>
|
||||
{children}
|
||||
<Viewport className="fixed inset-x-0 bottom-0 px-4 py-10" />
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import React, { ComponentPropsWithoutRef, useMemo } from 'react';
|
||||
import { Provider, Viewport } from '@radix-ui/react-toast';
|
||||
import { SimpleToast, SimpleToastProps } from './SimpleToast';
|
||||
import { useToast } from './useToast';
|
||||
|
||||
interface ToasterProps extends ComponentPropsWithoutRef<'div'> {}
|
||||
|
||||
export const Toaster = ({}: ToasterProps) => {
|
||||
const { toasts } = useToast();
|
||||
|
||||
const renderToasts = useMemo(
|
||||
() =>
|
||||
toasts.map(({ id, ...props }) => (
|
||||
<SimpleToast key={id} {...(props as SimpleToastProps)} id={id} />
|
||||
)),
|
||||
[toasts],
|
||||
);
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
{renderToasts}
|
||||
<Viewport className="z-toast fixed inset-x-0 bottom-0 mx-auto w-fit px-4 pb-10" />
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Toaster';
|
||||
export * from './useToast';
|
||||
@@ -0,0 +1,192 @@
|
||||
// Inspired by react-hot-toast library
|
||||
import React from 'react';
|
||||
import { type ToastProps } from '@radix-ui/react-toast';
|
||||
import { SimpleToastProps } from './SimpleToast';
|
||||
|
||||
const TOAST_LIMIT = 3;
|
||||
const TOAST_REMOVE_DELAY_DEFAULT = 7000;
|
||||
|
||||
type ToasterToast = ToastProps &
|
||||
SimpleToastProps & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: 'ADD_TOAST',
|
||||
UPDATE_TOAST: 'UPDATE_TOAST',
|
||||
DISMISS_TOAST: 'DISMISS_TOAST',
|
||||
REMOVE_TOAST: 'REMOVE_TOAST',
|
||||
} as const;
|
||||
|
||||
let count = 0;
|
||||
|
||||
const genId = () => {
|
||||
count = (count + 1) % Number.MAX_VALUE;
|
||||
return count.toString();
|
||||
};
|
||||
|
||||
type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType['ADD_TOAST'];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType['UPDATE_TOAST'];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType['DISMISS_TOAST'];
|
||||
toastId?: ToasterToast['id'];
|
||||
duration?: ToasterToast['duration'];
|
||||
}
|
||||
| {
|
||||
type: ActionType['REMOVE_TOAST'];
|
||||
toastId?: ToasterToast['id'];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const addToRemoveQueue = (toastId: string, duration: number) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: 'REMOVE_TOAST',
|
||||
toastId: toastId,
|
||||
});
|
||||
}, duration ?? TOAST_REMOVE_DELAY_DEFAULT);
|
||||
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'ADD_TOAST':
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
|
||||
case 'UPDATE_TOAST':
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id
|
||||
? ({ ...t, ...action.toast } as ToasterToast)
|
||||
: t,
|
||||
),
|
||||
};
|
||||
|
||||
case 'DISMISS_TOAST': {
|
||||
const { toastId, duration = TOAST_REMOVE_DELAY_DEFAULT } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId, duration);
|
||||
} else {
|
||||
state.toasts.forEach((_toast) => {
|
||||
addToRemoveQueue(_toast.id, duration);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t,
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'REMOVE_TOAST':
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(_state: State) => void> = [];
|
||||
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
const dispatch = (action: Action) => {
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState);
|
||||
});
|
||||
};
|
||||
|
||||
const toast = (props: ToasterToast) => {
|
||||
if (!props.duration) {
|
||||
props.duration = 2000;
|
||||
}
|
||||
const id = genId();
|
||||
|
||||
const update = (_props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: 'UPDATE_TOAST',
|
||||
toast: { ..._props, id },
|
||||
});
|
||||
const dismiss = () =>
|
||||
dispatch({ type: 'DISMISS_TOAST', toastId: id, duration: props.duration });
|
||||
|
||||
dispatch({
|
||||
type: 'ADD_TOAST',
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open: boolean) => {
|
||||
if (!open) dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
};
|
||||
};
|
||||
|
||||
const useToast = () => {
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
|
||||
};
|
||||
};
|
||||
|
||||
export { toast, useToast };
|
||||
@@ -0,0 +1,14 @@
|
||||
import { tv } from 'tailwind-variants';
|
||||
|
||||
export const tooltipTheme = tv({
|
||||
slots: {
|
||||
content: [
|
||||
'z-tooltip',
|
||||
'rounded-md',
|
||||
'bg-surface-high-contrast',
|
||||
'p-2',
|
||||
'text-elements-on-high-contrast',
|
||||
],
|
||||
arrow: ['fill-surface-high-contrast'],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import type {
|
||||
TooltipContentProps,
|
||||
TooltipTriggerProps,
|
||||
} from '@radix-ui/react-tooltip';
|
||||
import { ReactNode, useState } from 'react';
|
||||
|
||||
import { TooltipBase, type TooltipBaseProps } from './TooltipBase';
|
||||
|
||||
export interface TooltipProps extends TooltipBaseProps {
|
||||
triggerProps?: TooltipTriggerProps;
|
||||
contentProps?: TooltipContentProps;
|
||||
content?: ReactNode;
|
||||
}
|
||||
|
||||
// https://github.com/radix-ui/primitives/issues/955#issuecomment-1798201143
|
||||
// Wrap on top of Tooltip base to make tooltip open on mobile via click
|
||||
export const Tooltip = ({
|
||||
children,
|
||||
triggerProps,
|
||||
contentProps,
|
||||
content,
|
||||
...props
|
||||
}: TooltipProps) => {
|
||||
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<TooltipBase
|
||||
open={isTooltipVisible}
|
||||
onOpenChange={setIsTooltipVisible}
|
||||
{...props}
|
||||
>
|
||||
<TooltipBase.Trigger
|
||||
asChild
|
||||
onBlur={() => setIsTooltipVisible(false)}
|
||||
onClick={() => setIsTooltipVisible((prevOpen) => !prevOpen)}
|
||||
onFocus={() => setTimeout(() => setIsTooltipVisible(true), 0)}
|
||||
{...triggerProps}
|
||||
>
|
||||
{triggerProps?.children ?? children}
|
||||
</TooltipBase.Trigger>
|
||||
<TooltipBase.Content {...contentProps}>
|
||||
{content ?? contentProps?.children ?? 'Coming soon'}
|
||||
</TooltipBase.Content>
|
||||
</TooltipBase>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Provider,
|
||||
TooltipProps as RadixTooltipProps,
|
||||
Root,
|
||||
type TooltipProviderProps,
|
||||
} from '@radix-ui/react-tooltip';
|
||||
import { useState, type PropsWithChildren } from 'react';
|
||||
|
||||
import { TooltipContent } from './TooltipContent';
|
||||
import { TooltipTrigger } from './TooltipTrigger';
|
||||
|
||||
export interface TooltipBaseProps extends RadixTooltipProps {
|
||||
providerProps?: TooltipProviderProps;
|
||||
}
|
||||
|
||||
export const TooltipBase = ({
|
||||
children,
|
||||
providerProps,
|
||||
...props
|
||||
}: PropsWithChildren<TooltipBaseProps>) => {
|
||||
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<Provider {...providerProps}>
|
||||
<Root
|
||||
open={isTooltipVisible}
|
||||
onOpenChange={setIsTooltipVisible}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Root>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
TooltipBase.Trigger = TooltipTrigger;
|
||||
TooltipBase.Content = TooltipContent;
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Arrow,
|
||||
Content,
|
||||
Portal,
|
||||
type TooltipArrowProps,
|
||||
type TooltipContentProps,
|
||||
} from '@radix-ui/react-tooltip';
|
||||
|
||||
import { tooltipTheme } from '../Tooltip.theme';
|
||||
|
||||
export interface ContentProps extends TooltipContentProps {
|
||||
hasArrow?: boolean;
|
||||
arrowProps?: TooltipArrowProps;
|
||||
}
|
||||
|
||||
export const TooltipContent = ({
|
||||
children,
|
||||
arrowProps,
|
||||
className,
|
||||
hasArrow = true,
|
||||
...props
|
||||
}: ContentProps) => {
|
||||
const { content, arrow } = tooltipTheme();
|
||||
return (
|
||||
<Portal>
|
||||
<Content
|
||||
className={content({
|
||||
className,
|
||||
})}
|
||||
sideOffset={5}
|
||||
{...props}
|
||||
>
|
||||
{hasArrow && (
|
||||
<Arrow
|
||||
className={arrow({
|
||||
className: arrowProps?.className,
|
||||
})}
|
||||
{...arrowProps}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</Content>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './TooltipContent';
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Trigger, type TooltipTriggerProps } from '@radix-ui/react-tooltip';
|
||||
|
||||
export type TriggerProps = TooltipTriggerProps;
|
||||
|
||||
export const TooltipTrigger = ({ children, ...props }: TriggerProps) => {
|
||||
return (
|
||||
<Trigger asChild {...props}>
|
||||
{children}
|
||||
</Trigger>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './TooltipTrigger';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Tooltip';
|
||||
export * from './TooltipBase';
|
||||
@@ -1,3 +1,9 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
.focus-ring {
|
||||
@apply focus-visible:ring-[3px] focus-visible:ring-snowball-200 focus-visible:ring-offset-1 focus-visible:ring-offset-snowball-500 focus-visible:outline-none;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import assert from 'assert';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { GQLClient } from 'gql-client';
|
||||
|
||||
import { ThemeProvider } from '@material-tailwind/react';
|
||||
|
||||
import './index.css';
|
||||
import '@fontsource/inter';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { GQLClientProvider } from './context/GQLClientContext';
|
||||
import Web3ModalProvider from './context/Web3ModalProvider';
|
||||
import { SERVER_GQL_PATH } from './constants';
|
||||
import { Toaster } from 'components/shared/Toast';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement,
|
||||
@@ -28,12 +28,10 @@ const gqlClient = new GQLClient({ gqlEndpoint });
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider>
|
||||
<Web3ModalProvider>
|
||||
<GQLClientProvider client={gqlClient}>
|
||||
<App />
|
||||
<Toaster position="bottom-center" />
|
||||
</GQLClientProvider>
|
||||
</Web3ModalProvider>
|
||||
<GQLClientProvider client={gqlClient}>
|
||||
<App />
|
||||
<Toaster />
|
||||
</GQLClientProvider>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Calendar } from 'components/shared/Calendar';
|
||||
import { DatePicker } from 'components/shared/DatePicker';
|
||||
import { Radio } from 'components/shared/Radio';
|
||||
import { SegmentedControls } from 'components/shared/SegmentedControls';
|
||||
import { Switch } from 'components/shared/Switch';
|
||||
import { avatars, avatarsFallback } from './renders/avatar';
|
||||
import { renderBadges } from './renders/badge';
|
||||
import {
|
||||
renderButtonIcons,
|
||||
renderButtons,
|
||||
renderDisabledButtons,
|
||||
renderLinks,
|
||||
} from './renders/button';
|
||||
import {
|
||||
renderCheckbox,
|
||||
renderCheckboxWithDescription,
|
||||
} from './renders/checkbox';
|
||||
import {
|
||||
renderInlineNotificationWithDescriptions,
|
||||
renderInlineNotifications,
|
||||
} from './renders/inlineNotifications';
|
||||
import { renderInputs } from './renders/input';
|
||||
import { RADIO_OPTIONS } from './renders/radio';
|
||||
import { SEGMENTED_CONTROLS_OPTIONS } from './renders/segmentedControls';
|
||||
import {
|
||||
renderTabWithBadges,
|
||||
renderTabs,
|
||||
renderVerticalTabs,
|
||||
} from './renders/tabs';
|
||||
import { renderDefaultTag, renderMinimalTag } from './renders/tag';
|
||||
import { renderToast, renderToastsWithCta } from './renders/toast';
|
||||
import { renderTooltips } from './renders/tooltip';
|
||||
|
||||
type ValuePiece = Date | null;
|
||||
type Value = ValuePiece | [ValuePiece, ValuePiece];
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const [singleDate, setSingleDate] = useState<Value>();
|
||||
const [dateRange, setDateRange] = useState<Value>();
|
||||
const [selectedSegmentedControl, setSelectedSegmentedControl] =
|
||||
useState<string>('Test 1');
|
||||
const [switchValue, setSwitchValue] = useState(false);
|
||||
const [selectedRadio, setSelectedRadio] = useState<string>('');
|
||||
|
||||
return (
|
||||
<div className="relative h-full min-h-full">
|
||||
<div className="flex flex-col items-center justify-center container mx-auto px-20 py-20">
|
||||
<h1 className="text-4xl font-bold">Manual Storybook</h1>
|
||||
<p className="mt-4 text-lg text-center text-gray-500">
|
||||
Get started by editing{' '}
|
||||
<code className="p-2 font-mono text-sm bg-gray-100 rounded-md">
|
||||
packages/frontend/src/pages/components/index.tsx
|
||||
</code>
|
||||
</p>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Tag */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Tag</h1>
|
||||
<div className="flex flex-col gap-10 items-center justify-center">
|
||||
{renderDefaultTag()}
|
||||
{renderMinimalTag()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Toast */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Toasts</h1>
|
||||
{renderToastsWithCta()}
|
||||
{renderToast()}
|
||||
</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Tooltip */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Tooltip</h1>
|
||||
<div className="flex w-full flex-wrap max-w-[680px] justify-center gap-10">
|
||||
{renderTooltips()}
|
||||
</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Input */}
|
||||
<h1 className="text-2xl font-bold">Input</h1>
|
||||
<div className="flex w-full flex-col gap-10">{renderInputs()}</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Button */}
|
||||
<h1 className="text-2xl font-bold">Button</h1>
|
||||
<div className="flex flex-col gap-10">
|
||||
{renderButtons()}
|
||||
{renderButtonIcons()}
|
||||
</div>
|
||||
|
||||
{/* Link */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Link</h1>
|
||||
<div className="flex gap-4 items-center justify-center">
|
||||
{renderLinks()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Disabled button, icon only, and link */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-2xl font-bold text-center">Disabled</h1>
|
||||
<p className="text-lg text-center text-gray-500">
|
||||
Button – icon only – link
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-10 items-center justify-center">
|
||||
{renderDisabledButtons()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Badge */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Badge</h1>
|
||||
<div className="space-y-5">{renderBadges()}</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Checkbox */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Checkbox</h1>
|
||||
<div className="flex gap-10 flex-wrap">{renderCheckbox()}</div>
|
||||
<div className="flex gap-10 flex-wrap">
|
||||
{renderCheckboxWithDescription()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Calendar */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Calendar</h1>
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="space-y-5 flex flex-col items-center">
|
||||
<p>Selected date: {singleDate?.toString()}</p>
|
||||
<Calendar
|
||||
value={singleDate}
|
||||
onChange={setSingleDate}
|
||||
onSelect={setSingleDate}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-5 flex flex-col items-center">
|
||||
<p>
|
||||
Start date:{' '}
|
||||
{dateRange instanceof Array ? dateRange[0]?.toString() : ''}{' '}
|
||||
<br />
|
||||
End date:{' '}
|
||||
{dateRange instanceof Array ? dateRange[1]?.toString() : ''}
|
||||
</p>
|
||||
<Calendar
|
||||
selectRange
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl font-bold">Date Picker</h1>
|
||||
<div className="flex flex-col gap-10 items-center justify-center">
|
||||
<DatePicker value={singleDate} onChange={setSingleDate} />
|
||||
<DatePicker
|
||||
selectRange
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Avatar</h1>
|
||||
<div className="flex gap-10 flex-wrap max-w-[522px]">
|
||||
{avatars}
|
||||
{avatarsFallback}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Tabs</h1>
|
||||
<div className="flex flex-col gap-10 items-center justify-center">
|
||||
{renderTabs()}
|
||||
{renderTabWithBadges()}
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold">Vertical Tabs</h1>
|
||||
{renderVerticalTabs()}
|
||||
</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Segmented Controls */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Segmented Controls</h1>
|
||||
<div className="flex flex-col gap-10">
|
||||
<SegmentedControls
|
||||
options={SEGMENTED_CONTROLS_OPTIONS}
|
||||
value={selectedSegmentedControl}
|
||||
onChange={setSelectedSegmentedControl}
|
||||
/>
|
||||
<SegmentedControls
|
||||
size="sm"
|
||||
options={SEGMENTED_CONTROLS_OPTIONS}
|
||||
value={selectedSegmentedControl}
|
||||
onChange={setSelectedSegmentedControl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Switch */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Switch</h1>
|
||||
<div className="flex flex-col gap-10 items-center justify-center">
|
||||
<Switch
|
||||
label="Label"
|
||||
checked={switchValue}
|
||||
onCheckedChange={setSwitchValue}
|
||||
/>
|
||||
<Switch
|
||||
label="Label"
|
||||
description="Additional information or context"
|
||||
checked={switchValue}
|
||||
onCheckedChange={setSwitchValue}
|
||||
/>
|
||||
<Switch disabled label="Disabled unchecked" />
|
||||
<Switch disabled checked label="Disabled checked" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Radio */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Radio</h1>
|
||||
<div className="flex gap-20 items-start">
|
||||
<Radio
|
||||
options={RADIO_OPTIONS}
|
||||
orientation="vertical"
|
||||
value={selectedRadio}
|
||||
onValueChange={setSelectedRadio}
|
||||
/>
|
||||
<Radio
|
||||
options={RADIO_OPTIONS}
|
||||
orientation="horizontal"
|
||||
value={selectedRadio}
|
||||
onValueChange={setSelectedRadio}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||
|
||||
{/* Inline notification */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Inline Notification</h1>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{renderInlineNotifications()}
|
||||
</div>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{renderInlineNotificationWithDescriptions()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Avatar, AvatarVariants } from 'components/shared/Avatar';
|
||||
|
||||
const avatarSizes: AvatarVariants['size'][] = [18, 20, 24, 28, 32, 36, 40, 44];
|
||||
const avatarVariants: AvatarVariants['type'][] = ['gray', 'orange', 'blue'];
|
||||
|
||||
export const avatars = avatarSizes.map((size) => {
|
||||
return (
|
||||
<Avatar initials="SY" key={String(size)} size={size} imageSrc="/gray.png" />
|
||||
);
|
||||
});
|
||||
|
||||
export const avatarsFallback = avatarVariants.map((color) => {
|
||||
return avatarSizes.map((size) => {
|
||||
return (
|
||||
<Avatar initials="SY" key={`${color}-${size}`} type={color} size={size} />
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Badge } from 'components/shared/Badge';
|
||||
import { BadgeTheme } from 'components/shared/Badge/Badge.theme';
|
||||
|
||||
export const renderBadges = () => {
|
||||
return ['primary', 'secondary', 'tertiary', 'inset'].map((variant, index) => (
|
||||
<div className="flex gap-5" key={index}>
|
||||
{['sm', 'xs'].map((size) => (
|
||||
<Badge
|
||||
key={size}
|
||||
variant={variant as BadgeTheme['variant']}
|
||||
size={size as BadgeTheme['size']}
|
||||
>
|
||||
1
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Button, ButtonTheme } from 'components/shared/Button';
|
||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||
import React from 'react';
|
||||
|
||||
const buttonVariants = ['primary', 'secondary', 'tertiary', 'danger'] as const;
|
||||
const buttonSizes = ['lg', 'md', 'sm', 'xs'] as const;
|
||||
const iconOnlyVariants = [
|
||||
'primary',
|
||||
'secondary',
|
||||
'tertiary',
|
||||
'ghost',
|
||||
'danger',
|
||||
'danger-ghost',
|
||||
] as const;
|
||||
const linkVariants = ['link', 'link-emphasized'] as const;
|
||||
|
||||
export const renderButtons = () => {
|
||||
return buttonVariants.map((variant, index) => (
|
||||
<div className="flex gap-5 flex-wrap" key={index}>
|
||||
{buttonSizes.map((size) => (
|
||||
<Button
|
||||
leftIcon={<PlusIcon />}
|
||||
rightIcon={<PlusIcon />}
|
||||
variant={variant as ButtonTheme['variant']}
|
||||
size={size as ButtonTheme['size']}
|
||||
key={`${variant}-${size}`}
|
||||
>
|
||||
Button
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
export const renderButtonIcons = () => {
|
||||
return iconOnlyVariants.map((variant, index) => (
|
||||
<div className="flex gap-5 flex-wrap" key={index}>
|
||||
{buttonSizes.map((size) => (
|
||||
<Button
|
||||
iconOnly
|
||||
variant={variant as ButtonTheme['variant']}
|
||||
size={size as ButtonTheme['size']}
|
||||
key={`${variant}-${size}`}
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
export const renderLinks = () => {
|
||||
return linkVariants.map((variant) => (
|
||||
<Button
|
||||
variant={variant}
|
||||
key={variant}
|
||||
leftIcon={<PlusIcon />}
|
||||
rightIcon={<PlusIcon />}
|
||||
>
|
||||
Link
|
||||
</Button>
|
||||
));
|
||||
};
|
||||
|
||||
export const renderDisabledButtons = () => {
|
||||
return (
|
||||
<>
|
||||
{/* Button variants */}
|
||||
<Button leftIcon={<PlusIcon />} rightIcon={<PlusIcon />} disabled>
|
||||
Button
|
||||
</Button>
|
||||
{/* Icon only variants */}
|
||||
<Button iconOnly disabled>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
{/* Link variantws */}
|
||||
<Button
|
||||
variant="link"
|
||||
disabled
|
||||
leftIcon={<PlusIcon />}
|
||||
rightIcon={<PlusIcon />}
|
||||
>
|
||||
Link
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { Checkbox } from 'components/shared/Checkbox';
|
||||
|
||||
export const renderCheckbox = () => {
|
||||
return Array.from({ length: 5 }).map((_, index) => (
|
||||
<Checkbox
|
||||
id={`checkbox-${index + 1}`}
|
||||
key={index}
|
||||
label={`Label ${index + 1}`}
|
||||
disabled={index === 2 || index === 4 ? true : false}
|
||||
checked={index === 4 ? true : undefined}
|
||||
value={`value-${index + 1}`}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
export const renderCheckboxWithDescription = () => {
|
||||
return Array.from({ length: 2 }).map((_, index) => (
|
||||
<Checkbox
|
||||
id={`checkbox-description-${index + 1}`}
|
||||
key={index}
|
||||
label={`Label ${index + 1}`}
|
||||
description={`Description of the checkbox ${index + 1}`}
|
||||
value={`value-with-description-${index + 1}`}
|
||||
/>
|
||||
));
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { InlineNotification } from 'components/shared/InlineNotification';
|
||||
import { InlineNotificationTheme } from 'components/shared/InlineNotification/InlineNotification.theme';
|
||||
import React from 'react';
|
||||
|
||||
const inlineNotificationVariants = [
|
||||
'info',
|
||||
'danger',
|
||||
'warning',
|
||||
'success',
|
||||
'generic',
|
||||
];
|
||||
const inlineNotificationSizes = ['md', 'sm'];
|
||||
|
||||
export const renderInlineNotifications = () => {
|
||||
return inlineNotificationVariants.map((variant) => (
|
||||
<div className="space-y-2" key={variant}>
|
||||
{inlineNotificationSizes.map((size) => (
|
||||
<InlineNotification
|
||||
size={size as InlineNotificationTheme['size']}
|
||||
variant={variant as InlineNotificationTheme['variant']}
|
||||
key={`${variant}-${size}`}
|
||||
title="Notification title goes here"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
export const renderInlineNotificationWithDescriptions = () => {
|
||||
return inlineNotificationVariants.map((variant) => (
|
||||
<div className="space-y-2" key={variant}>
|
||||
{inlineNotificationSizes.map((size) => (
|
||||
<InlineNotification
|
||||
size={size as InlineNotificationTheme['size']}
|
||||
variant={variant as InlineNotificationTheme['variant']}
|
||||
key={`${variant}-${size}`}
|
||||
title="Notification title goes here"
|
||||
description="Description goes here"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { SearchIcon, CrossIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
export const renderInputs = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full gap-10">
|
||||
<Input
|
||||
label="Label"
|
||||
description="Additional information or context"
|
||||
leftIcon={<SearchIcon />}
|
||||
rightIcon={<CrossIcon />}
|
||||
placeholder="Placeholder text"
|
||||
/>
|
||||
<Input
|
||||
disabled
|
||||
label="Label"
|
||||
description="Additional information or context"
|
||||
placeholder="Placeholder text"
|
||||
/>
|
||||
<Input
|
||||
state="error"
|
||||
label="Label"
|
||||
description="Additional information or context"
|
||||
placeholder="Placeholder text"
|
||||
helperText="The error goes here"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full gap-10">
|
||||
<Input
|
||||
label="Label"
|
||||
leftIcon={<SearchIcon />}
|
||||
rightIcon={<CrossIcon />}
|
||||
description="Additional information or context"
|
||||
placeholder="Placeholder text"
|
||||
size="sm"
|
||||
/>
|
||||
<Input
|
||||
disabled
|
||||
label="Label"
|
||||
description="Additional information or context"
|
||||
placeholder="Placeholder text"
|
||||
size="sm"
|
||||
/>
|
||||
<Input
|
||||
state="error"
|
||||
label="Label"
|
||||
description="Additional information or context"
|
||||
placeholder="Placeholder text"
|
||||
size="sm"
|
||||
helperText="The error goes here"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { RadioOption } from 'components/shared/Radio';
|
||||
|
||||
export const RADIO_OPTIONS: RadioOption[] = [
|
||||
{
|
||||
label: 'Label 1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: 'Label 2',
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
label: 'Label 3',
|
||||
value: '3',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { Badge } from 'components/shared/Badge';
|
||||
import { SegmentedControlsOption } from 'components/shared/SegmentedControls';
|
||||
|
||||
export const SEGMENTED_CONTROLS_OPTIONS: SegmentedControlsOption[] = [
|
||||
{ label: 'Test 1', value: 'Test 1' },
|
||||
{
|
||||
label: 'Test 2',
|
||||
value: 'Test 2',
|
||||
leftIcon: (
|
||||
<Badge size="xs" variant="tertiary">
|
||||
1
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Test 3',
|
||||
value: 'Test 3',
|
||||
rightIcon: (
|
||||
<Badge size="xs" variant="tertiary">
|
||||
1
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Test 4',
|
||||
value: 'Test 4',
|
||||
leftIcon: (
|
||||
<Badge size="xs" variant="tertiary">
|
||||
1
|
||||
</Badge>
|
||||
),
|
||||
rightIcon: (
|
||||
<Badge size="xs" variant="tertiary">
|
||||
1
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Test 5',
|
||||
value: 'Test 5',
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { Tabs } from 'components/shared/Tabs';
|
||||
import { Badge } from 'components/shared/Badge';
|
||||
import { GlobeIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
const tabs = Array.from({ length: 8 });
|
||||
|
||||
export const renderTabs = () => {
|
||||
return (
|
||||
<Tabs defaultValue="A">
|
||||
<Tabs.List>
|
||||
{tabs.map((_, index) => (
|
||||
<Tabs.Trigger key={index} value={index.toString()}>
|
||||
Tab item {index}
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderTabWithBadges = () => {
|
||||
return (
|
||||
<Tabs defaultValue="A">
|
||||
<Tabs.List>
|
||||
{tabs.map((_, index) => (
|
||||
<Tabs.Trigger
|
||||
key={index}
|
||||
value={index.toString()}
|
||||
icon={<Badge variant="tertiary">{index}</Badge>}
|
||||
>
|
||||
Tab item
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderVerticalTabs = () => {
|
||||
return (
|
||||
<Tabs defaultValue="A" orientation="vertical">
|
||||
<Tabs.List>
|
||||
{tabs.slice(0, 4).map((_, index) => (
|
||||
<Tabs.Trigger
|
||||
key={index}
|
||||
icon={<GlobeIcon />}
|
||||
value={index.toString()}
|
||||
>
|
||||
Tab item {index}
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { Tag } from 'components/shared/Tag';
|
||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
export const renderDefaultTag = () =>
|
||||
(['default'] as const).map((style) => (
|
||||
<div key={style} className="flex gap-4 items-center">
|
||||
{(
|
||||
['attention', 'negative', 'positive', 'emphasized', 'neutral'] as const
|
||||
).map((type) => (
|
||||
<div key={type} className="flex flex-col gap-4">
|
||||
<Tag
|
||||
leftIcon={<PlusIcon />}
|
||||
rightIcon={<PlusIcon />}
|
||||
style={style}
|
||||
type={type}
|
||||
size="sm"
|
||||
>
|
||||
Label
|
||||
</Tag>
|
||||
<Tag
|
||||
leftIcon={<PlusIcon />}
|
||||
rightIcon={<PlusIcon />}
|
||||
size="xs"
|
||||
style={style}
|
||||
type={type}
|
||||
>
|
||||
Label
|
||||
</Tag>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
|
||||
export const renderMinimalTag = () =>
|
||||
(['minimal'] as const).map((style) => (
|
||||
<div key={style} className="flex gap-4 items-center">
|
||||
{(
|
||||
['attention', 'negative', 'positive', 'emphasized', 'neutral'] as const
|
||||
).map((type) => (
|
||||
<div key={type} className="flex flex-col gap-4">
|
||||
<Tag
|
||||
leftIcon={<PlusIcon />}
|
||||
rightIcon={<PlusIcon />}
|
||||
style={style}
|
||||
type={type}
|
||||
size="sm"
|
||||
>
|
||||
Label
|
||||
</Tag>
|
||||
<Tag
|
||||
leftIcon={<PlusIcon />}
|
||||
rightIcon={<PlusIcon />}
|
||||
size="xs"
|
||||
style={style}
|
||||
type={type}
|
||||
>
|
||||
Label
|
||||
</Tag>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
|
||||
export const renderToastsWithCta = () => {
|
||||
const { toast, dismiss } = useToast();
|
||||
|
||||
return (
|
||||
<div className="flex gap-10">
|
||||
{(['success', 'error', 'warning', 'info', 'loading'] as const).map(
|
||||
(variant, index) => (
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast({
|
||||
onDismiss: dismiss,
|
||||
id: `${variant}_${index}`,
|
||||
title: 'Project created',
|
||||
cta: [
|
||||
{
|
||||
buttonLabel: 'Button',
|
||||
size: 'xs',
|
||||
variant: 'tertiary',
|
||||
},
|
||||
{
|
||||
buttonLabel: 'Button',
|
||||
size: 'xs',
|
||||
},
|
||||
],
|
||||
variant,
|
||||
})
|
||||
}
|
||||
key={`${variant}_${index}`}
|
||||
>
|
||||
{variant} with cta
|
||||
</Button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderToast = () => {
|
||||
const { toast, dismiss } = useToast();
|
||||
|
||||
return (
|
||||
<div className="flex gap-10">
|
||||
{(['success', 'error', 'warning', 'info', 'loading'] as const).map(
|
||||
(variant, index) => (
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast({
|
||||
onDismiss: dismiss,
|
||||
id: `${variant}_${index}`,
|
||||
title: 'Project created',
|
||||
variant,
|
||||
})
|
||||
}
|
||||
key={`${variant}_${index}`}
|
||||
>
|
||||
{variant}
|
||||
</Button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Tooltip } from 'components/shared/Tooltip';
|
||||
import { ContentProps } from 'components/shared/Tooltip/TooltipContent';
|
||||
|
||||
const alignments: ContentProps['align'][] = ['start', 'center', 'end'];
|
||||
const sides: ContentProps['side'][] = ['left', 'top', 'bottom', 'right'];
|
||||
|
||||
export const renderTooltips = () => {
|
||||
const tooltips = sides.map((side) => {
|
||||
return alignments.map((align) => {
|
||||
return (
|
||||
<Tooltip
|
||||
key={`${side}-${align}`}
|
||||
content="tooltip content"
|
||||
contentProps={{ align, side }}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
key={`${side}-${align}`}
|
||||
className="h-16 self-center"
|
||||
>
|
||||
Tooltip ({side} - {align})
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
});
|
||||
return tooltips;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { clsx } from 'clsx';
|
||||
import type { ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
/**
|
||||
* Returns a merged class name string by merging and processing multiple class names and Tailwind CSS styles.
|
||||
*
|
||||
* @param {...string[]} args - One or more class names and/or Tailwind CSS styles to be merged.
|
||||
* @returns {string} - The merged class name string.
|
||||
*/
|
||||
export function cn(...args: ClassValue[]): string {
|
||||
return twMerge(clsx(args));
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Children, cloneElement, isValidElement } from 'react';
|
||||
import type { Attributes, ReactElement, ReactNode } from 'react';
|
||||
|
||||
/**
|
||||
* Clones an icon element with optional additional props.
|
||||
* @param {ReactNode} icon - The icon element to clone.
|
||||
* @param {Attributes & P} [props] - Additional props to apply to the cloned icon.
|
||||
* @returns {ReactNode} - The cloned icon element with the applied props.
|
||||
*/
|
||||
export function cloneIcon<P extends object>(
|
||||
icon: ReactNode,
|
||||
props?: Attributes & P,
|
||||
): ReactNode {
|
||||
return Children.map(icon, (child) =>
|
||||
isValidElement(child) ? cloneElement(child as ReactElement, props) : child,
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user