diff --git a/.DS_Store b/.DS_Store index e5a73da3e..4b8789623 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.flowconfig b/.flowconfig index 0c62c89b5..2fa50b7e3 100644 --- a/.flowconfig +++ b/.flowconfig @@ -11,7 +11,5 @@ [lints] [options] -esproposal.export_star_as=enable -module.file_ext=.less [strict] diff --git a/eslint.config.js b/eslint.config.js index 61a8a1ec8..dfc77550b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,5 +1,7 @@ import js from '@eslint/js'; import babelParser from '@babel/eslint-parser'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import typescriptParser from '@typescript-eslint/parser'; import flowtype from 'eslint-plugin-flowtype'; import { importX } from 'eslint-plugin-import-x'; import jest from 'eslint-plugin-jest'; @@ -7,13 +9,49 @@ import jsxA11y from 'eslint-plugin-jsx-a11y'; import react from 'eslint-plugin-react'; import globals from 'globals'; import stylistic from '@stylistic/eslint-plugin'; +import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'; + +const commonRules = { + 'class-methods-use-this': 'off', + '@stylistic/comma-dangle': ['error', 'never'], + "import-x/no-named-as-default": "off", + "import-x/no-named-as-default-member": "off", + 'import-x/prefer-default-export': 'off', + 'jsx-a11y/media-has-caption': 'off', + '@stylistic/jsx-quotes': ['error', 'prefer-single'], + '@stylistic/lines-between-class-members': 'off', + '@stylistic/max-len': ['error', { + code: 120, + ignoreStrings: true + }], + 'no-underscore-dangle': 'off', + 'no-use-before-define': 'off', + '@stylistic/quote-props': ['error', 'as-needed', { + keywords: false, + unnecessary: true, + numbers: true + }], + 'react/display-name': 'off', + 'react/default-props-match-prop-types': 'off', + 'react/destructuring-assignment': 'off', + 'react/jsx-no-bind': 'off', + 'react/jsx-props-no-spreading': 'off', + 'react/no-array-index-key': 'off', + 'react/no-did-update-set-state': 'off', + 'react/prefer-stateless-function': 'off', + 'react/require-default-props': 'off', + 'react/sort-comp': 'off', + 'react/static-property-placement': 'off', + '@stylistic/semi-style': ['error', 'last'], + '@stylistic/semi': ['error', 'always'] +}; export default [ importX.flatConfigs.recommended, js.configs.recommended, react.configs.flat.recommended, { - files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'], + files: ['**/*.{js,jsx,mjs,cjs}'], languageOptions: { ecmaVersion: 'latest', globals: { @@ -48,48 +86,64 @@ export default [ '@stylistic': stylistic }, rules: { - 'class-methods-use-this': 'off', - '@stylistic/comma-dangle': ['error', 'never'], - "import-x/no-named-as-default": "off", - "import-x/no-named-as-default-member": "off", - 'import-x/prefer-default-export': 'off', - 'jsx-a11y/media-has-caption': 'off', - '@stylistic/jsx-quotes': ['error', 'prefer-single'], - '@stylistic/lines-between-class-members': 'off', - '@stylistic/max-len': ['error', { - code: 120, - ignoreStrings: true - }], - 'no-underscore-dangle': 'off', - 'no-use-before-define': 'off', - '@stylistic/quote-props': ['error', 'as-needed', { - keywords: false, - unnecessary: true, - numbers: true - }], - 'react/display-name': 'off', - 'react/default-props-match-prop-types': 'off', - 'react/destructuring-assignment': 'off', + ...commonRules, 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] - }], - 'react/jsx-no-bind': 'off', - 'react/jsx-props-no-spreading': 'off', - 'react/no-array-index-key': 'off', - 'react/no-did-update-set-state': 'off', - 'react/prefer-stateless-function': 'off', - 'react/require-default-props': 'off', - 'react/sort-comp': 'off', - 'react/static-property-placement': 'off', - '@stylistic/semi-style': ['error', 'last'], - '@stylistic/semi': ['error', 'always'] + }] }, settings: { react: { version: 'detect' } } - }, { + }, + { + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 'latest', + globals: { + ...globals.browser, + ...jest.environments.globals.globals, + __dirname: 'readonly', + Atomics: 'readonly', + JSX: 'readonly', + SharedArrayBuffer: 'readonly', + TimeoutID: 'readonly', + process: 'readonly' + }, + parser: typescriptParser, + parserOptions: { + ecmaFeatures: { + jsx: true + }, + project: './tsconfig.json' + }, + sourceType: 'module' + }, + plugins: { + react, + '@typescript-eslint': typescriptEslint, + jest, + 'jsx-a11y': jsxA11y, + '@stylistic': stylistic + }, + rules: { + ...commonRules, + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error"] + }, + settings: { + react: { + version: 'detect' + }, + 'import-x/resolver-next': [ + createTypeScriptImportResolver({ + project: 'packages/*/{ts,js}config.json' + }) + ] + } + }, + { ignores: [ '**/dist/*' ] diff --git a/package.json b/package.json index 678306938..f96cc82af 100644 --- a/package.json +++ b/package.json @@ -19,27 +19,31 @@ "@babel/core": "^7.28.0", "@babel/eslint-parser": "^7.28.0", "@babel/preset-env": "^7.28.0", - "@eslint/compat": "^1.3.1", + "@babel/preset-typescript": "^7.27.1", + "@eslint/compat": "^1.3.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.31.0", + "@eslint/js": "^9.33.0", + "@stylistic/eslint-plugin": "^5.2.3", + "@typescript-eslint/eslint-plugin": "^8.39.1", + "@typescript-eslint/parser": "^8.39.1", "babel-eslint": "^10.1.0", - "babel-jest": "^30.0.4", + "babel-jest": "^30.0.5", "babel-plugin-transform-flow-strip-types": "^6.22.0", - "eslint": "^9.31.0", + "eslint": "^9.33.0", "eslint-config-airbnb": "^19.0.4", + "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-storybook": "^9.0.17", - "flow-bin": "^0.275.0", + "eslint-plugin-storybook": "^9.1.2", + "flow-bin": "^0.278.0", "globals": "^16.3.0", "jest": "^27.5.1", - "minimist": "^1.2.6", - "@stylistic/eslint-plugin": "^5.2.2", - "underscore": "^1.13.4" + "minimist": "^1.2.8", + "underscore": "^1.13.7" }, "jest": { "projects": [ diff --git a/packages/.DS_Store b/packages/.DS_Store index 6ce06acdf..eea224ace 100644 Binary files a/packages/.DS_Store and b/packages/.DS_Store differ diff --git a/packages/performant-ui/package.json b/packages/performant-ui/package.json new file mode 100644 index 000000000..3ccbc7995 --- /dev/null +++ b/packages/performant-ui/package.json @@ -0,0 +1,32 @@ +{ + "name": "@performant-software/performant-ui", + "version": "0.0.1-beta.50", + "main": "./dist/index.cjs.js", + "module": "./dist/index.es.js", + "types": "./dist/performant-ui/src/index.d.ts", + "type": "module", + "scripts": { + "build": "vite build", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "peerDependencies": { + "react": ">= 16.13.1 < 20.0.0", + "react-dom": ">= 16.13.1 < 20.0.0" + }, + "dependencies": { + "clsx": "^2.1.1", + "@headlessui/react": "^2.2.6", + "react-icons": "^5.5.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.2.1", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "typescript": "^5.8.3", + "vite": "^5.1.4", + "vite-plugin-dts": "4.5.4" + } +} \ No newline at end of file diff --git a/packages/performant-ui/src/components/Avatar.tsx b/packages/performant-ui/src/components/Avatar.tsx new file mode 100644 index 000000000..b6226bee9 --- /dev/null +++ b/packages/performant-ui/src/components/Avatar.tsx @@ -0,0 +1,48 @@ +import clsx from 'clsx' +import React from 'react' + +interface Props { + alt?: string + initials: string + src?: string + classes?: { + container?: string + img?: string + initials?: string + } +} + +const Avatar: React.FC = (props) => { + return ( +
+ {props.src + ? ( + {props.alt} + ) + : ( + + {props.initials} + + )} +
+ ) +} + +export default Avatar diff --git a/packages/performant-ui/src/components/Badge.tsx b/packages/performant-ui/src/components/Badge.tsx new file mode 100644 index 000000000..70397bb0a --- /dev/null +++ b/packages/performant-ui/src/components/Badge.tsx @@ -0,0 +1,32 @@ +import clsx from 'clsx'; +import React from 'react'; + +const colors = { + faircopy: 'bg-[#E9ECF5] hover:bg-[#CED8E9] text-[#324872]', + green: 'bg-green-100 hover:bg-green-200 text-green-500', + gray: 'bg-gray-100 hover:bg-gray-200 text-gray-500', + red: 'bg-red-100 hover:bg-red-200 text-red-500', + blue: 'bg-blue-100 hover:bg-blue-200 text-blue-500' +} as const; + +interface Props { + className?: string + children: React.ReactNode + color?: keyof typeof colors +} + +const Badge: React.FC = (props) => { + const color = props.color || 'faircopy'; + + return ( + + {props.children} + + ); +}; + +export default Badge; \ No newline at end of file diff --git a/packages/performant-ui/src/components/Button.tsx b/packages/performant-ui/src/components/Button.tsx new file mode 100644 index 000000000..9b076b59b --- /dev/null +++ b/packages/performant-ui/src/components/Button.tsx @@ -0,0 +1,51 @@ +import clsx from 'clsx'; +import React, { useMemo } from 'react'; + +interface Props { + ariaLabel?: string + className?: string + children?: React.ReactNode + disabled?: boolean + iconOnly?: boolean + onClick?: (...args: any[]) => any + size?: 'xs' | 's' | 'base' | 'l' | 'xl' + type?: 'button' | 'submit' | 'reset' + variant?: 'filled' | 'outline' | 'plain' +} + +const Button: React.FC = (props) => { + const size = useMemo(() => props.size || 'base', [props.size]); + const variant = useMemo(() => props.variant || 'filled', [props.variant]); + + return ( + + ); +}; + +export default Button; diff --git a/packages/performant-ui/src/components/Card.tsx b/packages/performant-ui/src/components/Card.tsx new file mode 100644 index 000000000..7caf1507f --- /dev/null +++ b/packages/performant-ui/src/components/Card.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import clsx from 'clsx'; + +interface CardProps { + children: React.ReactNode + className?: string +} + +interface CardSectionProps { + children: React.ReactNode + className?: string + padded?: boolean +} + +type CardComponent = React.FC & { + Section: React.FC; +}; + +const Card: CardComponent = (props) => { + return ( +
+ {props.children} +
+ ); +}; + +Card.Section = (props: CardSectionProps) => { + const { padded = true } = props; + + return ( +
+ {props.children} +
+ ); +}; + +export default Card; diff --git a/packages/performant-ui/src/components/Checkbox.tsx b/packages/performant-ui/src/components/Checkbox.tsx new file mode 100644 index 000000000..ad1c30747 --- /dev/null +++ b/packages/performant-ui/src/components/Checkbox.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Description, Field, Checkbox as HeadlessCheckbox, Label } from '@headlessui/react'; +import { FaCheck } from 'react-icons/fa'; +import { MdOutlineHorizontalRule } from 'react-icons/md'; +import clsx from 'clsx'; + +interface Props { + classes?: { + field?: string, + checkbox?: string, + label?: string, + description?: string + } + description?: string; + disabled?: boolean; + label?: string; + indeterminate?: boolean; + onChange: (arg: boolean) => any; + value?: boolean; +} + +const Checkbox: React.FC = (props) => { + return ( + + props.onChange(val)} + className={clsx( + 'group rounded-sm border border-zinc-200 w-4 h-4 flex items-center justify-center data-checked:bg-primary data-checked:border-primary data-indeterminate:border-primary data-indeterminate:bg-primary my-0.5 outline-offset-2 focus:outline-2 focus:outline-primary data-disabled:opacity-50 data-disabled:grayscale hover:border-zinc-300 hover:data-checked:bg-primary-light hover:data-checked:border-primary-light', + props.classes?.checkbox + )} + > + + + +
+ {props.label && } + {props.description && ( + + {props.description} + )} +
+
+ ); +}; + +export default Checkbox; diff --git a/packages/performant-ui/src/components/Dialog.tsx b/packages/performant-ui/src/components/Dialog.tsx new file mode 100644 index 000000000..ec279ba7b --- /dev/null +++ b/packages/performant-ui/src/components/Dialog.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Dialog as HeadlessDialog, DialogBackdrop, DialogPanel, DialogProps } from '@headlessui/react'; +import clsx from 'clsx'; + +interface Props extends DialogProps { + children: React.ReactNode +} + +const Dialog: React.FC = (props: Props) => { + return ( + + + + {props.children} + + + + ); +}; + +export default Dialog; diff --git a/packages/performant-ui/src/components/Dropdown.tsx b/packages/performant-ui/src/components/Dropdown.tsx new file mode 100644 index 000000000..124f9acf0 --- /dev/null +++ b/packages/performant-ui/src/components/Dropdown.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { Menu, MenuButton, MenuButtonProps, MenuItem, MenuItems } from '@headlessui/react'; +import { findByType } from '../helpers/Element'; +import clsx from 'clsx'; + +interface DropdownProps { + children: React.ReactNode + className?: string +} + +interface DropdownItemProps { + label?: string + description?: string + icon?: React.FC + onClick: (...args: any[]) => any +} + +type DropdownComponent = React.FC & { + Button: React.FC + Divider: React.FC + Item: React.FC +} + +const Dropdown: DropdownComponent = (props) => { + const button = findByType(props.children, Dropdown.Button); + const menuContents = findByType(props.children, [Dropdown.Item, Dropdown.Divider]); + + return ( + + {button} + + {menuContents} + + + ); +}; + +Dropdown.Button = (props: MenuButtonProps) => { + return ( + + ); +}; +Dropdown.Button.displayName = 'Dropdown.Button' + +Dropdown.Divider = () => { + return ( +
+ ); +}; +Dropdown.Divider.displayName = 'Dropdown.Divider' + +Dropdown.Item = (props: DropdownItemProps) => { + return ( + +
+ + {props.icon && } + {props.label} + + {props.description &&

{props.description}

} +
+
+ ); +}; +Dropdown.Item.displayName = 'Dropdown.Item' + +export default Dropdown; diff --git a/packages/performant-ui/src/components/Input.tsx b/packages/performant-ui/src/components/Input.tsx new file mode 100644 index 000000000..c524b1b60 --- /dev/null +++ b/packages/performant-ui/src/components/Input.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { Description, Field, Input as HeadlessInput, Label } from '@headlessui/react'; +import clsx from 'clsx'; + +type InputType = 'text' | 'password' | 'email' | 'url' | 'tel' | 'search' | 'number' + +interface Props { + classes?: { + field?: string + input?: string + label?: string + description?: string + } + disabled?: boolean + error?: boolean + helperText?: string + iconLeft?: React.FC + iconRight?: React.FC + type?: InputType + label?: string + onChange: (val: string) => any + placeholder?: string + value: string +} + +const Input: React.FC = (props) => ( + + {props.label && ( + + )} +
+ {props.iconLeft && ( + + )} + props.onChange(e.target.value)} + type={props.type} + value={props.value} + /> + {props.iconRight && ( + + )} +
+ {props.helperText && ( + + {props.helperText} + + )} +
+); + +export default Input; \ No newline at end of file diff --git a/packages/performant-ui/src/components/Listbox.tsx b/packages/performant-ui/src/components/Listbox.tsx new file mode 100644 index 000000000..29c19b14f --- /dev/null +++ b/packages/performant-ui/src/components/Listbox.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Field, Listbox as HeadlessListbox, Label, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'; +import { HiCheck } from 'react-icons/hi'; +import { HiChevronUpDown } from 'react-icons/hi2'; +import clsx from 'clsx'; + +interface ListboxItem { + id: number | string, + label: string +} + +interface Props { + classes?: { + button?: string + label?: string + options?: string + option?: string + } + disabled?: boolean; + label?: string + options: ListboxItem[]; + onChange: (arg: ListboxItem) => any; + placeholder?: string + value?: ListboxItem; +} + +const Listbox: React.FC = (props) => ( + + {props.label && ( + + )} + + + {props.value + ? {props.value.label} + : {props.placeholder}} + + + {props.options.map((option) => ( + + +
{option.label}
+
+ ))} +
+
+
+); + +export default Listbox; \ No newline at end of file diff --git a/packages/performant-ui/src/components/MediaSelect.tsx b/packages/performant-ui/src/components/MediaSelect.tsx new file mode 100644 index 000000000..3fb8330d3 --- /dev/null +++ b/packages/performant-ui/src/components/MediaSelect.tsx @@ -0,0 +1,97 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { HiOutlineTrash, HiPhoto } from 'react-icons/hi2'; +import Button from './Button'; +import clsx from 'clsx'; + +interface Props { + accept?: string + description?: string + disabled?: boolean + fileDescription?: string + label?: string + imageUrl?: string + onChange: (arg: FileList) => void + onRemoveFile?: (index: number) => void +} + +const MediaSelect: React.FC = (props) => { + const [imageUrl, setImageUrl] = useState(props.imageUrl); + + const inputRef = useRef(null); + + const openDialog = () => inputRef.current?.click(); + + const onCleanup = useCallback((url: string) => { + if (url !== props.imageUrl) { + URL.revokeObjectURL(url); + } + }, [props.imageUrl]); + + const onChange = useCallback((e: React.ChangeEvent) => { + setImageUrl(prev => { + const newUrl = URL.createObjectURL(Array.from(e.target.files)[0]); + + if (prev) { + onCleanup(prev); + } + + return newUrl; + }); + + props.onChange(e.target.files); + }, [props.onChange, setImageUrl]); + + useEffect(() => { + return () => { + if (imageUrl) { + onCleanup(imageUrl); + } + }; + }, []); + + return ( + <> +
+ {props.label && ( + + {props.label} + + )} + +
+ + + ); +}; + +export default MediaSelect; diff --git a/packages/performant-ui/src/components/Navbar.tsx b/packages/performant-ui/src/components/Navbar.tsx new file mode 100644 index 000000000..ee99f218d --- /dev/null +++ b/packages/performant-ui/src/components/Navbar.tsx @@ -0,0 +1,116 @@ +import React, { useMemo } from 'react'; +import clsx from 'clsx'; +import { findByType } from '../helpers/Element'; + +interface NavbarProps { + children: React.ReactNode + className?: string + divider?: boolean; +} + +interface ChildrenProps { + children: React.ReactNode +} + +interface TabProps extends any { + active: boolean + as?: React.FC | string + href?: string + label: string + onClick?: string +} + +type NavbarComponent = React.FC & { + Controls: React.FC + Tab: React.FC + Tabs: React.FC + Logo: React.FC +} + +const Navbar: NavbarComponent = (props) => { + const logo = findByType(props.children, Navbar.Logo); + const tabs = findByType(props.children, Navbar.Tabs); + const controls = findByType(props.children, Navbar.Controls); + + return ( + + ); +}; + +Navbar.Logo = (props: ChildrenProps) => { + return ( +
+ {props.children} +
+ ); +}; +Navbar.Logo.displayName = 'Navbar.Logo' + +Navbar.Tabs = (props: ChildrenProps) => { + return ( +
+ {props.children} +
+ ); +}; +Navbar.Tabs.displayName = 'Navbar.Tabs' + +Navbar.Tab = (props: TabProps) => { + const WrapperComponent = useMemo(() => props.href ? 'a' : 'button', [props.href]); + + // for a11y, render as links when a URL is passed + const wrapperProps = useMemo(() => ({ + href: props.href, + onClick: props.onClick, + type: props.href ? undefined : 'button' + }), [props.href, props.onClick]); + + if (props.as) { + return ( + + ); + } + + return ( + + {props.label} + {props.active && ( +
+ )} + + ); +}; +Navbar.Tab.displayName = 'Navbar.Tab' + +Navbar.Controls = (props: ChildrenProps) => { + return ( +
+ {props.children} +
+ ); +}; +Navbar.Controls.displayName = 'Navbar.Controls' + +export default Navbar; diff --git a/packages/performant-ui/src/components/PageStats.tsx b/packages/performant-ui/src/components/PageStats.tsx new file mode 100644 index 000000000..8da605d98 --- /dev/null +++ b/packages/performant-ui/src/components/PageStats.tsx @@ -0,0 +1,25 @@ +import React, { useMemo } from 'react' + +interface Props { + perPage: number, + page: number, + itemCount: number +} + +const PageStats: React.FC = (props) => { + const start = (props.perPage * (props.page - 1)) + 1 + const end = Math.min(start + props.perPage - 1, props.itemCount) + + return ( +

+ Showing  + {start} +  to  + {end} +  of  + {props.itemCount} +

+ ); +} + +export default PageStats diff --git a/packages/performant-ui/src/components/Pagination.tsx b/packages/performant-ui/src/components/Pagination.tsx new file mode 100644 index 000000000..2d5e9df84 --- /dev/null +++ b/packages/performant-ui/src/components/Pagination.tsx @@ -0,0 +1,99 @@ +import React, { useCallback, useMemo } from 'react'; +import Button from './Button'; +import { HiArrowLongLeft, HiArrowLongRight, HiChevronLeft, HiChevronRight } from 'react-icons/hi2'; +import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; + +interface Props { + className?: string + current: number + onChange: (page: number) => void + pageCount: number +} + +// How many buttons to render +const PAGE_BUTTON_COUNT = 7 + +// Max number of items to show on the edges +const MAX_ITEMS_ON_EDGES = 3 + +const getPageBatches = (count: number, current: number) => { + if (count <= PAGE_BUTTON_COUNT) { + const arr = [] + for (let i = 1; i <= count; i++) { + arr.push(i) + } + return arr; + } + + if (current > MAX_ITEMS_ON_EDGES && current <= count - MAX_ITEMS_ON_EDGES) { + return [1, null, current - 1, current, current + 1, null, count] + } + + if (current > count - MAX_ITEMS_ON_EDGES) { + return [1, 2, 3, null, count - 2, count - 1, count]; + } + + if (current <= MAX_ITEMS_ON_EDGES) { + return [1, 2, 3, null, count - 2, count - 1, count] + } + + return []; +}; + +const Pagination: React.FC = (props) => { + const batches = useMemo( + () => getPageBatches(props.pageCount, props.current), + [props.pageCount, props.current] + ); + + const onNext = useCallback(() => props.onChange(props.current + 1), [props.current]) + const onPrevious = useCallback(() => props.onChange(props.current - 1), [props.current]) + + return ( +
+
+ + {batches.map(pg => { + if (pg) { + return ( + + ) + } else { + return ( + + ) + } + })} + +
+
+ ); +}; + +export default Pagination; \ No newline at end of file diff --git a/packages/performant-ui/src/components/RadioGroup.tsx b/packages/performant-ui/src/components/RadioGroup.tsx new file mode 100644 index 000000000..b8d86edf3 --- /dev/null +++ b/packages/performant-ui/src/components/RadioGroup.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { Radio, RadioGroup as HeadlessRadioGroup, Label, Description, Field } from '@headlessui/react'; +import clsx from 'clsx'; + +interface RadioOption { + id: string | number + label: string, + description?: string +} + +interface Props { + classes?: { + root?: string, + field?: string, + button?: string, + label?: string, + description?: string + } + disabled?: boolean + onChange: (arg: RadioOption) => any + options?: RadioOption[] + value: RadioOption +} + +const RadioGroup: React.FC = (props) => { + return ( + + {props.options?.map(opt => ( + + + +
+ + {opt.description && ( + + {opt.description} + + )} +
+
+
+ ))} +
+ ); +}; + +interface RadioIconProps { + className?: string + selected?: boolean +} + +const RadioIcon = (props: RadioIconProps) => ( +
+ {props.selected && ( +
+ )} +
+); + +export default RadioGroup; diff --git a/packages/performant-ui/src/components/Switch.tsx b/packages/performant-ui/src/components/Switch.tsx new file mode 100644 index 000000000..8809e6c85 --- /dev/null +++ b/packages/performant-ui/src/components/Switch.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import clsx from 'clsx'; +import { Field, Label, Switch as HeadlessSwitch, Description } from '@headlessui/react'; + +interface Props { + classes?: { + description?: string, + label?: string, + root?: string, + switch?: string, + } + description?: string + onChange: (arg: boolean) => any + label: string + side?: 'left' | 'right' + value: boolean +} + +const Switch: React.FC = (props) => { + const side = props.side || 'left'; + + return ( + + {side === 'right' && ( + + )} + + + + {side === 'left' && ( + + )} + + ); +}; + +const SwitchText = (props: Props) => ( +
+ + {props.description && ( + + {props.description} + + )} +
+); + +export default Switch; diff --git a/packages/performant-ui/src/components/Table.tsx b/packages/performant-ui/src/components/Table.tsx new file mode 100644 index 000000000..35f510477 --- /dev/null +++ b/packages/performant-ui/src/components/Table.tsx @@ -0,0 +1,111 @@ +import React from 'react' +import clsx from 'clsx'; + +interface TableProps { + children?: React.ReactNode + classes?: { + container?: string, + header?: string, + table?: string + } + label?: string +} + +interface ChildElementProps { + children?: React.ReactNode + className?: string +} + +type TableComponent = React.FC & { + Body: React.FC + Cell: React.FC + Row: React.FC + HeadCell: React.FC + Head: React.FC +} + +const Table: TableComponent = (props) => { + return ( +
+ {props.label && ( +

+ {props.label} +

+ )} + + {props.children} +
+
+ ) +} + +Table.Head = (props: ChildElementProps) => ( + + {props.children} + +) + +Table.HeadCell = (props: ChildElementProps) => ( + + {props.children} + +) + +Table.Row = (props: ChildElementProps) => ( + + {props.children} + +) + +Table.Cell = (props: ChildElementProps) => ( + + {props.children} + +) + +Table.Body = (props: ChildElementProps) => { + return ( + + {props.children} + + ) +} + +export default Table diff --git a/packages/performant-ui/src/components/TextArea.tsx b/packages/performant-ui/src/components/TextArea.tsx new file mode 100644 index 000000000..bf483ffd5 --- /dev/null +++ b/packages/performant-ui/src/components/TextArea.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { Description, Field, Label, Textarea } from '@headlessui/react'; +import clsx from 'clsx'; + +interface Props { + classes?: { + field?: string + textarea?: string + label?: string + description?: string + errorText?: string + } + disabled?: boolean + error?: boolean + label?: string + errorText?: string + helperText?: string + onChange: (val: string) => any + placeholder?: string + rows?: number + value?: string +} + +const TextArea: React.FC = (props) => { + return ( + + {props.label && ( + + )} + {props.helperText && ( + + {props.helperText} + + )} +