├── test ├── .gitkeep └── utils │ └── theme │ └── th.test.ts ├── patches └── .gitkeep ├── src ├── hooks │ ├── .gitkeep │ ├── useTheme.ts │ ├── index.ts │ ├── useHasMounted.ts │ ├── useZodForm.ts │ ├── useColorMode.ts │ ├── breakpoints.ts │ ├── useHoverPopoverState.ts │ └── useColorScheme.ts ├── @types │ ├── index.d.ts │ ├── react-no-ssr │ │ └── index.d.ts │ ├── @emotion │ │ └── react │ │ │ └── index.d.ts │ └── csstype │ │ └── index.d.ts ├── components │ ├── badge │ │ ├── index.ts │ │ ├── Badge.stories.tsx │ │ └── Badge.tsx │ ├── spinner │ │ ├── index.ts │ │ ├── Spinner.stories.tsx │ │ └── Spinner.tsx │ ├── avatar │ │ ├── index.ts │ │ ├── Avatar.stories.tsx │ │ └── Avatar.tsx │ ├── input │ │ ├── index.ts │ │ ├── Input.stories.tsx │ │ ├── InputGroup.stories.tsx │ │ └── Input.tsx │ ├── color-mode-provider │ │ ├── index.ts │ │ ├── context.ts │ │ └── ColorModeProvider.tsx │ ├── icon │ │ ├── index.ts │ │ ├── Icon.tsx │ │ └── Icon.stories.tsx │ ├── reveal │ │ ├── index.ts │ │ ├── Reveal.stories.tsx │ │ └── Reveal.tsx │ ├── typography │ │ ├── index.ts │ │ ├── Text.tsx │ │ └── Heading.tsx │ ├── tooltip │ │ ├── index.ts │ │ └── Tooltip.stories.tsx │ ├── avatar-group │ │ ├── index.ts │ │ ├── AvatarGroup.stories.tsx │ │ └── AvatarGroup.tsx │ ├── drawer │ │ ├── index.ts │ │ ├── DrawerBody.tsx │ │ ├── DrawerFooter.tsx │ │ ├── DrawerHeader.tsx │ │ ├── Drawer.tsx │ │ └── Drawer.utils.ts │ ├── form │ │ ├── index.ts │ │ ├── FormControl.context.ts │ │ ├── SubmitButton.tsx │ │ ├── Form.tsx │ │ ├── FormControl.stories.tsx │ │ ├── ErrorMessage.tsx │ │ ├── FormControl.tsx │ │ └── Form.stories.tsx │ ├── conditional-wrapper │ │ ├── index.ts │ │ ├── ConditionalWrapper.tsx │ │ └── ConditionalWrapper.stories.mdx │ ├── list │ │ ├── index.ts │ │ ├── ListItem.tsx │ │ ├── List.tsx │ │ └── List.stories.tsx │ ├── button │ │ ├── index.ts │ │ ├── IconButton.stories.tsx │ │ ├── IconButton.tsx │ │ ├── Button.stories.tsx │ │ └── Button.tsx │ ├── menu │ │ ├── common.ts │ │ ├── MenuDivider.tsx │ │ ├── index.ts │ │ ├── MenuOptionGroup.context.ts │ │ ├── MenuButton.tsx │ │ ├── Menu.context.ts │ │ ├── MenuOptionItem.tsx │ │ ├── MenuOptionGroup.tsx │ │ ├── MenuList.tsx │ │ ├── MenuListItem.tsx │ │ ├── Menu.tsx │ │ └── Menu.stories.tsx │ ├── primitives │ │ └── index.ts │ ├── modal │ │ ├── index.ts │ │ ├── ModalBody.tsx │ │ ├── ModalFooter.tsx │ │ ├── context.ts │ │ ├── ModalHeader.tsx │ │ └── Modal.stories.tsx │ ├── toast │ │ ├── index.ts │ │ ├── Toast.stories.tsx │ │ └── ToastContainer.tsx │ ├── popover │ │ ├── index.ts │ │ ├── PopoverBody.tsx │ │ ├── PopoverFooter.tsx │ │ ├── context.ts │ │ ├── PopoverHeader.tsx │ │ └── Popover.stories.tsx │ ├── types.ts │ ├── layout │ │ ├── index.ts │ │ ├── HStack.tsx │ │ ├── VStack.tsx │ │ ├── Flex.stories.tsx │ │ ├── VStack.stories.tsx │ │ ├── HStack.stories.tsx │ │ ├── Grid.stories.tsx │ │ ├── Grid.tsx │ │ └── Flex.tsx │ ├── tabs │ │ ├── TabPanels.tsx │ │ ├── index.ts │ │ ├── TabPanel.tsx │ │ ├── Tabs.context.ts │ │ ├── Tabs.stories.tsx │ │ ├── Tabs.tsx │ │ ├── TabList.tsx │ │ └── Tab.tsx │ └── index.ts ├── utils │ ├── index.ts │ ├── color.ts │ ├── regex.ts │ ├── storybook.ts │ ├── string.ts │ ├── emitter.ts │ ├── jsx.tsx │ ├── motion.ts │ ├── theme │ │ └── th.ts │ ├── css-vars.ts │ └── styled-system │ │ └── transforms.ts ├── styles │ ├── modules │ │ ├── variables.ts │ │ ├── mixins.ts │ │ └── keyframes.ts │ ├── GlobalStyles.tsx │ └── theme │ │ ├── modes.ts │ │ └── typography.ts ├── index.ts ├── stories │ ├── Typography.stories.tsx │ └── Colors.stories.tsx └── KleeProvider.tsx ├── .husky ├── .gitignore ├── pre-commit └── common.sh ├── example ├── .npmignore ├── index.html ├── tsconfig.json ├── index.tsx └── package.json ├── .github ├── screens │ └── autocomplete.jpg └── workflows │ ├── size.yml │ └── ci.yaml ├── .gitignore ├── .prettierrc ├── .eslintrc.js ├── .storybook ├── preview.js └── main.js ├── LICENSE ├── tsconfig.json ├── .npmignore └── package.json /test/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /patches/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hooks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /example/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist -------------------------------------------------------------------------------- /src/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare var __DEV__: boolean 2 | -------------------------------------------------------------------------------- /src/components/badge/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Badge' 2 | -------------------------------------------------------------------------------- /src/components/spinner/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Spinner' 2 | -------------------------------------------------------------------------------- /src/components/avatar/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Avatar } from './Avatar' 2 | -------------------------------------------------------------------------------- /src/components/input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Input' 2 | export * from './InputGroup' 3 | -------------------------------------------------------------------------------- /src/components/color-mode-provider/index.ts: -------------------------------------------------------------------------------- 1 | export { ColorModeProvider } from './ColorModeProvider' 2 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { CssVars, cssVar } from './css-vars' 2 | export { th } from './theme/th' 3 | -------------------------------------------------------------------------------- /.github/screens/autocomplete.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Liinkiing/klee/HEAD/.github/screens/autocomplete.jpg -------------------------------------------------------------------------------- /src/components/icon/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Icon } from './Icon' 2 | 3 | export type { IconProps } from './Icon' 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | 7 | storybook-static/ 8 | 9 | /.idea/ 10 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | . "$(dirname "$0")/common.sh" 4 | 5 | yarn lint-staged 6 | -------------------------------------------------------------------------------- /src/components/reveal/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Reveal } from './Reveal' 2 | 3 | export type { RevealProps } from './Reveal' 4 | -------------------------------------------------------------------------------- /src/components/typography/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Heading } from './Heading' 2 | export { default as Text } from './Text' 3 | -------------------------------------------------------------------------------- /src/components/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tooltip } from './Tooltip' 2 | 3 | export type { TooltipProps } from './Tooltip' 4 | -------------------------------------------------------------------------------- /src/components/avatar-group/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AvatarGroup } from './AvatarGroup' 2 | 3 | export type { AvatarGroupProps } from './AvatarGroup' 4 | -------------------------------------------------------------------------------- /src/components/drawer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Drawer' 2 | export * from './DrawerHeader' 3 | export * from './DrawerBody' 4 | export * from './DrawerFooter' 5 | -------------------------------------------------------------------------------- /src/components/form/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Form' 2 | export * from './SubmitButton' 3 | export * from './ErrorMessage' 4 | export * from './FormControl' 5 | -------------------------------------------------------------------------------- /src/components/conditional-wrapper/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ConditionalWrapper } from './ConditionalWrapper' 2 | 3 | export type { ConditionalWrapperProps } from './ConditionalWrapper' 4 | -------------------------------------------------------------------------------- /src/@types/react-no-ssr/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-no-ssr' { 2 | import { FC, ReactNode } from 'react' 3 | declare const NoSSR: FC<{ children: ReactNode }> 4 | 5 | export = NoSSR 6 | } 7 | -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { useTheme as useEmotionTheme } from '@emotion/react' 2 | 3 | import { KleeTheme } from '../styles/theme' 4 | 5 | export const useTheme = (): KleeTheme => useEmotionTheme() 6 | -------------------------------------------------------------------------------- /.husky/common.sh: -------------------------------------------------------------------------------- 1 | command_exists () { 2 | command -v "$1" >/dev/null 2>&1 3 | } 4 | 5 | # Workaround for Windows 10, Git Bash and Yarn 6 | if command_exists winpty && test -t 1; then 7 | exec < /dev/tty 8 | fi 9 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './theme' 2 | export * from './breakpoints' 3 | export * from './useColorMode' 4 | export * from './useTheme' 5 | export * from './useZodForm' 6 | export * from './useColorScheme' 7 | -------------------------------------------------------------------------------- /src/@types/@emotion/react/index.d.ts: -------------------------------------------------------------------------------- 1 | import '@emotion/react' 2 | 3 | import { KleeTheme } from '../../../styles/theme' 4 | 5 | declare module '@emotion/react' { 6 | export interface Theme extends KleeTheme {} 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/modules/variables.ts: -------------------------------------------------------------------------------- 1 | import { Breakpoints, breakpoints } from '../theme' 2 | 3 | export const BREAKPOINTS: Breakpoints = breakpoints 4 | 5 | export const ELASTIC_BEZIER = 'cubic-bezier(0.32, 2, 0.55, 0.27)' 6 | -------------------------------------------------------------------------------- /src/components/list/index.ts: -------------------------------------------------------------------------------- 1 | export { default as List } from './List' 2 | export { default as ListItem } from './ListItem' 3 | 4 | export type { ListProps } from './List' 5 | export type { ListItemProps } from './ListItem' 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "all", 4 | "endOfLine": "auto", 5 | "arrowParens": "avoid", 6 | "parser": "typescript", 7 | "singleQuote": true, 8 | "printWidth": 120, 9 | "tabWidth": 2 10 | } 11 | -------------------------------------------------------------------------------- /src/components/button/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Button } from './Button' 2 | export { default as IconButton } from './IconButton' 3 | 4 | export type { ButtonProps } from './Button' 5 | export type { IconButtonProps } from './IconButton' 6 | -------------------------------------------------------------------------------- /src/components/menu/common.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | export type RenderProps = (props: { readonly open: boolean }) => ReactNode 4 | 5 | export type CommonProps = { 6 | readonly children?: RenderProps | ReactNode 7 | } 8 | -------------------------------------------------------------------------------- /src/components/primitives/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Box } from './Box' 2 | 3 | export type { BoxProps, PolymorphicBox, PolymorphicComponent, BoxOwnProps, BoxPropsOf, MotionBoxPropsOf } from './Box' 4 | export { klee, kmotion } from './utils' 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": [ 3 | "react-app", 4 | "prettier/@typescript-eslint", 5 | "plugin:prettier/recommended" 6 | ], 7 | "settings": { 8 | "react": { 9 | "version": "detect" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/utils/color.ts: -------------------------------------------------------------------------------- 1 | import fontColorContrast from 'font-color-contrast' 2 | 3 | import colors from '../styles/modules/colors' 4 | 5 | export const colorContrast = (color: string): string => 6 | fontColorContrast(color) === '#ffffff' ? colors.white : colors.black 7 | -------------------------------------------------------------------------------- /src/utils/regex.ts: -------------------------------------------------------------------------------- 1 | export const WEBSITE_REGEX = 2 | /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/ 3 | -------------------------------------------------------------------------------- /src/utils/storybook.ts: -------------------------------------------------------------------------------- 1 | import * as FiIcons from 'react-icons/fi' 2 | 3 | export const ICON_CONTROL = { 4 | control: { 5 | type: 'select', 6 | options: Object.entries(FiIcons).map(([moduleName]) => moduleName), 7 | }, 8 | __ICONS: FiIcons as any, 9 | } as const 10 | -------------------------------------------------------------------------------- /src/hooks/useHasMounted.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | export const useHasMounted = (): boolean => { 4 | const [hasMounted, setHasMounted] = useState(false) 5 | useEffect(() => { 6 | setHasMounted(true) 7 | }, []) 8 | 9 | return hasMounted 10 | } 11 | -------------------------------------------------------------------------------- /src/components/modal/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Modal } from './Modal' 2 | export { default as ModalHeader } from './ModalHeader' 3 | export { default as ModalBody } from './ModalBody' 4 | export { default as ModalFooter } from './ModalFooter' 5 | 6 | export type { ModalProps } from './Modal' 7 | -------------------------------------------------------------------------------- /src/utils/string.ts: -------------------------------------------------------------------------------- 1 | export const uuid = (): string => { 2 | const _p8 = (s?: boolean): string => { 3 | const p = `${Math.random().toString(16)}000000000`.substr(2, 8) 4 | return s ? `-${p.substr(0, 4)}-${p.substr(4, 4)}` : p 5 | } 6 | return _p8() + _p8(true) + _p8(true) + _p8() 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/emitter.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | 3 | import { ToastProps } from '../components/toast/Toast' 4 | 5 | export enum UIEvents { 6 | ToastShow = 'toast-show', 7 | } 8 | 9 | type Events = { 10 | [UIEvents.ToastShow]: ToastProps 11 | } 12 | 13 | export const Emitter = mitt() 14 | -------------------------------------------------------------------------------- /src/components/toast/index.ts: -------------------------------------------------------------------------------- 1 | import { Emitter, UIEvents } from '../../utils/emitter' 2 | import { uuid } from '../../utils/string' 3 | import type { ToastProps } from './Toast' 4 | 5 | export const toast = (value: Omit): void => { 6 | Emitter.emit(UIEvents.ToastShow, { placement: 'top', ...value, id: uuid() }) 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/GlobalStyles.tsx: -------------------------------------------------------------------------------- 1 | import { Global } from '@emotion/react' 2 | import React from 'react' 3 | 4 | import { initializeCssVars } from '../utils/css-vars' 5 | 6 | export const GlobalStyles = () => ( 7 | 12 | ) 13 | 14 | export default GlobalStyles 15 | -------------------------------------------------------------------------------- /src/components/popover/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Popover } from './Popover' 2 | export { default as PopoverHeader } from './PopoverHeader' 3 | export { default as PopoverBody } from './PopoverBody' 4 | export { default as PopoverFooter } from './PopoverFooter' 5 | export { usePopover } from './context' 6 | 7 | export type { PopoverProps } from './Popover' 8 | -------------------------------------------------------------------------------- /src/components/color-mode-provider/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | export type AppColorScheme = 'light' | 'dark' 4 | export type Context = { 5 | readonly currentMode: AppColorScheme 6 | readonly changeCurrentMode: (newMode: AppColorScheme) => void 7 | } 8 | 9 | export const ColorModeContext = createContext(undefined) 10 | -------------------------------------------------------------------------------- /src/components/form/FormControl.context.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export interface Context { 4 | readonly id: string 5 | readonly helperId?: string 6 | } 7 | 8 | export const FormControlContext = React.createContext(null) 9 | 10 | export const useFormControl = (): Context | null => { 11 | return React.useContext(FormControlContext) 12 | } 13 | -------------------------------------------------------------------------------- /src/components/types.ts: -------------------------------------------------------------------------------- 1 | import { KleeTheme } from '../styles/theme' 2 | 3 | export interface ShowableOnCreate { 4 | /** 5 | * When `true`, the component will be initially visible 6 | */ 7 | readonly showOnCreate?: boolean 8 | } 9 | 10 | export interface ColorSchemable { 11 | /** 12 | * @default blue 13 | */ 14 | readonly colorScheme?: keyof KleeTheme['colors'] 15 | } 16 | -------------------------------------------------------------------------------- /src/components/layout/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Flex } from './Flex' 2 | export { default as Grid } from './Grid' 3 | export { default as VStack } from './VStack' 4 | export { default as HStack } from './HStack' 5 | 6 | export type { FlexProps } from './Flex' 7 | export type { GridProps } from './Grid' 8 | export type { VStackProps } from './VStack' 9 | export type { HStackProps } from './HStack' 10 | -------------------------------------------------------------------------------- /src/components/tabs/TabPanels.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | 3 | import { Box, BoxProps } from '../primitives/Box' 4 | 5 | export interface TabPanelsProps extends BoxProps {} 6 | 7 | export const TabPanels: FC = ({ children, ...props }) => { 8 | return {children} 9 | } 10 | 11 | TabPanels.displayName = 'Tab.Panels' 12 | 13 | export default TabPanels 14 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | skip_step: install 13 | build_script: size-limit:build 14 | github_token: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /src/components/drawer/DrawerBody.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | 3 | import { ModalBody } from '../modal' 4 | import { ModalBodyProps } from '../modal/ModalBody' 5 | 6 | export interface DrawerBodyProps extends ModalBodyProps {} 7 | 8 | export const DrawerBody: FC = ({ ...props }) => 9 | 10 | DrawerBody.displayName = 'Drawer.Body' 11 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/popover/PopoverBody.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | 3 | import { Flex } from '../layout' 4 | import { BoxProps } from '../primitives/Box' 5 | 6 | const PopoverBody: FC = ({ children, ...rest }) => { 7 | return ( 8 | 9 | {children} 10 | 11 | ) 12 | } 13 | 14 | PopoverBody.displayName = 'Popover.Body' 15 | 16 | export default PopoverBody 17 | -------------------------------------------------------------------------------- /src/hooks/useZodForm.ts: -------------------------------------------------------------------------------- 1 | import { zodResolver } from '@hookform/resolvers/zod' 2 | import { useForm, UseFormProps } from 'react-hook-form' 3 | import type { TypeOf, ZodSchema } from 'zod' 4 | 5 | type Options> = UseFormProps> & { 6 | schema: T 7 | } 8 | 9 | export const useZodForm = >({ schema, ...options }: Options) => { 10 | return useForm({ ...options, resolver: zodResolver(schema) }) 11 | } 12 | -------------------------------------------------------------------------------- /src/@types/csstype/index.d.ts: -------------------------------------------------------------------------------- 1 | import 'csstype' 2 | 3 | declare module 'csstype' { 4 | export interface Properties { 5 | '--klee-focus-border-color'?: any 6 | '--klee-invalid-focus-border-color'?: any 7 | '--klee-transform'?: any 8 | '--klee-translate-x'?: any 9 | '--klee-translate-y'?: any 10 | '--klee-rotate'?: any 11 | '--klee-scale-x'?: any 12 | '--klee-scale-y'?: any 13 | '--klee-skew-x'?: any 14 | '--klee-skew-y'?: any 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/popover/PopoverFooter.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | 3 | import { Flex } from '../layout' 4 | import { BoxProps } from '../primitives/Box' 5 | 6 | const PopoverFooter: FC = ({ children, ...rest }) => { 7 | return ( 8 | 9 | {children} 10 | 11 | ) 12 | } 13 | 14 | PopoverFooter.displayName = 'Popover.Footer' 15 | 16 | export default PopoverFooter 17 | -------------------------------------------------------------------------------- /src/components/modal/ModalBody.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | 3 | import { Flex } from '../layout' 4 | import { FlexProps } from '../layout/Flex' 5 | 6 | export interface ModalBodyProps extends FlexProps {} 7 | 8 | const ModalBody: FC = ({ children, ...rest }) => { 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | } 15 | 16 | ModalBody.displayName = 'Modal.Body' 17 | 18 | export default ModalBody 19 | -------------------------------------------------------------------------------- /src/components/conditional-wrapper/ConditionalWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactElement, ReactNode } from 'react' 2 | 3 | export interface ConditionalWrapperProps { 4 | readonly when: boolean 5 | readonly children: ReactNode 6 | readonly wrapper: (children: ReactNode) => ReactNode 7 | } 8 | 9 | export const ConditionalWrapper: FC = ({ children, when, wrapper }) => 10 | when === true ? (wrapper(children) as ReactElement) : (children as ReactElement) 11 | 12 | export default ConditionalWrapper 13 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": false, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "removeComments": true, 12 | "strictNullChecks": true, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "lib": ["es2015", "es2016", "dom"], 16 | "types": ["node"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/drawer/DrawerFooter.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | 3 | import { KleeBorder } from '../../styles/theme' 4 | import { ModalFooter } from '../modal' 5 | import { ModalFooterProps } from '../modal/ModalFooter' 6 | 7 | export interface DrawerFooterProps extends ModalFooterProps {} 8 | 9 | export const DrawerFooter: FC = ({ ...props }) => ( 10 | 11 | ) 12 | 13 | DrawerFooter.displayName = 'Drawer.Body' 14 | -------------------------------------------------------------------------------- /src/components/drawer/DrawerHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | 3 | import { KleeBorder } from '../../styles/theme' 4 | import { ModalHeader } from '../modal' 5 | import { ModalHeaderProps } from '../modal/ModalHeader' 6 | 7 | export interface DrawerHeaderProps extends ModalHeaderProps {} 8 | 9 | export const DrawerHeader: FC = ({ ...props }) => ( 10 | 11 | ) 12 | 13 | DrawerHeader.displayName = 'Drawer.Body' 14 | -------------------------------------------------------------------------------- /src/components/menu/MenuDivider.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | 3 | import Box, { BoxProps } from '../primitives/Box' 4 | 5 | const MenuDivider: FC = ({ ...props }) => { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | MenuDivider.displayName = 'Menu.Divider' 22 | 23 | export default MenuDivider 24 | -------------------------------------------------------------------------------- /src/components/layout/HStack.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, forwardRef } from 'react' 2 | 3 | import { PolymorphicComponent } from '../primitives/Box' 4 | import Flex, { FlexProps } from './Flex' 5 | 6 | export interface HStackProps extends FlexProps {} 7 | 8 | export const HStack: FC = forwardRef(({ children, ...props }, ref) => { 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | }) 15 | 16 | export default HStack as PolymorphicComponent 17 | -------------------------------------------------------------------------------- /src/components/modal/ModalFooter.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | 3 | import { Flex } from '../layout' 4 | import { FlexProps } from '../layout/Flex' 5 | 6 | export interface ModalFooterProps extends FlexProps {} 7 | 8 | const ModalFooter: FC = ({ children, ...rest }) => { 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | } 15 | 16 | ModalFooter.displayName = 'Modal.Footer' 17 | 18 | export default ModalFooter 19 | -------------------------------------------------------------------------------- /src/components/layout/VStack.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, forwardRef } from 'react' 2 | 3 | import { PolymorphicComponent } from '../primitives/Box' 4 | import Flex, { FlexProps } from './Flex' 5 | 6 | export interface VStackProps extends FlexProps {} 7 | 8 | export const VStack: FC = forwardRef(({ children, ...props }, ref) => { 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | }) 15 | 16 | export default VStack as PolymorphicComponent 17 | -------------------------------------------------------------------------------- /src/components/tabs/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tab } from './Tab' 2 | export { default as TabList } from './TabList' 3 | export { default as TabPanel } from './TabPanel' 4 | export { default as TabPanels } from './TabPanels' 5 | export { useTabs } from './Tabs.context' 6 | export { default as Tabs } from './Tabs' 7 | 8 | export type { TabProps } from './Tab' 9 | export type { TabListProps } from './TabList' 10 | export type { TabPanelProps } from './TabPanel' 11 | export type { TabPanelsProps } from './TabPanels' 12 | export type { TabsProps } from './Tabs' 13 | -------------------------------------------------------------------------------- /src/components/menu/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Menu } from './Menu' 2 | export { default as MenuButton } from './MenuButton' 3 | export { default as MenuList } from './MenuList' 4 | export { default as MenuListItem } from './MenuListItem' 5 | export { default as MenuDivider } from './MenuDivider' 6 | export { default as MenuOptionGroup } from './MenuOptionGroup' 7 | export { default as MenuOptionItem } from './MenuOptionItem' 8 | 9 | export type { MenuProps } from './Menu' 10 | export type { MenuListProps } from './MenuList' 11 | export type { MenuListItemProps } from './MenuListItem' 12 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import 'react-app-polyfill/ie11' 3 | import { createRoot } from 'react-dom/client' 4 | 5 | import { Button, Flex, KleeProvider } from '../.' 6 | 7 | const App = () => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | 20 | const root = createRoot(document.getElementById('root')!) 21 | 22 | root.render() 23 | -------------------------------------------------------------------------------- /src/components/tabs/TabPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { TabPanel as BaseTabPanel } from 'reakit/Tab' 3 | 4 | import { Box, BoxProps } from '../primitives/Box' 5 | import { useTabs } from './Tabs.context' 6 | 7 | export interface TabPanelProps extends BoxProps {} 8 | 9 | export const TabPanel: FC = ({ children, _focus, ...props }) => { 10 | const { tabs } = useTabs() 11 | return ( 12 | 13 | {children} 14 | 15 | ) 16 | } 17 | 18 | TabPanel.displayName = 'Tab.Panel' 19 | 20 | export default TabPanel 21 | -------------------------------------------------------------------------------- /src/components/list/ListItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, forwardRef } from 'react' 2 | 3 | import Box, { BoxProps } from '../primitives/Box' 4 | import Text from '../typography/Text' 5 | 6 | export interface ListItemProps extends BoxProps {} 7 | 8 | export const ListItem: FC = forwardRef(({ children, ...props }, ref) => { 9 | return ( 10 | 11 | {typeof children === 'string' ? {children} : children} 12 | 13 | ) 14 | }) 15 | 16 | ListItem.displayName = 'List.Item' 17 | 18 | export default ListItem 19 | -------------------------------------------------------------------------------- /src/components/menu/MenuOptionGroup.context.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export interface Context { 4 | readonly value?: string | string[] 5 | readonly onChange?: (newValue: any) => void 6 | readonly type: 'checkbox' | 'radio' 7 | } 8 | 9 | export const MenuOptionGroupContext = React.createContext({ 10 | type: 'checkbox', 11 | }) 12 | 13 | export const useMenuOptionGroup = (): Context => { 14 | const context = React.useContext(MenuOptionGroupContext) 15 | if (!context) { 16 | throw new Error(`You can't use the MenuOptionGroupContext outsides a MenuOptionGroup component.`) 17 | } 18 | return context 19 | } 20 | -------------------------------------------------------------------------------- /src/components/menu/MenuButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { MenuButton as ReakitMenuButton } from 'reakit/Menu' 3 | 4 | import { Button } from '../button' 5 | import { PolymorphicComponent } from '../primitives/Box' 6 | import { useMenu } from './Menu.context' 7 | import { CommonProps } from './common' 8 | 9 | const MenuButton = React.forwardRef(({ ...props }, ref) => { 10 | const { reakitMenu } = useMenu() 11 | return 12 | }) 13 | 14 | MenuButton.displayName = 'Menu.Button' 15 | 16 | export default MenuButton as PolymorphicComponent 17 | -------------------------------------------------------------------------------- /src/components/input/Input.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { Input, InputProps } from './Input' 5 | 6 | const meta: Meta = { 7 | title: 'Library/Forms/Input', 8 | component: Input, 9 | parameters: { 10 | controls: { expanded: true }, 11 | }, 12 | } 13 | 14 | export default meta 15 | 16 | const Template: Story = ({ isValid, ...args }) => { 17 | return 18 | } 19 | 20 | export const Default = Template.bind({}) 21 | 22 | Default.args = { 23 | isValid: true, 24 | placeholder: 'Klee', 25 | } 26 | -------------------------------------------------------------------------------- /src/components/form/SubmitButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { useFormContext } from 'react-hook-form' 3 | 4 | import { Button, ButtonProps } from '../button' 5 | 6 | interface SubmitButtonProps extends ButtonProps {} 7 | 8 | export const SubmitButton: FC = ({ children, ...props }) => { 9 | const form = useFormContext() 10 | if (!form) { 11 | throw new Error(' must be used within a
component') 12 | } 13 | 14 | const { isValid, isDirty, isSubmitting } = form.formState 15 | return ( 16 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/menu/Menu.context.ts: -------------------------------------------------------------------------------- 1 | import { TippyProps } from '@tippyjs/react/headless' 2 | import * as React from 'react' 3 | import type { MenuStateReturn } from 'reakit/Menu' 4 | 5 | export interface Context { 6 | readonly reakitMenu: MenuStateReturn 7 | readonly placement: TippyProps['placement'] 8 | readonly hideOnClickOutside: boolean 9 | readonly closeOnSelect: boolean 10 | } 11 | 12 | export const MenuContext = React.createContext(undefined) 13 | 14 | export const useMenu = (): Context => { 15 | const context = React.useContext(MenuContext) 16 | if (!context) { 17 | throw new Error(`You can't use the MenuContext outsides a Menu component.`) 18 | } 19 | return context 20 | } 21 | -------------------------------------------------------------------------------- /src/components/popover/context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export interface IPopoverContext { 4 | readonly hide: () => void 5 | readonly show: () => void 6 | readonly toggle: () => void 7 | readonly hideCloseButton?: boolean 8 | readonly visible: boolean 9 | } 10 | 11 | export const PopoverContext = React.createContext({ 12 | hide: () => {}, 13 | show: () => {}, 14 | toggle: () => {}, 15 | visible: false, 16 | hideCloseButton: false, 17 | }) 18 | 19 | export const usePopover = (): IPopoverContext => { 20 | const context = React.useContext(PopoverContext) 21 | if (!context) { 22 | throw new Error(`You can't use the ModalContext outsides a Popover component.`) 23 | } 24 | return context 25 | } 26 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import { Portal } from 'reakit/Portal' 2 | 3 | export * from './button' 4 | export * from './icon' 5 | export * from './layout' 6 | export * from './menu' 7 | export * from './primitives' 8 | export * from './typography' 9 | export * from './avatar' 10 | export * from './avatar-group' 11 | export * from './list' 12 | export * from './reveal' 13 | export * from './tooltip' 14 | export * from './popover' 15 | export * from './modal' 16 | export * from './toast' 17 | export * from './tabs' 18 | export * from './conditional-wrapper' 19 | export * from './input' 20 | export * from './form' 21 | export * from './drawer' 22 | export * from './badge' 23 | export * from './color-mode-provider' 24 | export * from './spinner' 25 | export { Portal } 26 | -------------------------------------------------------------------------------- /src/components/form/Form.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FieldValues, FormProvider, SubmitHandler, UseFormReturn } from 'react-hook-form' 3 | 4 | import { Box } from '../primitives' 5 | import { BoxPropsOf } from '../primitives/Box' 6 | 7 | export interface FormProps extends Omit, 'onSubmit'> { 8 | readonly form: UseFormReturn 9 | readonly onSubmit?: SubmitHandler 10 | } 11 | 12 | export const Form = ({ children, form, onSubmit, ...props }: FormProps) => { 13 | return ( 14 | 15 | 16 | {children} 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/modal/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export interface Context { 4 | readonly hide: () => void 5 | readonly show: () => void 6 | readonly toggle: () => void 7 | readonly hideCloseButton?: boolean 8 | readonly preventClose?: boolean 9 | readonly visible: boolean 10 | } 11 | 12 | export const ModalContext = React.createContext({ 13 | hide: () => {}, 14 | show: () => {}, 15 | toggle: () => {}, 16 | preventClose: false, 17 | visible: false, 18 | hideCloseButton: false, 19 | }) 20 | 21 | export const useModal = (): Context => { 22 | const context = React.useContext(ModalContext) 23 | if (!context) { 24 | throw new Error(`You can't use the ModalContext outsides a Modal component.`) 25 | } 26 | return context 27 | } 28 | -------------------------------------------------------------------------------- /src/components/layout/Flex.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { Text } from '../typography' 5 | import Flex, { FlexProps } from './Flex' 6 | 7 | const meta: Meta = { 8 | title: 'Library/Layout/Flex', 9 | component: Flex, 10 | parameters: { 11 | layout: 'centered', 12 | controls: { disable: true }, 13 | }, 14 | } 15 | 16 | export default meta 17 | 18 | export const Default: Story = () => ( 19 | 20 | Hello 21 | Everybody 22 | 23 | I am Klee 24 | I am Fischl 25 | 26 | 27 | ) 28 | -------------------------------------------------------------------------------- /src/components/tabs/Tabs.context.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { useTabState } from 'reakit/Tab' 3 | 4 | export type TabsVariant = 'line' | 'rounded' 5 | export type TabsAlign = 'start' | 'center' | 'end' 6 | export type TabsOrientation = 'horizontal' | 'vertical' 7 | 8 | export type Context = { 9 | tabs: ReturnType 10 | colorScheme: string 11 | stretch: boolean 12 | align: TabsAlign 13 | variant: TabsVariant 14 | orientation: TabsOrientation 15 | } 16 | 17 | export const TabsContext = React.createContext(undefined) 18 | 19 | export const useTabs = (): Context => { 20 | const context = React.useContext(TabsContext) 21 | if (!context) { 22 | throw new Error(`You can't use the TabsContext outsides a Menu component.`) 23 | } 24 | return context 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/useColorMode.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext } from 'react' 2 | 3 | import { ColorModeContext } from '../components/color-mode-provider/context' 4 | 5 | export const useColorMode = () => { 6 | const context = useContext(ColorModeContext) 7 | if (!context) { 8 | throw new Error('You must wrap your application within a `ColorModeProvider` component to use the color mode!') 9 | } 10 | const { currentMode, changeCurrentMode } = context 11 | 12 | const toggleMode = useCallback(() => { 13 | if (currentMode === 'light') { 14 | changeCurrentMode('dark') 15 | } else { 16 | changeCurrentMode('light') 17 | } 18 | }, [currentMode, changeCurrentMode]) 19 | 20 | return { 21 | mode: currentMode, 22 | changeMode: changeCurrentMode, 23 | toggleMode: toggleMode, 24 | } as const 25 | } 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enums 3 | */ 4 | export { KleeBorder, KleeRadius, KleeShadow, KleeZIndex, KleeBreakpoint } from './styles/theme' 5 | export { KleeHeadingSize } from './components/typography/Heading' 6 | export { 7 | KleeFontWeight, 8 | KleeFontSize, 9 | KleeLetterSpacing, 10 | KleeLineHeight, 11 | KleeFontFamily, 12 | } from './styles/theme/typography' 13 | 14 | /** 15 | * Components 16 | */ 17 | export * from './components' 18 | 19 | /** 20 | * Types 21 | */ 22 | export { KleeTheme } from './styles/theme' 23 | 24 | /** 25 | * Utils 26 | */ 27 | export * from './utils' 28 | 29 | /** 30 | * Hooks 31 | */ 32 | export * from './hooks' 33 | 34 | /** 35 | * Theming 36 | */ 37 | export { extendTheme } from './styles/theme' 38 | 39 | /** 40 | * Provider 41 | */ 42 | export { KleeProvider } from './KleeProvider' 43 | -------------------------------------------------------------------------------- /src/components/button/IconButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { ICON_CONTROL } from '../../utils/storybook' 5 | import { Icon } from '../icon' 6 | import { IconButton, IconButtonProps } from './IconButton' 7 | 8 | const meta: Meta = { 9 | title: 'Library/IconButton', 10 | component: IconButton, 11 | argTypes: { 12 | icon: ICON_CONTROL, 13 | }, 14 | parameters: { 15 | controls: { expanded: true }, 16 | }, 17 | } 18 | 19 | export default meta 20 | 21 | const Template: Story & { icon: string }> = ({ icon, ...args }) => ( 22 | } {...args} /> 23 | ) 24 | 25 | export const Default = Template.bind({}) 26 | 27 | Default.args = { 28 | icon: 'FiCheckCircle', 29 | } 30 | -------------------------------------------------------------------------------- /src/components/spinner/Spinner.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { Spinner, SpinnerProps } from './Spinner' 5 | 6 | const meta: Meta = { 7 | title: 'Library/Spinner', 8 | component: Spinner, 9 | args: { 10 | size: 'lg', 11 | }, 12 | argTypes: { 13 | size: { 14 | control: { 15 | type: 'select', 16 | options: ['xs', 'sm', 'md', 'lg'], 17 | }, 18 | }, 19 | color: { 20 | control: { 21 | type: 'color', 22 | }, 23 | }, 24 | }, 25 | parameters: { 26 | controls: { expanded: true }, 27 | }, 28 | } 29 | 30 | export default meta 31 | 32 | const Template: Story = args => 33 | 34 | export const Default = Template.bind({}) 35 | 36 | Default.args = {} 37 | -------------------------------------------------------------------------------- /src/hooks/breakpoints.ts: -------------------------------------------------------------------------------- 1 | import { useMatchMedia } from '@liinkiing/react-hooks' 2 | 3 | import { breakpoints, KleeBreakpoint } from '../styles/theme' 4 | 5 | const isKleeBreakpoint = (breakpoint: KleeBreakpoint | string): breakpoint is KleeBreakpoint => { 6 | return Object.keys(breakpoints).includes(breakpoint as string) 7 | } 8 | 9 | export const useBreakpoint = (breakpoint: KleeBreakpoint | string): boolean => { 10 | const value = isKleeBreakpoint(breakpoint) ? `screen and (max-width: ${breakpoints[breakpoint]})` : breakpoint 11 | 12 | return useMatchMedia(value) 13 | } 14 | 15 | export const useIsMobile = (): boolean => useBreakpoint(KleeBreakpoint.Tablet) 16 | export const useIsTablet = (): boolean => useBreakpoint(KleeBreakpoint.Desktop) 17 | export const useIsDesktop = (): boolean => useBreakpoint(KleeBreakpoint.Wide) 18 | export const useIsWide = (): boolean => useBreakpoint(KleeBreakpoint.UltraWide) 19 | -------------------------------------------------------------------------------- /src/components/avatar/Avatar.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import Avatar, { AvatarProps } from './Avatar' 5 | 6 | const meta: Meta = { 7 | title: 'Library/Avatar', 8 | component: Avatar, 9 | parameters: { 10 | controls: { expanded: true }, 11 | }, 12 | } 13 | 14 | export default meta 15 | 16 | const Template: Story = args => 17 | 18 | export const Default = Template.bind({}) 19 | 20 | Default.args = { 21 | name: 'Omar Jbara', 22 | } 23 | 24 | export const WithPicture = Template.bind({}) 25 | 26 | WithPicture.args = { 27 | name: 'Mikasa Estucasa', 28 | src: 'https://risibank.fr/cache/stickers/d1261/126102-full.png', 29 | } 30 | 31 | export const Squared = Template.bind({}) 32 | 33 | Squared.args = { 34 | name: 'Mikasa Estucasa', 35 | src: 'https://risibank.fr/cache/stickers/d1261/126102-full.png', 36 | squared: true, 37 | } 38 | -------------------------------------------------------------------------------- /src/components/layout/VStack.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { Text } from '../typography' 5 | import VStack, { VStackProps } from './VStack' 6 | 7 | const meta: Meta = { 8 | title: 'Library/Layout/VStack', 9 | component: VStack, 10 | parameters: { 11 | layout: 'centered', 12 | controls: { disable: true }, 13 | }, 14 | } 15 | 16 | export default meta 17 | 18 | export const Default: Story = () => ( 19 | 20 | Hello 21 | Everybody 22 | 23 | 24 | I am {''} 25 | 26 | 27 | (basically, a {''} which render by default in column) 28 | 29 | 30 | 31 | ) 32 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "@emotion/react": "^11", 12 | "@emotion/styled": "^11", 13 | "@hookform/resolvers": "^2", 14 | "@styled-system/css": "^5", 15 | "framer-motion": "^4", 16 | "react-app-polyfill": "^1.0.0", 17 | "react-hook-form": "^7", 18 | "react-icons": "^4", 19 | "zod": "^3" 20 | }, 21 | "alias": { 22 | "react": "../node_modules/react", 23 | "react-dom": "../node_modules/react-dom/profiling", 24 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.12.10", 28 | "@types/react": "^16.9.11", 29 | "@types/react-dom": "^16.8.4", 30 | "parcel": "^1.12.3", 31 | "typescript": "^3.4.5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/layout/HStack.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { Text } from '../typography' 5 | import HStack, { HStackProps } from './HStack' 6 | 7 | const meta: Meta = { 8 | title: 'Library/Layout/HStack', 9 | component: HStack, 10 | parameters: { 11 | layout: 'centered', 12 | controls: { disable: true }, 13 | story: { expanded: true }, 14 | }, 15 | } 16 | 17 | export default meta 18 | 19 | export const Default: Story = () => ( 20 | 21 | Hello 22 | Everybody 23 | 24 | 25 | I am {''} 26 | 27 | 28 | (basically, a {''} which render by default in row) 29 | 30 | 31 | 32 | ) 33 | -------------------------------------------------------------------------------- /src/components/list/List.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FC, forwardRef } from 'react' 3 | 4 | import Box, { BoxProps } from '../primitives/Box' 5 | import ListItem from './ListItem' 6 | 7 | export interface ListProps extends BoxProps { 8 | readonly direction?: BoxProps['flexDirection'] 9 | } 10 | 11 | type SubComponents = { 12 | Item: typeof ListItem 13 | } 14 | 15 | export const List = forwardRef( 16 | ({ children, flexDirection, direction = 'column', ...props }, ref) => { 17 | return ( 18 | 29 | {children} 30 | 31 | ) 32 | }, 33 | ) 34 | 35 | List.displayName = 'List' 36 | ;(List as any).Item = ListItem 37 | 38 | export default List as unknown as FC & SubComponents 39 | -------------------------------------------------------------------------------- /src/utils/jsx.tsx: -------------------------------------------------------------------------------- 1 | import { Children, isValidElement, ReactElement, ReactNode } from 'react' 2 | 3 | export const hasProps = (jsx: ReactNode): jsx is ReactElement => Object.prototype.hasOwnProperty.call(jsx, 'props') 4 | 5 | export const jsxInnerText = (jsx: ReactNode): string => { 6 | if (jsx === null || typeof jsx === 'boolean' || typeof jsx === 'undefined') { 7 | return '' 8 | } 9 | 10 | if (typeof jsx === 'number') { 11 | return jsx.toString() 12 | } 13 | 14 | if (typeof jsx === 'string') { 15 | return jsx 16 | } 17 | 18 | if (Array.isArray(jsx)) { 19 | return jsx.reduce((acc, node) => acc + jsxInnerText(node), '') 20 | } 21 | 22 | if (hasProps(jsx) && Object.prototype.hasOwnProperty.call(jsx.props, 'children')) { 23 | return jsxInnerText(jsx.props.children) 24 | } 25 | 26 | return '' 27 | } 28 | 29 | export const cleanChildren = (children?: ReactNode) => { 30 | if (!children) return [] 31 | return Children.toArray(children).filter(child => isValidElement(child)) 32 | } 33 | -------------------------------------------------------------------------------- /src/components/reveal/Reveal.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { Button } from '../button' 5 | import Text from '../typography/Text' 6 | import type { RevealProps } from './Reveal' 7 | import Reveal from './Reveal' 8 | 9 | const meta: Meta = { 10 | title: 'Library/Reveal', 11 | component: Reveal, 12 | parameters: { 13 | chromatic: { disable: true }, 14 | controls: { expanded: true }, 15 | }, 16 | } 17 | 18 | export default meta 19 | 20 | export const Default: Story = args => ( 21 | 22 | Hello Klee 23 | 24 | ) 25 | 26 | export const WithMultipleChildren: Story = args => ( 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | 34 | WithMultipleChildren.args = { 35 | appear: 'from-left', 36 | staggerChildren: 0.1, 37 | } 38 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { themes } from '@storybook/theming' 2 | import { useDarkMode } from 'storybook-dark-mode' 3 | 4 | import KleeProvider from '../src/KleeProvider' 5 | import { kleeTheme } from '../src/styles/theme' 6 | 7 | export const parameters = { 8 | // https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args 9 | actions: { argTypesRegex: '^on.*' }, 10 | darkMode: { 11 | dark: { 12 | ...themes.dark, 13 | appBg: kleeTheme.modes.dark.menu.background, 14 | appContentBg: kleeTheme.modes.dark.background, 15 | barBg: kleeTheme.modes.dark.background, 16 | }, 17 | light: { 18 | ...themes.normal, 19 | }, 20 | }, 21 | } 22 | 23 | const ThemeWrapper = ({ children }) => { 24 | const isDarkMode = useDarkMode() 25 | return ( 26 | 27 | {children} 28 | 29 | ) 30 | } 31 | 32 | export const decorators = [ 33 | Story => ( 34 | 35 | 36 | 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Omar Jbara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/components/tabs/Tabs.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import Tabs from './Tabs' 5 | import { TabsProps } from './Tabs' 6 | 7 | const meta: Meta = { 8 | title: 'Library/Tabs', 9 | component: Tabs, 10 | argTypes: { 11 | children: { control: { disable: true }, table: { disable: true } }, 12 | }, 13 | parameters: { 14 | controls: { expanded: true }, 15 | }, 16 | } 17 | 18 | export default meta 19 | 20 | const Template: Story = args => ( 21 | 22 | 23 | Tab 1 24 | Tab 2 25 | Tab 3 26 | 27 | 28 | Tab 1 29 | Tab 2 30 | Tab 3 31 | 32 | 33 | ) 34 | 35 | export const Default = Template.bind({}) 36 | 37 | export const WithStretchedTabs = Template.bind({}) 38 | 39 | WithStretchedTabs.args = { 40 | stretch: true, 41 | } 42 | 43 | export const WithVerticalOrientation = Template.bind({}) 44 | 45 | WithVerticalOrientation.args = { 46 | orientation: 'vertical', 47 | } 48 | -------------------------------------------------------------------------------- /src/components/popover/PopoverHeader.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | 3 | /** @jsx jsx */ 4 | import { jsx } from '@emotion/react' 5 | import css from '@styled-system/css' 6 | import { FC } from 'react' 7 | import { FiX } from 'react-icons/fi' 8 | 9 | import { KleeFontSize } from '../../styles/theme/typography' 10 | import { IconButton } from '../button/IconButton' 11 | import { Icon } from '../icon' 12 | import { Flex } from '../layout' 13 | import { Box } from '../primitives' 14 | import { BoxProps } from '../primitives/Box' 15 | import { usePopover } from './context' 16 | 17 | const PopoverHeader: FC = ({ children, ...rest }) => { 18 | const { hide, hideCloseButton } = usePopover() 19 | return ( 20 | 21 | 30 | {children} 31 | 32 | {!hideCloseButton && } onClick={hide} />} 33 | 34 | ) 35 | } 36 | 37 | PopoverHeader.displayName = 'Popover.Header' 38 | 39 | export default PopoverHeader 40 | -------------------------------------------------------------------------------- /src/components/avatar-group/AvatarGroup.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { Avatar } from '../avatar' 5 | import AvatarGroup, { AvatarGroupProps } from './AvatarGroup' 6 | 7 | const meta: Meta = { 8 | title: 'Library/AvatarGroup', 9 | component: AvatarGroup, 10 | argTypes: { 11 | direction: { 12 | control: { 13 | type: 'select', 14 | options: ['row', 'column'], 15 | }, 16 | }, 17 | max: { 18 | control: { type: 'range', min: 1, max: 8, step: 1 }, 19 | }, 20 | }, 21 | parameters: { 22 | controls: { expanded: true }, 23 | }, 24 | } 25 | 26 | export default meta 27 | 28 | const Template: Story = args => ( 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | 41 | export const Default = Template.bind({}) 42 | 43 | Default.args = { 44 | max: 5, 45 | } 46 | -------------------------------------------------------------------------------- /src/components/list/List.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { Avatar } from '../avatar' 5 | import Text from '../typography/Text' 6 | import type { ListProps } from './List' 7 | import List from './List' 8 | 9 | const meta: Meta = { 10 | title: 'Library/List', 11 | component: List, 12 | argTypes: { 13 | direction: { 14 | control: { 15 | type: 'select', 16 | options: ['row', 'column', 'row-reverse', 'column-reverse'], 17 | }, 18 | }, 19 | }, 20 | parameters: { 21 | controls: { expanded: true }, 22 | }, 23 | } 24 | 25 | export default meta 26 | 27 | const Template: Story = args => ( 28 | 29 | 30 | 35 | Hello Klee 36 | 37 | 38 | 43 | Hello Klee 44 | 45 | 46 | ) 47 | 48 | export const Default = Template.bind({}) 49 | -------------------------------------------------------------------------------- /src/styles/modules/mixins.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react' 2 | 3 | import { CssVars } from '../../utils/css-vars' 4 | import colors from './colors' 5 | 6 | export const pxToRem = (px: number) => `${(px / 16).toFixed(3)}rem` 7 | 8 | export const BASE_FOCUS = { 9 | outline: 'none', 10 | boxShadow: `var(${CssVars.FocusBorderColor}) 0px 0px 0px 2px`, 11 | } 12 | 13 | export const linearGradient = (direction: 'vertical' | 'horizontal', color: string = colors.white) => css` 14 | linear-gradient(to ${direction === 'horizontal' ? 'right' : 'bottom'}, 15 | ${color} 0%, 16 | rgba(255, 255, 255, 0) 5%, 17 | rgba(255, 255, 255, 0) 95%, 18 | ${color} 100%); 19 | ` 20 | 21 | const SCROLLBAR_WIDTH = 16 22 | 23 | export const customScrollbar = css` 24 | ::-webkit-scrollbar { 25 | width: ${SCROLLBAR_WIDTH}px; 26 | } 27 | 28 | ::-webkit-scrollbar-track { 29 | box-shadow: inset 0 0 ${SCROLLBAR_WIDTH}px ${SCROLLBAR_WIDTH}px transparent; 30 | border: solid ${SCROLLBAR_WIDTH - 10}px transparent; 31 | } 32 | 33 | ::-webkit-scrollbar-thumb { 34 | box-shadow: inset 0 0 ${SCROLLBAR_WIDTH}px ${SCROLLBAR_WIDTH}px rgba(187, 187, 190, 0.38); 35 | border: solid ${SCROLLBAR_WIDTH - 10}px transparent; 36 | border-radius: ${SCROLLBAR_WIDTH}px; 37 | } 38 | 39 | ::-webkit-scrollbar-button { 40 | display: none; 41 | } 42 | ` 43 | -------------------------------------------------------------------------------- /src/components/form/FormControl.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | import { FiUser } from 'react-icons/fi' 4 | 5 | import { Icon } from '../icon' 6 | import { Input, InputGroup } from '../input' 7 | import { FormControl, FormControlProps } from './FormControl' 8 | 9 | const meta: Meta = { 10 | title: 'Library/Forms/FormControl', 11 | component: FormControl, 12 | parameters: { 13 | controls: { expanded: true }, 14 | }, 15 | } 16 | 17 | export default meta 18 | 19 | export const Default: Story = args => ( 20 | 21 | Username 22 | 23 | The username will be public 24 | 25 | ) 26 | 27 | Default.args = { 28 | id: 'username', 29 | } 30 | 31 | export const WithInputGroup: Story = args => ( 32 | 33 | Username 34 | 35 | 36 | 37 | 38 | 39 | 40 | The username will be public 41 | 42 | ) 43 | 44 | WithInputGroup.args = { 45 | id: 'username', 46 | } 47 | -------------------------------------------------------------------------------- /src/components/badge/Badge.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { KleeTheme } from '../../styles/theme' 5 | import { HStack } from '../layout' 6 | import { Badge, BadgeProps } from './Badge' 7 | 8 | const meta: Meta = { 9 | title: 'Library/Badge', 10 | component: Badge, 11 | parameters: { 12 | controls: { expanded: true }, 13 | }, 14 | } 15 | 16 | export default meta 17 | 18 | export const Default: Story = args => 19 | 20 | Default.args = { 21 | children: 'Hello Klee', 22 | } 23 | 24 | const colors: Array = [ 25 | 'rose', 26 | 'pink', 27 | 'fuchsia', 28 | 'purple', 29 | 'violet', 30 | 'indigo', 31 | 'blue', 32 | 'cyan', 33 | 'teal', 34 | 'emerald', 35 | 'green', 36 | 'lime', 37 | 'yellow', 38 | 'amber', 39 | 'orange', 40 | 'red', 41 | 'gray', 42 | 'light-blue', 43 | 'warm-gray', 44 | 'true-gray', 45 | 'cool-gray', 46 | 'blue-gray', 47 | ] 48 | 49 | export const AllColorScheme: Story = args => ( 50 | 51 | {colors.map(color => ( 52 | 53 | {color} 54 | 55 | ))} 56 | 57 | ) 58 | 59 | AllColorScheme.argTypes = { 60 | colorScheme: { table: { disable: true }, control: { disable: true } }, 61 | } 62 | -------------------------------------------------------------------------------- /src/components/color-mode-provider/ColorModeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useEffect } from 'react' 2 | import type { FC } from 'react' 3 | import { useMemo, useState } from 'react' 4 | 5 | import type { AppColorScheme, Context } from './context' 6 | import { ColorModeContext } from './context' 7 | 8 | const BODY_CLASS = 'klee-mode' 9 | 10 | interface Props { 11 | readonly children: ReactNode 12 | readonly defaultColorMode?: AppColorScheme 13 | } 14 | 15 | export const ColorModeProvider: FC = ({ defaultColorMode = 'light', children }) => { 16 | const [currentMode, setCurrentMode] = useState(defaultColorMode) 17 | const context = useMemo( 18 | () => ({ 19 | changeCurrentMode: setCurrentMode, 20 | currentMode, 21 | }), 22 | [currentMode], 23 | ) 24 | useEffect(() => { 25 | const clearClassnames = () => { 26 | classNames.forEach(className => { 27 | window.document.body.classList.remove(className) 28 | }) 29 | } 30 | const classNames = [`${BODY_CLASS}-light`, `${BODY_CLASS}-dark`] 31 | clearClassnames() 32 | window.document.body.classList.add(`${BODY_CLASS}-${currentMode}`) 33 | 34 | return () => { 35 | clearClassnames() 36 | } 37 | }, [currentMode]) 38 | 39 | return {children} 40 | } 41 | 42 | export default ColorModeProvider 43 | -------------------------------------------------------------------------------- /src/components/typography/Text.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | 3 | import { KleeFontFamily, KleeFontWeight, KleeLineHeight } from '../../styles/theme/typography' 4 | import { jsxInnerText } from '../../utils/jsx' 5 | import Box, { BoxOwnProps, PolymorphicComponent } from '../primitives/Box' 6 | 7 | type Props = BoxOwnProps & { 8 | readonly truncate?: number | boolean 9 | } 10 | 11 | const Text = forwardRef(({ children, truncate, sx, ...props }, ref) => { 12 | let content = children 13 | const innerText = jsxInnerText(content) 14 | if (truncate && typeof truncate === 'number' && innerText.length > truncate) { 15 | content = `${innerText.slice(0, truncate)}…` 16 | } 17 | return ( 18 | 37 | {content} 38 | 39 | ) 40 | }) 41 | 42 | Text.displayName = 'Text' 43 | 44 | export default Text as PolymorphicComponent 45 | -------------------------------------------------------------------------------- /src/components/drawer/Drawer.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | 3 | import { Modal, ModalProps } from '../modal' 4 | import { getAnimationPropsBasedOnPlacement, getPropsBasedOnPlacement } from './Drawer.utils' 5 | import { DrawerBody } from './DrawerBody' 6 | import { DrawerFooter } from './DrawerFooter' 7 | import { DrawerHeader } from './DrawerHeader' 8 | 9 | export type DrawerPlacement = 'top' | 'bottom' | 'right' | 'left' 10 | 11 | export interface DrawerProps extends Omit { 12 | readonly placement?: DrawerPlacement 13 | } 14 | 15 | type SubComponents = { 16 | Header: typeof DrawerHeader 17 | Body: typeof DrawerBody 18 | Footer: typeof DrawerFooter 19 | } 20 | 21 | export const Drawer: FC & SubComponents = ({ 22 | children, 23 | overlay, 24 | minWidth, 25 | width, 26 | placement = 'right', 27 | showOnCreate = false, 28 | ...props 29 | }) => { 30 | return ( 31 | 42 | {children} 43 | 44 | ) 45 | } 46 | 47 | Drawer.Header = DrawerHeader 48 | Drawer.Body = DrawerBody 49 | Drawer.Footer = DrawerFooter 50 | 51 | Drawer.displayName = 'Drawer' 52 | -------------------------------------------------------------------------------- /src/utils/motion.ts: -------------------------------------------------------------------------------- 1 | import { MotionProps } from 'framer-motion' 2 | 3 | export const LAYOUT_TRANSITION_SPRING = { 4 | type: 'spring', 5 | damping: 26, 6 | stiffness: 340, 7 | } 8 | 9 | export const MENU_TRANSITION_DURATION = 0.2 10 | 11 | export const ease = [0.48, 0.15, 0.25, 0.96] 12 | 13 | type CommonAnimation = 'SlideFromRight' | 'SlideFromLeft' | 'SlideFromTop' | 'SlideFromBottom' 14 | 15 | export const CommonAnimations: Record = { 16 | SlideFromBottom: { 17 | initial: { 18 | opacity: 0, 19 | y: 20, 20 | }, 21 | animate: { 22 | opacity: 1, 23 | y: 0, 24 | }, 25 | exit: { 26 | opacity: 0, 27 | y: 20, 28 | }, 29 | }, 30 | SlideFromTop: { 31 | initial: { 32 | opacity: 0, 33 | y: -20, 34 | }, 35 | animate: { 36 | opacity: 1, 37 | y: 0, 38 | }, 39 | exit: { 40 | opacity: 0, 41 | y: -20, 42 | }, 43 | }, 44 | SlideFromLeft: { 45 | initial: { 46 | opacity: 0, 47 | x: -20, 48 | }, 49 | animate: { 50 | opacity: 1, 51 | x: 0, 52 | }, 53 | exit: { 54 | opacity: 0, 55 | x: -20, 56 | }, 57 | }, 58 | SlideFromRight: { 59 | initial: { 60 | opacity: 0, 61 | x: 20, 62 | }, 63 | animate: { 64 | opacity: 1, 65 | x: 0, 66 | }, 67 | exit: { 68 | opacity: 0, 69 | x: 20, 70 | }, 71 | }, 72 | } 73 | -------------------------------------------------------------------------------- /src/components/conditional-wrapper/ConditionalWrapper.stories.mdx: -------------------------------------------------------------------------------- 1 | import ConditionalWrapper from './ConditionalWrapper' 2 | import Text from '../typography/Text' 3 | import VStack from '../layout/VStack' 4 | import { Meta, Story } from '@storybook/addon-docs' 5 | 6 | 7 | 8 | # ConditionalWrapper 9 | 10 | The `ConditionalWrapper` component allows you to wrap anything only `when` props is true. Otherwise, it will directly render the children. 11 | 12 | Here is how it is rendered `when={1 + 1 === 2}` 13 | 14 | 15 | 16 | 17 | I am the wrapper 18 | {children} 19 | 20 | }> 21 | I am the children 22 | 23 | 24 | 25 | Here is how it is rendered `when={1 + 1 === 10}` 26 | 27 | 28 | 29 | 30 | I am the wrapper 31 | {children} 32 | 33 | }> 34 | I am the children 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/components/button/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | import React, { FC, forwardRef, ReactElement } from 'react' 3 | import { variant as systemVariant } from 'styled-system' 4 | 5 | import { KleeFontSize } from '../../styles/theme/typography' 6 | import Button, { ButtonProps } from './Button' 7 | 8 | type VariantSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' 9 | export interface IconButtonProps extends Omit { 10 | icon: ReactElement 11 | variantSize?: VariantSize 12 | } 13 | 14 | const IconButtonInner = styled(Button)( 15 | systemVariant<{}, NonNullable>({ 16 | prop: 'variantSize', 17 | variants: { 18 | xs: { 19 | p: 1, 20 | fontSize: KleeFontSize.Xs, 21 | }, 22 | sm: { 23 | p: 1, 24 | fontSize: KleeFontSize.Sm, 25 | }, 26 | md: { 27 | p: 2, 28 | fontSize: KleeFontSize.Md, 29 | }, 30 | lg: { 31 | p: 3, 32 | fontSize: KleeFontSize.Xl2, 33 | }, 34 | xl: { 35 | p: 4, 36 | fontSize: KleeFontSize.Xl5, 37 | }, 38 | }, 39 | }), 40 | ) 41 | 42 | export const IconButton: FC = forwardRef( 43 | ({ children, icon, variant = 'primary', variantSize = 'md', ...props }, ref) => { 44 | return ( 45 | 46 | {icon} 47 | 48 | ) 49 | }, 50 | ) 51 | 52 | export default IconButton 53 | -------------------------------------------------------------------------------- /src/components/modal/ModalHeader.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | 3 | /** @jsx jsx */ 4 | import { jsx } from '@emotion/react' 5 | import css from '@styled-system/css' 6 | import { FC } from 'react' 7 | import { FiX } from 'react-icons/fi' 8 | 9 | import { KleeFontSize } from '../../styles/theme/typography' 10 | import { IconButton } from '../button/IconButton' 11 | import { Icon } from '../icon' 12 | import { Flex } from '../layout' 13 | import { FlexProps } from '../layout/Flex' 14 | import { Box } from '../primitives' 15 | import { Heading } from '../typography' 16 | import { useModal } from './context' 17 | 18 | export interface ModalHeaderProps extends FlexProps {} 19 | 20 | const ModalHeader: FC = ({ children, ...rest }) => { 21 | const { hide, preventClose, hideCloseButton } = useModal() 22 | return ( 23 | 24 | 33 | {typeof children === 'string' ? {children} : children} 34 | 35 | {!hideCloseButton && ( 36 | } 41 | onClick={hide} 42 | /> 43 | )} 44 | 45 | ) 46 | } 47 | 48 | ModalHeader.displayName = 'Modal.Header' 49 | 50 | export default ModalHeader 51 | -------------------------------------------------------------------------------- /src/hooks/useHoverPopoverState.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/reakit/reakit/pull/633, but added timeout in parameters, thx MY MEEEEEEN 2 | import { useCallback, useEffect, useRef } from 'react' 3 | import { PopoverInitialState, PopoverStateReturn, usePopoverState } from 'reakit/Popover' 4 | 5 | export const useHoverPopoverState = (initialState?: PopoverInitialState & { timeout?: number }): PopoverStateReturn => { 6 | const timeout = initialState?.timeout ?? 300 7 | const showTimeout = useRef(null) 8 | const hideTimeout = useRef(null) 9 | const popover = usePopoverState(initialState) 10 | const clearTimeouts = useCallback(() => { 11 | if (showTimeout.current !== null) { 12 | window.clearTimeout(showTimeout.current) 13 | } 14 | if (hideTimeout.current !== null) { 15 | window.clearTimeout(hideTimeout.current) 16 | } 17 | }, []) 18 | const show = useCallback(() => { 19 | clearTimeouts() 20 | showTimeout.current = window.setTimeout(() => { 21 | popover.show() 22 | }, timeout) 23 | 24 | // eslint-disable-next-line react-hooks/exhaustive-deps 25 | }, [clearTimeouts, popover.show]) 26 | const hide = useCallback(() => { 27 | clearTimeouts() 28 | hideTimeout.current = window.setTimeout(() => { 29 | popover.hide() 30 | }, timeout) 31 | 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | }, [clearTimeouts, popover.hide]) 34 | useEffect( 35 | () => () => { 36 | clearTimeouts() 37 | }, 38 | [clearTimeouts], 39 | ) 40 | return { 41 | ...popover, 42 | show, 43 | hide, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/theme/th.ts: -------------------------------------------------------------------------------- 1 | import get from 'lodash/get' 2 | 3 | import colors, { ThemeColorsValues } from '../../styles/modules/colors' 4 | import { BORDERS, RADII, SHADOWS, SPACING, Z_INDICES } from '../../styles/theme' 5 | import { FONT_FAMILIES, FONT_SIZES, FONT_WEIGHTS, LETTER_SPACINGS, LINE_HEIGHTS } from '../../styles/theme/typography' 6 | 7 | export const th = { 8 | border(value: T): typeof BORDERS[T] { 9 | return BORDERS[value] 10 | }, 11 | letterSpacing(value: T): typeof LETTER_SPACINGS[T] { 12 | return LETTER_SPACINGS[value] 13 | }, 14 | font(value: T): typeof FONT_FAMILIES[T] { 15 | return FONT_FAMILIES[value] 16 | }, 17 | fontSize(value: T): typeof FONT_SIZES[T] { 18 | return FONT_SIZES[value] 19 | }, 20 | fontWeight(value: T): typeof FONT_WEIGHTS[T] { 21 | return FONT_WEIGHTS[value] 22 | }, 23 | lineHeight(value: T): typeof LINE_HEIGHTS[T] { 24 | return LINE_HEIGHTS[value] 25 | }, 26 | radius(value: T): typeof RADII[T] { 27 | return RADII[value] 28 | }, 29 | shadow(value: T): typeof SHADOWS[T] { 30 | return SHADOWS[value] 31 | }, 32 | zIndex(value: T): typeof Z_INDICES[T] { 33 | return Z_INDICES[value] 34 | }, 35 | color(path: ThemeColorsValues, fallback?: string): string { 36 | return get(colors, path) ?? fallback 37 | }, 38 | space(value: T): typeof SPACING[T] { 39 | return SPACING[value] 40 | }, 41 | } as const 42 | -------------------------------------------------------------------------------- /src/hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | import { themeGet } from '@styled-system/theme-get' 2 | import { ResponsiveValue } from 'styled-system' 3 | 4 | import { BoxProps } from '../components' 5 | import { useTheme } from './useTheme' 6 | 7 | type Options = { colorScheme: string; shading?: number; fallback?: string } 8 | 9 | const isMultipleOptions = (options: Options | Options[]): options is Options[] => { 10 | return Array.isArray(options) 11 | } 12 | 13 | export const useColorScheme = (options: Options | Options[]): ResponsiveValue => { 14 | const theme = useTheme() 15 | if (isMultipleOptions(options)) { 16 | return options.map(({ colorScheme, fallback, shading }) => 17 | themeGet(`colors.${colorScheme}.${shading}`, fallback)({ theme }), 18 | ) 19 | } else { 20 | const { colorScheme, shading, fallback } = options 21 | return themeGet(`colors.${colorScheme}.${shading}`, fallback)({ theme }) 22 | } 23 | } 24 | 25 | export const useMultipleColorScheme = >>( 26 | mapping: T, 27 | ): Record> => { 28 | const theme = useTheme() 29 | let result: T = { ...mapping } 30 | for (const prop in mapping) { 31 | if (mapping.hasOwnProperty(prop)) { 32 | const index = prop as keyof BoxProps 33 | const value = mapping[index] 34 | if (!value) continue 35 | if (isMultipleOptions(value)) { 36 | result[index] = value.map(option => 37 | themeGet(`colors.${option.colorScheme}.${option.shading}`, option.fallback)({ theme }), 38 | ) 39 | } else { 40 | result[prop] = themeGet(`colors.${value.colorScheme}.${value.shading}`, value.fallback)({ theme }) 41 | } 42 | } 43 | } 44 | 45 | return result 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src"], 4 | "exclude": ["node_modules"], 5 | "compilerOptions": { 6 | "typeRoots": ["node_modules/@types/*", "src/@types/*"], 7 | "module": "esnext", 8 | "lib": ["dom", "esnext"], 9 | "importHelpers": true, 10 | // output .d.ts declaration files for consumers 11 | "declaration": true, 12 | // output .js.map sourcemap files for consumers 13 | "sourceMap": true, 14 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 15 | "rootDir": "./src", 16 | // stricter type-checking for stronger correctness. Recommended by TS 17 | "strict": true, 18 | // linter checks for common issues 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | // use Node's module resolution algorithm, instead of the legacy TS one 25 | "moduleResolution": "node", 26 | // transpile JSX to React.createElement 27 | "jsx": "react", 28 | // interop between ESM and CJS modules. Recommended by TS 29 | "esModuleInterop": true, 30 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 31 | "skipLibCheck": true, 32 | // error out if import and file system have a casing mismatch. Recommended by TS 33 | "forceConsistentCasingInFileNames": true, 34 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 35 | "noEmit": true, 36 | "types": ["@types/node", "@types/jest"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/button/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { ICON_CONTROL } from '../../utils/storybook' 5 | import { Icon } from '../icon' 6 | import { HStack } from '../layout' 7 | import Button, { ButtonProps } from './Button' 8 | 9 | const meta: Meta = { 10 | title: 'Library/Button', 11 | component: Button, 12 | argTypes: { 13 | startIcon: { table: { disable: true }, control: { disable: true } }, 14 | endIcon: { table: { disable: true }, control: { disable: true } }, 15 | }, 16 | args: { 17 | children: 'Join a course', 18 | }, 19 | parameters: { 20 | controls: { expanded: true }, 21 | }, 22 | } 23 | 24 | export default meta 25 | 26 | const Template: Story = args => }> 29 | {({ hide }) => ( 30 | <> 31 | 32 | Hello Klee 33 | 34 | 35 | How are you doing? 36 | 37 | 38 | 41 | 42 | 43 | )} 44 | 45 | ) 46 | 47 | export const Default = Template.bind({}) 48 | 49 | Default.args = { 50 | buttonText: 'Click me', 51 | hideCloseButton: true, 52 | ariaLabel: 'Popover example', 53 | } 54 | 55 | export const WithCustomization = Template.bind({}) 56 | 57 | WithCustomization.args = { 58 | buttonText: 'Click me', 59 | hideCloseButton: true, 60 | ariaLabel: 'Popover example', 61 | bg: 'cyan.600', 62 | color: 'white', 63 | minWidth: 400, 64 | } 65 | 66 | export const OnHover = Template.bind({}) 67 | 68 | OnHover.args = { 69 | ...WithCustomization.args, 70 | buttonText: 'Hover me', 71 | showOnCreate: false, 72 | trigger: 'hover', 73 | } 74 | -------------------------------------------------------------------------------- /test/utils/theme/th.test.ts: -------------------------------------------------------------------------------- 1 | import { th } from '../../../src' 2 | 3 | describe('utils/theme/th', () => { 4 | describe('color', () => { 5 | it('should return the correct value', () => { 6 | expect(th.color('amber.100')).toBe('#fef3c7') 7 | }) 8 | it('should return the fallback when value not found', () => { 9 | expect(th.color('not.found', '#000')).toBe('#000') 10 | }) 11 | }) 12 | describe('border', () => { 13 | it('should return the correct value', () => { 14 | expect(th.border('md')).toBe('4px solid') 15 | }) 16 | }) 17 | describe('letterSpacing', () => { 18 | it('should return the correct value', () => { 19 | expect(th.letterSpacing('widest')).toBe('0.1em') 20 | }) 21 | }) 22 | describe('font', () => { 23 | it('should return the correct value', () => { 24 | expect(th.font('mono')).toBe(`SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace`) 25 | }) 26 | }) 27 | describe('fontSize', () => { 28 | it('should return the correct value', () => { 29 | expect(th.fontSize('2xl')).toBe('1.5rem') 30 | }) 31 | }) 32 | describe('fontWeight', () => { 33 | it('should return the correct value', () => { 34 | expect(th.fontWeight('bold')).toBe(700) 35 | }) 36 | }) 37 | describe('lineHeight', () => { 38 | it('should return the correct value', () => { 39 | expect(th.lineHeight('none')).toBe('1') 40 | }) 41 | }) 42 | describe('radius', () => { 43 | it('should return the correct value', () => { 44 | expect(th.radius('2xl')).toBe('1rem') 45 | }) 46 | }) 47 | describe('shadow', () => { 48 | it('should return the correct value', () => { 49 | expect(th.shadow('base')).toBe('0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)') 50 | }) 51 | }) 52 | describe('zIndex', () => { 53 | it('should return the correct value', () => { 54 | expect(th.zIndex('modal')).toBe(1400) 55 | }) 56 | }) 57 | describe('space', () => { 58 | it('should return the correct value', () => { 59 | expect(th.space(8)).toBe('2rem') 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /src/components/menu/MenuOptionGroup.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | 3 | /** @jsx jsx */ 4 | import { jsx } from '@emotion/react' 5 | import css from '@styled-system/css' 6 | import { FC, useMemo } from 'react' 7 | 8 | import { KleeBorder } from '../../styles/theme' 9 | import { KleeFontSize, KleeFontWeight } from '../../styles/theme/typography' 10 | import Box, { BoxProps } from '../primitives/Box' 11 | import Text from '../typography/Text' 12 | import { Context, MenuOptionGroupContext } from './MenuOptionGroup.context' 13 | 14 | interface Props extends Omit { 15 | readonly title?: string 16 | readonly type: 'checkbox' | 'radio' 17 | readonly value?: string | string[] 18 | readonly onChange?: (newValue: any) => void 19 | } 20 | 21 | const MenuOptionGroup: FC = ({ children, title, value, onChange, type, ...props }) => { 22 | const context = useMemo( 23 | () => ({ 24 | onChange, 25 | value, 26 | type, 27 | }), 28 | [onChange, value, type], 29 | ) 30 | 31 | return ( 32 | 33 | 43 | {title && ( 44 | 57 | {title} 58 | 59 | )} 60 | {children} 61 | 62 | 63 | ) 64 | } 65 | 66 | MenuOptionGroup.displayName = 'Menu.OptionGroup' 67 | 68 | export default MenuOptionGroup 69 | -------------------------------------------------------------------------------- /src/components/typography/Heading.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, forwardRef } from 'react' 2 | import { ResponsiveValue } from 'styled-system' 3 | 4 | import { KleeFontSize } from '../../styles/theme/typography' 5 | import { jsxInnerText } from '../../utils/jsx' 6 | import Box, { BoxOwnProps } from '../primitives/Box' 7 | 8 | export enum KleeHeadingSize { 9 | Xl = 'xl', 10 | Lg = 'lg', 11 | Md = 'md', 12 | Sm = 'sm', 13 | Xs = 'xs', 14 | } 15 | 16 | type Size = 'xl' | 'lg' | 'md' | 'sm' | 'xs' 17 | 18 | const sizes: { [Key in Size]: ResponsiveValue } = { 19 | xl: [KleeFontSize.Xl3, null, KleeFontSize.Xl4], 20 | lg: [KleeFontSize.Xl, null, KleeFontSize.Xl2], 21 | md: KleeFontSize.Xl, 22 | sm: KleeFontSize.Md, 23 | xs: KleeFontSize.Sm, 24 | } 25 | 26 | type Props = Omit & { 27 | readonly as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' 28 | readonly size?: KleeHeadingSize | Size 29 | readonly truncate?: number | boolean 30 | } 31 | 32 | const Heading: FC = forwardRef( 33 | ({ children, truncate, size = 'xl', as = 'h2', sx, ...rest }, ref) => { 34 | let content = children 35 | const innerText = jsxInnerText(content) 36 | if (truncate && typeof truncate === 'number' && innerText.length > truncate) { 37 | content = `${innerText.slice(0, truncate)}…` 38 | } 39 | 40 | return ( 41 | 61 | {content} 62 | 63 | ) 64 | }, 65 | ) 66 | 67 | Heading.displayName = 'Heading' 68 | 69 | export default Heading 70 | -------------------------------------------------------------------------------- /src/components/toast/Toast.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { Button } from '../button' 5 | import { Flex } from '../layout' 6 | import { List } from '../list' 7 | import { Reveal } from '../reveal' 8 | import { Text } from '../typography' 9 | import { IToast, Toast } from './Toast' 10 | import { toast } from './index' 11 | 12 | const meta: Meta = { 13 | title: 'Library/Toast', 14 | component: Toast, 15 | argTypes: { 16 | id: { table: { disable: true }, control: { disable: true } }, 17 | onHide: { table: { disable: true }, control: { disable: true } }, 18 | placement: { 19 | defaultValue: 'bottom', 20 | }, 21 | duration: { 22 | defaultValue: null, 23 | table: { 24 | defaultValue: { 25 | summary: 'A computed value that corresponds to (total characters of the children * 100ms)', 26 | }, 27 | }, 28 | }, 29 | }, 30 | parameters: { 31 | controls: { expanded: true }, 32 | }, 33 | } 34 | 35 | export default meta 36 | 37 | const Template: Story> = args => ( 38 | 45 | ) 46 | 47 | export const Default = Template.bind({}) 48 | 49 | Default.args = { 50 | content: 'Hello I am a toast', 51 | } 52 | 53 | export const WithCustomContent = Template.bind({}) 54 | 55 | WithCustomContent.argTypes = { 56 | content: { table: { disable: true }, control: { disable: true } }, 57 | } 58 | 59 | WithCustomContent.args = { 60 | content: ( 61 | 62 | You can also use other components 63 | 64 | Like the 65 | the 66 | reveal 67 | component 68 | 69 | 70 | 🥵 71 | 72 | 73 | 74 | 75 | ), 76 | } 77 | -------------------------------------------------------------------------------- /src/components/tabs/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useMemo } from 'react' 2 | import { TabInitialState, useTabState } from 'reakit/Tab' 3 | 4 | import { KleeFontFamily } from '../../styles/theme/typography' 5 | import { Flex } from '../layout/Flex' 6 | import { BoxProps } from '../primitives/Box' 7 | import Tab from './Tab' 8 | import TabList from './TabList' 9 | import TabPanel from './TabPanel' 10 | import TabPanels from './TabPanels' 11 | import { Context, TabsAlign, TabsContext, TabsOrientation, TabsVariant } from './Tabs.context' 12 | 13 | export interface TabsProps extends Pick, Omit { 14 | readonly onChange?: (tabId: string) => void 15 | readonly stretch?: boolean 16 | readonly align?: TabsAlign 17 | readonly variant?: TabsVariant 18 | readonly orientation?: TabsOrientation 19 | readonly colorScheme?: string 20 | } 21 | 22 | type SubComponents = { 23 | List: typeof TabList 24 | Tab: typeof Tab 25 | Panels: typeof TabPanels 26 | Panel: typeof TabPanel 27 | } 28 | 29 | export const Tabs: FC & SubComponents = ({ 30 | children, 31 | selectedId, 32 | onChange, 33 | orientation = 'horizontal', 34 | align = 'start', 35 | stretch = false, 36 | colorScheme = 'blue', 37 | variant = 'line', 38 | ...props 39 | }) => { 40 | const tabs = useTabState({ 41 | selectedId, 42 | orientation, 43 | }) 44 | const context = useMemo( 45 | () => ({ tabs, colorScheme, variant, stretch, align, orientation }), 46 | [tabs, align, stretch, colorScheme, variant, orientation], 47 | ) 48 | 49 | useEffect(() => { 50 | if (tabs.selectedId) { 51 | onChange?.(tabs.selectedId) 52 | } 53 | }, [tabs.selectedId, onChange]) 54 | 55 | return ( 56 | 57 | 58 | {children} 59 | 60 | 61 | ) 62 | } 63 | 64 | Tabs.displayName = 'Tabs' 65 | 66 | Tabs.List = TabList 67 | Tabs.Tab = Tab 68 | Tabs.Panels = TabPanels 69 | Tabs.Panel = TabPanel 70 | 71 | export default Tabs 72 | -------------------------------------------------------------------------------- /src/utils/styled-system/transforms.ts: -------------------------------------------------------------------------------- 1 | import { themeGet } from '@styled-system/theme-get' 2 | import get from 'lodash/get' 3 | import { Scale } from 'styled-system' 4 | 5 | const globalSet = new Set(['none', '-moz-initial', 'inherit', 'initial', 'revert', 'unset']) 6 | 7 | const trimSpace = (str: string) => str.trim() 8 | 9 | export const bgClipTransform = (value: string | null | undefined) => { 10 | if (!value) return {} 11 | return value === 'text' ? { color: 'transparent', backgroundClip: 'text' } : { backgroundClip: value } 12 | } 13 | 14 | export const translateTransform = (value: string | number | null | undefined, scale?: Scale) => { 15 | if (!value) return 0 16 | if (!scale) { 17 | return value 18 | } 19 | if (typeof value === 'string' && value in scale) { 20 | return scale[value as any] 21 | } 22 | if (typeof value === 'string') { 23 | return value 24 | } 25 | const index = Math.abs(value) 26 | const computedValue = scale[index] 27 | if (!computedValue) { 28 | return `${value}px` 29 | } 30 | if (value < 0) { 31 | return typeof computedValue === 'string' ? `-${computedValue}` : -computedValue 32 | } 33 | 34 | return computedValue ?? value 35 | } 36 | 37 | // Taken from here and adapted it a bit for simplicity sake 38 | // https://github.com/chakra-ui/chakra-ui/blob/75edcf41e7ff4acc2569f2169949063c164d8f6e/packages/styled-system/src/utils/parse-gradient.ts 39 | // Again, check ChakraUI, a great library! 40 | export const bgGradientTransform = (value: string | null | undefined, colors?: Scale) => { 41 | if (value == null || globalSet.has(value)) return value 42 | const themeColor = get(colors, value) 43 | if (themeColor) return themeColor 44 | const regex = /(?^[a-z-A-Z]+)\((?(.*))\)/g 45 | const { type, values } = regex.exec(value)?.groups ?? {} 46 | if (!type || !values) return value 47 | 48 | const _type = type.includes('-gradient') ? type : `${type}-gradient` 49 | const [direction, ...stops] = values.split(',').map(trimSpace).filter(Boolean) 50 | if (stops?.length === 0) return value 51 | 52 | stops.unshift(direction) 53 | 54 | const _values = stops.map(stop => { 55 | const [_color, _stop] = stop.split(' ') 56 | 57 | const color = themeGet(`colors.${_color}`, _color)({ theme: { colors } }) 58 | return _stop ? [color, _stop].join(' ') : color 59 | }) 60 | 61 | return `${_type}(${_values.join(', ')})` 62 | } 63 | -------------------------------------------------------------------------------- /src/components/form/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | import get from 'lodash/get' 3 | import { AnimatePresence, AnimationProps, motion, MotionProps } from 'framer-motion' 4 | import React, { FC } from 'react' 5 | import { useFormContext } from 'react-hook-form' 6 | import { FiAlertTriangle } from 'react-icons/fi' 7 | 8 | import { KleeFontFamily, KleeFontSize, KleeFontWeight } from '../../styles/theme/typography' 9 | import { LAYOUT_TRANSITION_SPRING } from '../../utils/motion' 10 | import { Icon } from '../icon' 11 | import { Flex, FlexProps } from '../layout' 12 | 13 | export interface ErrorMessageProps 14 | extends FlexProps, 15 | Pick, 16 | Pick { 17 | readonly name?: string 18 | } 19 | 20 | const ErrorMessageInner = styled(motion(Flex))() 21 | 22 | const COMMON_PROPS: Partial = { 23 | fontSize: KleeFontSize.Sm, 24 | fontFamily: KleeFontFamily.Body, 25 | align: 'center', 26 | fontWeight: KleeFontWeight.Semibold, 27 | color: 'red.500', 28 | role: 'alert', 29 | } 30 | 31 | const ErrorIcon = () => 32 | 33 | export const ErrorMessage: FC = ({ 34 | children, 35 | name, 36 | animate, 37 | initial, 38 | exit, 39 | transition, 40 | ...props 41 | }) => { 42 | const formContext = useFormContext() 43 | if (children) { 44 | return ( 45 | 46 | 47 | {children} 48 | 49 | ) 50 | } 51 | if (!name) return null 52 | if (!formContext || !formContext.formState) return null 53 | const { errors } = formContext.formState 54 | const hasFieldError = !!get(errors, `${name}.message`) 55 | return ( 56 | 57 | {hasFieldError && ( 58 | 67 | 68 | {get(errors, `${name}.message`)} 69 | 70 | )} 71 | 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /src/components/avatar-group/AvatarGroup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { FC, ReactElement } from 'react' 3 | 4 | import { KleeBorder } from '../../styles/theme' 5 | import { cleanChildren } from '../../utils/jsx' 6 | import Avatar, { AvatarProps, AvatarSize } from '../avatar/Avatar' 7 | import Flex, { FlexProps } from '../layout/Flex' 8 | import Box, { BoxProps } from '../primitives/Box' 9 | 10 | export interface AvatarGroupProps 11 | extends Omit, 12 | Pick, 13 | Pick { 14 | readonly max?: number 15 | } 16 | 17 | const getMarginForSize = (size: AvatarSize): number => { 18 | switch (size) { 19 | case 'xs': 20 | case 'sm': 21 | return -4 22 | case 'md': 23 | default: 24 | return -6 25 | case 'lg': 26 | return -10 27 | case 'xl': 28 | return -12 29 | } 30 | } 31 | 32 | const getBorderForSize = (size: AvatarSize): string => { 33 | switch (size) { 34 | case 'xs': 35 | return KleeBorder.Sm 36 | case 'sm': 37 | case 'md': 38 | case 'lg': 39 | default: 40 | return KleeBorder.Md 41 | case 'xl': 42 | return KleeBorder.Lg 43 | } 44 | } 45 | 46 | export const AvatarGroup: FC = ({ 47 | children, 48 | max, 49 | flexDirection, 50 | direction, 51 | size = 'md', 52 | squared = false, 53 | ...rest 54 | }) => { 55 | const validChildren = cleanChildren(children) 56 | const computedDirection = flexDirection ?? direction ?? 'row' 57 | const margins = { 58 | [computedDirection === 'row' ? 'mr' : 'mb']: getMarginForSize(size), 59 | } 60 | const computedMax = max ?? validChildren.length 61 | const count = validChildren.length - computedMax 62 | const commonProps = { 63 | size, 64 | squared, 65 | color: 'white', 66 | border: getBorderForSize(size), 67 | borderColor: 'background', 68 | } 69 | const renderAvatarChildren = validChildren.slice(0, computedMax).map((c, i) => ( 70 | 71 | {React.cloneElement(c as ReactElement, commonProps)} 72 | 73 | )) 74 | return ( 75 | 76 | {renderAvatarChildren} 77 | {count > 0 && ( 78 | 79 | +{count} 80 | 81 | )} 82 | 83 | ) 84 | } 85 | 86 | export default AvatarGroup 87 | -------------------------------------------------------------------------------- /src/components/drawer/Drawer.utils.ts: -------------------------------------------------------------------------------- 1 | import { MotionProps } from 'framer-motion' 2 | 3 | import { KleeRadius } from '../../styles/theme' 4 | import { CommonAnimations } from '../../utils/motion' 5 | import { DrawerPlacement, DrawerProps } from './Drawer' 6 | 7 | export const getPropsBasedOnPlacement = ( 8 | placement: DrawerPlacement, 9 | dimensions: Pick, 10 | ): Partial => { 11 | const X_MIN_WIDTH = '240px' 12 | switch (placement) { 13 | case 'top': 14 | return { 15 | m: 0, 16 | width: '100%', 17 | height: 'auto', 18 | maxHeight: '90%', 19 | borderTopLeftRadius: [0, 0], 20 | borderTopRightRadius: [0, 0], 21 | borderBottomLeftRadius: KleeRadius.Lg, 22 | borderBottomRightRadius: KleeRadius.Lg, 23 | } 24 | case 'bottom': 25 | return { 26 | mt: 'auto', 27 | width: '100%', 28 | height: 'auto', 29 | maxHeight: '90%', 30 | borderTopLeftRadius: KleeRadius.Lg, 31 | borderTopRightRadius: KleeRadius.Lg, 32 | borderBottomLeftRadius: [0, 0], 33 | borderBottomRightRadius: [0, 0], 34 | } 35 | case 'right': 36 | return { 37 | m: 0, 38 | width: dimensions?.width ?? 'auto', 39 | minWidth: dimensions?.minWidth ?? X_MIN_WIDTH, 40 | height: '100%', 41 | alignSelf: 'flex-end', 42 | borderTopLeftRadius: KleeRadius.Lg, 43 | borderBottomLeftRadius: KleeRadius.Lg, 44 | borderTopRightRadius: [0, 0], 45 | borderBottomRightRadius: [0, 0], 46 | } 47 | case 'left': 48 | return { 49 | m: 0, 50 | width: dimensions?.width ?? 'auto', 51 | minWidth: dimensions?.minWidth ?? X_MIN_WIDTH, 52 | height: '100%', 53 | alignSelf: 'flex-start', 54 | borderTopRightRadius: KleeRadius.Lg, 55 | borderBottomRightRadius: KleeRadius.Lg, 56 | borderTopLeftRadius: [0, 0], 57 | borderBottomLeftRadius: [0, 0], 58 | } 59 | } 60 | } 61 | 62 | export const getAnimationPropsBasedOnPlacement = (placement: DrawerPlacement): Partial => { 63 | switch (placement) { 64 | case 'top': 65 | return CommonAnimations.SlideFromTop 66 | case 'bottom': 67 | return CommonAnimations.SlideFromBottom 68 | case 'right': 69 | return CommonAnimations.SlideFromRight 70 | case 'left': 71 | return CommonAnimations.SlideFromLeft 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/components/tabs/TabList.tsx: -------------------------------------------------------------------------------- 1 | import { LayoutGroup } from 'framer-motion' 2 | import React, { CSSProperties, FC } from 'react' 3 | import { TabList as BaseTabList } from 'reakit/Tab' 4 | import { variant } from 'styled-system' 5 | 6 | import { KleeBorder } from '../../styles/theme' 7 | import { Box, BoxProps } from '../primitives/Box' 8 | import { TabsAlign, TabsOrientation, TabsVariant, useTabs } from './Tabs.context' 9 | 10 | export interface TabListProps extends BoxProps { 11 | readonly ariaLabel: string 12 | } 13 | 14 | const variants = [ 15 | variant<{}, TabsVariant>({ 16 | variants: { 17 | line: { 18 | borderBottom: KleeBorder.Sm, 19 | borderRight: KleeBorder.Sm, 20 | borderBottomColor: 'tabs.list.borderColor', 21 | borderRightColor: 'tabs.list.borderColor', 22 | }, 23 | rounded: {}, 24 | }, 25 | }), 26 | variant<{}, TabsOrientation>({ 27 | prop: 'variantOrientation', 28 | variants: { 29 | horizontal: { 30 | mb: 3, 31 | borderRight: KleeBorder.None, 32 | flexDirection: 'row', 33 | }, 34 | vertical: { 35 | borderBottom: KleeBorder.None, 36 | mr: 3, 37 | flexDirection: 'column', 38 | }, 39 | }, 40 | }), 41 | ] 42 | const getAlign = (align: TabsAlign): CSSProperties['justifyContent'] => { 43 | switch (align) { 44 | default: 45 | case 'start': 46 | return 'flex-start' 47 | case 'center': 48 | return 'center' 49 | case 'end': 50 | return 'flex-end' 51 | } 52 | } 53 | 54 | export const TabList: FC = ({ children, ariaLabel, sx, ...props }) => { 55 | const { tabs, variant, stretch, align, orientation } = useTabs() 56 | return ( 57 | 58 | 79 | {children} 80 | 81 | 82 | ) 83 | } 84 | 85 | TabList.displayName = 'Tab.List' 86 | 87 | export default TabList 88 | -------------------------------------------------------------------------------- /src/KleeProvider.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | 3 | /** @jsx jsx */ 4 | import { jsx, ThemeProvider } from '@emotion/react' 5 | import merge from 'deepmerge' 6 | import { FC, ReactNode, useMemo } from 'react' 7 | import { Provider } from 'reakit/Provider' 8 | import invariant from 'tiny-invariant' 9 | 10 | import ColorModeProvider from './components/color-mode-provider/ColorModeProvider' 11 | import type { AppColorScheme } from './components/color-mode-provider/context' 12 | import { ToastsContainer } from './components/toast/ToastContainer' 13 | import { useColorMode } from './hooks' 14 | import { CSSReset } from './styles/CSSReset' 15 | import { GlobalFonts } from './styles/GlobalFonts' 16 | import GlobalStyles from './styles/GlobalStyles' 17 | import { KleeTheme, kleeTheme } from './styles/theme' 18 | 19 | interface Props { 20 | readonly children: ReactNode 21 | readonly resetCSS?: boolean 22 | readonly useNunitoFont?: boolean 23 | readonly defaultColorMode?: AppColorScheme 24 | readonly theme?: KleeTheme 25 | } 26 | 27 | type InnerProps = Required> 28 | 29 | const KleeProviderInner: FC = ({ resetCSS = true, useNunitoFont = true, theme = kleeTheme, children }) => { 30 | const { mode } = useColorMode() 31 | const appTheme = useMemo(() => { 32 | return merge.all([ 33 | theme, 34 | { 35 | currentMode: mode, 36 | colors: theme.modes[mode], 37 | }, 38 | ]) 39 | }, [theme, mode]) 40 | return ( 41 | 42 | {resetCSS && } 43 | 44 | {useNunitoFont && } 45 | 46 | {children} 47 | 48 | ) 49 | } 50 | 51 | export const KleeProvider: FC = ({ 52 | resetCSS = true, 53 | useNunitoFont = true, 54 | theme = kleeTheme, 55 | defaultColorMode = 'light', 56 | children, 57 | }) => { 58 | invariant( 59 | ['light', 'dark'].includes(defaultColorMode), 60 | 'The `defaultColorMode` must either be "light" or "dark", but you provided "' + defaultColorMode + '"', 61 | ) 62 | return ( 63 | 64 | 65 | 66 | {children} 67 | 68 | 69 | 70 | ) 71 | } 72 | 73 | export default KleeProvider 74 | -------------------------------------------------------------------------------- /src/components/spinner/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { keyframes } from '@emotion/react' 2 | import type { Properties } from 'csstype' 3 | import React from 'react' 4 | import type { FC } from 'react' 5 | import type { IconType } from 'react-icons' 6 | import { CgSpinnerAlt, CgSpinnerTwoAlt, CgSpinnerTwo, CgSpinner } from 'react-icons/cg' 7 | import { FaSpinner } from 'react-icons/fa' 8 | import { 9 | ImSpinner, 10 | ImSpinner2, 11 | ImSpinner3, 12 | ImSpinner4, 13 | ImSpinner5, 14 | ImSpinner6, 15 | ImSpinner7, 16 | ImSpinner8, 17 | ImSpinner9, 18 | ImSpinner10, 19 | ImSpinner11, 20 | } from 'react-icons/im' 21 | 22 | import { Icon, IconProps } from '../icon' 23 | 24 | type Variant = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12' | '13' | '14' | '15' | '16' 25 | 26 | export interface SpinnerProps extends Omit { 27 | readonly variant?: Variant 28 | /** 29 | * The animation duration, in seconds 30 | */ 31 | readonly duration?: number 32 | readonly easing?: Properties['animationTimingFunction'] 33 | } 34 | 35 | const spinning = keyframes` 36 | 0% { 37 | transform: rotate(0deg); 38 | } 39 | 100% { 40 | transform: rotate(359deg); 41 | } 42 | ` 43 | 44 | const getAs = (variant: Variant): IconType => { 45 | switch (variant) { 46 | case '1': 47 | return ImSpinner 48 | case '2': 49 | return ImSpinner2 50 | case '3': 51 | return ImSpinner3 52 | case '4': 53 | return ImSpinner4 54 | case '5': 55 | return ImSpinner5 56 | case '6': 57 | return ImSpinner6 58 | case '7': 59 | return ImSpinner7 60 | case '8': 61 | return ImSpinner8 62 | case '9': 63 | return ImSpinner9 64 | case '10': 65 | return ImSpinner10 66 | case '11': 67 | return ImSpinner11 68 | case '12': 69 | return FaSpinner 70 | case '13': 71 | return CgSpinnerAlt 72 | case '14': 73 | return CgSpinnerTwoAlt 74 | case '15': 75 | return CgSpinnerTwo 76 | case '16': 77 | return CgSpinner 78 | } 79 | } 80 | 81 | export const Spinner: FC = ({ variant = '2', duration = 2, easing = 'ease', ...props }) => { 82 | return ( 83 | 94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /src/stories/Colors.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { Flex, Grid, Heading, Text, VStack } from '../components' 5 | import colors from '../styles/modules/colors' 6 | import { KleeBorder, KleeShadow } from '../styles/theme' 7 | import { KleeFontWeight } from '../styles/theme/typography' 8 | import { colorContrast } from '../utils/color' 9 | 10 | const meta: Meta = { 11 | title: 'Tokens/Colors', 12 | parameters: { 13 | controls: { disable: true }, 14 | }, 15 | } 16 | 17 | export default meta 18 | 19 | const PALETTE_COLORS: Array = [ 20 | 'rose', 21 | 'pink', 22 | 'fuchsia', 23 | 'purple', 24 | 'violet', 25 | 'indigo', 26 | 'yellow', 27 | 'teal', 28 | 'blue', 29 | 'light-blue', 30 | 'cyan', 31 | 'emerald', 32 | 'green', 33 | 'lime', 34 | 'amber', 35 | 'orange', 36 | 'red', 37 | 'warm-gray', 38 | 'true-gray', 39 | 'gray', 40 | 'cool-gray', 41 | 'blue-gray', 42 | ] 43 | 44 | export const Default: Story = () => { 45 | const palettes = PALETTE_COLORS.map(color => ({ 46 | color, 47 | palette: Object.entries(colors[color]).map(([shade, hex]) => ({ shade, hex })), 48 | })) 49 | return ( 50 | 51 | {palettes.map(value => ( 52 | 53 | 58 | {value.color} 59 | 60 | 61 | {value.palette.map(p => ( 62 | 74 | {p.shade} 75 | 76 | ({p.hex}) 77 | 78 | 79 | ))} 80 | 81 | 82 | ))} 83 | 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /src/components/tooltip/Tooltip.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { KleeFontWeight } from '../../styles/theme/typography' 5 | import { Button } from '../button' 6 | import Flex from '../layout/Flex' 7 | import { List } from '../list' 8 | import Box from '../primitives/Box' 9 | import { Reveal } from '../reveal' 10 | import Text from '../typography/Text' 11 | import Tooltip from './Tooltip' 12 | import { TooltipProps } from './Tooltip' 13 | 14 | const meta: Meta = { 15 | title: 'Library/Tooltip', 16 | component: Tooltip, 17 | argTypes: { 18 | truncate: { table: { disable: true }, control: { disable: true } }, 19 | trigger: { 20 | control: { 21 | type: 'check', 22 | options: ['mouseenter', 'focus', 'click', 'focusin', 'manual'], 23 | }, 24 | }, 25 | }, 26 | args: { 27 | showOnCreate: true, 28 | label: 'Hello Klee', 29 | trigger: ['mouseenter', 'focus'], 30 | }, 31 | parameters: { 32 | controls: { expanded: true }, 33 | }, 34 | } 35 | 36 | export default meta 37 | 38 | const Template: Story = args => ( 39 | 40 | 41 | 42 | ) 43 | 44 | export const Default = Template.bind({}) 45 | 46 | export const WithCustomization = Template.bind({}) 47 | 48 | WithCustomization.args = { 49 | bg: 'amber.500', 50 | } 51 | 52 | export const WithHTMLContent = Template.bind({}) 53 | 54 | WithHTMLContent.argTypes = { 55 | label: { table: { disable: true }, control: { disable: true } }, 56 | } 57 | 58 | WithHTMLContent.args = { 59 | label: ( 60 | 61 | 62 | Hi I have some{' '} 63 | 64 | 65 | ✨ 66 | {' '} 67 | HTML{' '} 68 | 69 | ✨ 70 | 71 | 72 | 73 | 74 | 75 | hello 76 | hello 77 | hello 78 | 79 | 80 | world 81 | world 82 | world 83 | 84 | 85 | 86 | ), 87 | } 88 | -------------------------------------------------------------------------------- /src/components/menu/MenuList.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | import { motion } from 'framer-motion' 3 | import * as React from 'react' 4 | import { FC, forwardRef } from 'react' 5 | import { Menu } from 'reakit/Menu' 6 | 7 | import { useShadowModeValue } from '../../hooks' 8 | import { KleeBorder, KleeRadius, KleeShadow, KleeZIndex } from '../../styles/theme' 9 | import { KleeFontSize } from '../../styles/theme/typography' 10 | import { ease, MENU_TRANSITION_DURATION } from '../../utils/motion' 11 | import Flex from '../layout/Flex' 12 | import { Box } from '../primitives' 13 | import { BoxProps } from '../primitives/Box' 14 | import { useMenu } from './Menu.context' 15 | import { CommonProps } from './common' 16 | 17 | export type MenuListProps = BoxProps & 18 | CommonProps & 19 | ( 20 | | { 21 | ariaLabel: string 22 | ariaLabelledby?: never 23 | } 24 | | { 25 | ariaLabel?: never 26 | ariaLabelledby: string 27 | } 28 | ) 29 | 30 | const MenuItems = styled(motion(Flex))` 31 | &:active, 32 | &:focus { 33 | outline: none; 34 | } 35 | ` 36 | 37 | const MenuList = forwardRef(({ children, ariaLabel, ariaLabelledby, ...props }, ref) => { 38 | const { reakitMenu, hideOnClickOutside } = useMenu() 39 | const label = ariaLabel ?? props['aria-label'] ?? undefined 40 | const labelledby = ariaLabelledby ?? props['aria-labelledby'] ?? undefined 41 | return ( 42 | 52 | 72 | {children} 73 | 74 | 75 | ) 76 | }) as FC 77 | 78 | MenuList.displayName = 'Menu.List' 79 | 80 | export default MenuList 81 | -------------------------------------------------------------------------------- /src/components/form/FormControl.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useMemo } from 'react' 2 | 3 | import { useColorModeValue } from '../../hooks' 4 | import { KleeFontFamily, KleeFontSize, KleeFontWeight } from '../../styles/theme/typography' 5 | import { cleanChildren } from '../../utils/jsx' 6 | import { Flex, FlexProps } from '../layout' 7 | import { Box, BoxProps } from '../primitives' 8 | import { BoxPropsOf } from '../primitives/Box' 9 | import { ErrorMessage } from './ErrorMessage' 10 | import { FormControlContext, useFormControl } from './FormControl.context' 11 | 12 | export interface FormControlProps extends Omit { 13 | readonly id: string 14 | readonly hideErrorMessage?: boolean 15 | } 16 | 17 | export interface FormControlLabelProps extends BoxPropsOf<'label'> {} 18 | export interface FormControlHelperTextProps extends BoxProps {} 19 | 20 | type SubComponents = { Label: typeof FormControlLabel; HelperText: typeof FormControlHelperText } 21 | 22 | export const FormControlLabel: FC = ({ ...props }) => { 23 | const context = useFormControl() 24 | return ( 25 | 32 | ) 33 | } 34 | 35 | FormControlLabel.displayName = 'FormControl.Label' 36 | 37 | export const FormControlHelperText: FC = ({ ...props }) => { 38 | const context = useFormControl() 39 | return ( 40 | 47 | ) 48 | } 49 | 50 | FormControlHelperText.displayName = 'FormControl.HelperText' 51 | 52 | export const FormControl: FC & SubComponents = ({ 53 | children, 54 | id, 55 | hideErrorMessage = false, 56 | ...props 57 | }) => { 58 | const hasHelperText = useMemo( 59 | () => !!cleanChildren(children).find((c: any) => c.type === FormControlHelperText), 60 | [children], 61 | ) 62 | return ( 63 | 64 | 65 | {children} 66 | {!hideErrorMessage ? : null} 67 | 68 | 69 | ) 70 | } 71 | 72 | FormControl.displayName = 'FormControl' 73 | 74 | FormControl.Label = FormControlLabel 75 | FormControl.HelperText = FormControlHelperText 76 | -------------------------------------------------------------------------------- /src/components/menu/MenuListItem.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | 3 | /** @jsx jsx */ 4 | import { jsx } from '@emotion/react' 5 | import styled from '@emotion/styled' 6 | import css from '@styled-system/css' 7 | import { FC, forwardRef, MouseEventHandler, ReactNode, useCallback } from 'react' 8 | import { MenuItem } from 'reakit/Menu' 9 | 10 | import { useColorModeValue } from '../../hooks' 11 | import { KleeLineHeight } from '../../styles/theme/typography' 12 | import Flex from '../layout/Flex' 13 | import { BoxProps } from '../primitives' 14 | import Text from '../typography/Text' 15 | import { MenuProps } from './Menu' 16 | import { useMenu } from './Menu.context' 17 | 18 | export interface MenuListItemProps extends BoxProps, Pick { 19 | readonly disabled?: boolean 20 | readonly children: ReactNode 21 | } 22 | 23 | const MenuListItemInner = styled(Flex)` 24 | pointer-events: all; 25 | &[disabled] { 26 | cursor: not-allowed; 27 | } 28 | &:hover { 29 | cursor: pointer; 30 | &[disabled] { 31 | cursor: not-allowed; 32 | } 33 | } 34 | &:active, 35 | &:focus { 36 | outline: none; 37 | } 38 | ` 39 | 40 | const MenuListItem: FC = forwardRef( 41 | ({ disabled, children, onClick, closeOnSelect, ...props }, ref) => { 42 | const { reakitMenu, closeOnSelect: menuCloseOnSelect } = useMenu() 43 | const onClickHandler = useCallback( 44 | e => { 45 | onClick?.(e) 46 | const shouldHide = (() => { 47 | if (closeOnSelect !== undefined && closeOnSelect) { 48 | return true 49 | } 50 | if (closeOnSelect !== undefined && !closeOnSelect) { 51 | return false 52 | } 53 | return menuCloseOnSelect && !closeOnSelect 54 | })() 55 | if (shouldHide) { 56 | reakitMenu.hide() 57 | } 58 | }, 59 | [closeOnSelect, menuCloseOnSelect, onClick, reakitMenu], 60 | ) 61 | return ( 62 | 85 | {typeof children === 'string' ? {children} : children} 86 | 87 | ) 88 | }, 89 | ) 90 | MenuListItem.displayName = 'Menu.ListItem' 91 | 92 | export default MenuListItem 93 | -------------------------------------------------------------------------------- /src/components/input/InputGroup.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | import { FiGlobe, FiMail, FiUser } from 'react-icons/fi' 4 | 5 | import { Avatar } from '../avatar' 6 | import { Icon } from '../icon' 7 | import { HStack, VStack } from '../layout' 8 | import { Input } from './Input' 9 | import { InputGroup, InputGroupProps } from './InputGroup' 10 | 11 | const meta: Meta = { 12 | title: 'Library/Forms/InputGroup', 13 | component: InputGroup, 14 | args: { 15 | focusBorderColor: 'blue.300', 16 | }, 17 | parameters: { 18 | controls: { expanded: true }, 19 | }, 20 | } 21 | 22 | export default meta 23 | 24 | export const WithAddons: Story = args => ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | @ 48 | 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | 56 | WithAddons.args = {} 57 | 58 | export const WithElements: Story = args => ( 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | @ 82 | 83 | 84 | 85 | 86 | 87 | 88 | ) 89 | 90 | WithElements.args = {} 91 | -------------------------------------------------------------------------------- /src/components/layout/Grid.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { Text } from '../typography' 5 | import Grid from './Grid' 6 | 7 | const meta: Meta = { 8 | title: 'Library/Layout/Grid', 9 | component: Grid, 10 | argTypes: { 11 | templateColumns: { 12 | control: { disabled: true }, 13 | table: { disable: true }, 14 | }, 15 | rowGap: { 16 | control: { disabled: true }, 17 | table: { disable: true }, 18 | }, 19 | columnGap: { 20 | control: { disabled: true }, 21 | table: { disable: true }, 22 | }, 23 | autoFlow: { 24 | control: { disabled: true }, 25 | table: { disable: true }, 26 | }, 27 | autoRows: { 28 | control: { disabled: true }, 29 | table: { disable: true }, 30 | }, 31 | autoColumns: { 32 | control: { disabled: true }, 33 | table: { disable: true }, 34 | }, 35 | templateRows: { 36 | control: { disabled: true }, 37 | table: { disable: true }, 38 | }, 39 | templateAreas: { 40 | control: { disabled: true }, 41 | table: { disable: true }, 42 | }, 43 | area: { 44 | control: { disabled: true }, 45 | table: { disable: true }, 46 | }, 47 | column: { 48 | control: { disabled: true }, 49 | table: { disable: true }, 50 | }, 51 | row: { 52 | control: { disabled: true }, 53 | table: { disable: true }, 54 | }, 55 | autoFit: { 56 | control: { disabled: true }, 57 | table: { disable: true }, 58 | }, 59 | autoFill: { 60 | control: { disabled: true }, 61 | table: { disable: true }, 62 | }, 63 | }, 64 | parameters: { 65 | controls: { expanded: true }, 66 | layout: 'fullscreen', 67 | }, 68 | } 69 | 70 | export default meta 71 | 72 | export const Default: Story = () => ( 73 | 74 | 75 | Hello 76 | 77 | 78 | Hello 79 | 80 | 81 | Hello 82 | 83 | 84 | ) 85 | 86 | Default.parameters = { 87 | controls: { disable: true }, 88 | } 89 | 90 | export const WithAutoFitFill: Story<{ mode: 'fit' | 'fill' }> = ({ mode = 'fill' }) => ( 91 | 97 | 98 | Hello 99 | 100 | 101 | Hello 102 | 103 | 104 | Hello 105 | 106 | 107 | ) 108 | 109 | WithAutoFitFill.args = { 110 | mode: 'fill', 111 | } 112 | 113 | WithAutoFitFill.argTypes = { 114 | mode: { 115 | control: { 116 | type: 'select', 117 | options: ['fit', 'fill'], 118 | defaultValue: 'fill', 119 | }, 120 | }, 121 | } 122 | -------------------------------------------------------------------------------- /src/components/menu/Menu.tsx: -------------------------------------------------------------------------------- 1 | import Tippy, { TippyProps } from '@tippyjs/react/headless' 2 | import * as React from 'react' 3 | import { FC, ReactElement, ReactNode, useMemo } from 'react' 4 | import { MenuInitialState, useMenuState, MenuProps as ReakitMenuProps } from 'reakit/Menu' 5 | 6 | import { KleeZIndex, Z_INDICES } from '../../styles/theme' 7 | import { MENU_TRANSITION_DURATION } from '../../utils/motion' 8 | import Box from '../primitives/Box' 9 | import { ShowableOnCreate } from '../types' 10 | import { Context, MenuContext } from './Menu.context' 11 | import MenuButton from './MenuButton' 12 | import MenuDivider from './MenuDivider' 13 | import MenuList from './MenuList' 14 | import MenuListItem from './MenuListItem' 15 | import MenuOptionGroup from './MenuOptionGroup' 16 | import MenuOptionItem from './MenuOptionItem' 17 | import { CommonProps } from './common' 18 | 19 | export interface MenuProps 20 | extends CommonProps, 21 | ShowableOnCreate, 22 | Partial>, 23 | Partial>, 24 | Partial> { 25 | readonly children: ReactNode 26 | /** 27 | * When enabled, the menu will auto close itself after you've selected a choice. It can also be enabled on a 28 | * per-instance of a `Menu.ListItem` 29 | */ 30 | readonly closeOnSelect?: boolean 31 | } 32 | 33 | type SubComponents = { 34 | Button: typeof MenuButton 35 | List: typeof MenuList 36 | ListItem: typeof MenuListItem 37 | Divider: typeof MenuDivider 38 | OptionGroup: typeof MenuOptionGroup 39 | OptionItem: typeof MenuOptionItem 40 | } 41 | 42 | const Menu: FC & SubComponents = ({ 43 | placement = 'bottom-start', 44 | offset = [0, 10], 45 | closeOnSelect = true, 46 | loop = false, 47 | hideOnClickOutside = true, 48 | showOnCreate = false, 49 | children, 50 | }) => { 51 | const button = React.Children.toArray(children).find((c: any) => c.type === MenuButton) as ReactElement 52 | const list = React.Children.toArray(children).find((c: any) => c.type === MenuList) as ReactElement 53 | const menu = useMenuState({ animated: MENU_TRANSITION_DURATION * 1000, loop, visible: showOnCreate }) 54 | const context = useMemo( 55 | () => ({ reakitMenu: menu, closeOnSelect, hideOnClickOutside, placement }), 56 | [menu, closeOnSelect, hideOnClickOutside, placement], 57 | ) 58 | return ( 59 | 60 | 61 | (list ? React.cloneElement(list, attrs) : null)} 66 | animation 67 | visible={menu.visible} 68 | popperOptions={{ 69 | strategy: 'fixed', 70 | }} 71 | > 72 | {button} 73 | 74 | 75 | 76 | ) 77 | } 78 | 79 | Menu.displayName = 'Menu' 80 | 81 | Menu.Button = MenuButton 82 | Menu.List = MenuList 83 | Menu.ListItem = MenuListItem 84 | Menu.Divider = MenuDivider 85 | Menu.OptionGroup = MenuOptionGroup 86 | Menu.OptionItem = MenuOptionItem 87 | 88 | export default Menu 89 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const toPath = _path => path.join(process.cwd(), _path) 3 | 4 | const EXLUDED_DOCGEN_PROPS = [ 5 | 'as', 6 | 'gap', 7 | 'sx', 8 | '_hover', 9 | '_active', 10 | '_focus', 11 | '_disabled', 12 | 'uppercase', 13 | 'zIndex', 14 | 'minSize', 15 | 'maxSize', 16 | 'boxShadow', 17 | 'textShadow', 18 | 'fontSize', 19 | 'letterSpacing', 20 | 'fontWeight', 21 | 'fontFamily', 22 | 'lineHeight', 23 | 'css', 24 | 'disableFocusStyles', 25 | '_variants', 26 | '_selected', 27 | 'bgGradient', 28 | 'backgroundGradient', 29 | 'bgClip', 30 | 'backgroundClip', 31 | 'focusBorderColor', 32 | 'align', 33 | 'justify', 34 | 'wrap', 35 | 'direction', 36 | 'basis', 37 | 'grow', 38 | 'shrink', 39 | 'spacing', 40 | 'transform', 41 | 'transformOrigin', 42 | 'enableGpuAcceleration', 43 | 'scale', 44 | 'scaleX', 45 | 'scaleY', 46 | 'rotate', 47 | 'translateX', 48 | 'translateY', 49 | 'skewY', 50 | 'skewX', 51 | 'ref', 52 | '_invalid', 53 | 'invalidFocusBorderColor', 54 | '_light', 55 | '_dark', 56 | '_focusVisible', 57 | 'inset', 58 | ] 59 | 60 | const ALLOWED_DOCGEN_NODE_MODULES = ['tippy.js'] 61 | 62 | module.exports = { 63 | core: { 64 | builder: 'webpack5', 65 | }, 66 | webpackFinal: async config => { 67 | return { 68 | ...config, 69 | resolve: { 70 | ...config.resolve, 71 | alias: { 72 | ...config.resolve.alias, 73 | '@emotion/core': toPath('node_modules/@emotion/react'), 74 | '@emotion/styled-base': toPath('node_modules/@emotion/styled'), 75 | '@emotion/styled': toPath('node_modules/@emotion/styled'), 76 | 'emotion-theming': toPath('node_modules/@emotion/react'), 77 | }, 78 | }, 79 | } 80 | }, 81 | stories: ['../src/**/*.stories.@(ts|tsx|js|jsx|mdx)'], 82 | addons: [ 83 | '@storybook/addon-viewport', 84 | '@storybook/addon-storysource', 85 | '@storybook/addon-links', 86 | { 87 | name: '@storybook/addon-docs', 88 | options: { 89 | configureJSX: true, 90 | }, 91 | }, 92 | '@storybook/addon-essentials', 93 | 'storybook-dark-mode', 94 | ], 95 | typescript: { 96 | check: true, 97 | reactDocgen: 'react-docgen-typescript', 98 | reactDocgenTypescriptOptions: { 99 | shouldExtractLiteralValuesFromEnum: true, 100 | propFilter: (prop, component) => { 101 | if (component.name === 'Box') { 102 | return false 103 | } 104 | if (EXLUDED_DOCGEN_PROPS.includes(prop.name)) { 105 | return false 106 | } 107 | if ( 108 | prop.parent && 109 | prop.parent.fileName && 110 | ALLOWED_DOCGEN_NODE_MODULES.some(module => prop.parent.fileName.includes(module)) 111 | ) { 112 | return true 113 | } 114 | if (prop.parent && /node_modules/.test(prop.parent.fileName)) { 115 | return false 116 | } 117 | 118 | return true 119 | }, 120 | 121 | compilerOptions: { 122 | allowSyntheticDefaultImports: false, 123 | esModuleInterop: false, 124 | }, 125 | }, 126 | }, 127 | } 128 | -------------------------------------------------------------------------------- /src/components/layout/Grid.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | import { GridProps as StyledGridProps } from 'styled-system' 3 | 4 | import Box, { BoxProps, PolymorphicComponent } from '../primitives/Box' 5 | 6 | type AutoFillFitOptions = { 7 | min: string 8 | max: string 9 | } 10 | 11 | export interface GridOptions { 12 | templateColumns?: StyledGridProps['gridTemplateColumns'] 13 | gap?: BoxProps['gridGap'] 14 | rowGap?: BoxProps['gridGap'] 15 | columnGap?: BoxProps['gridGap'] 16 | autoFlow?: StyledGridProps['gridAutoFlow'] 17 | autoRows?: StyledGridProps['gridAutoRows'] 18 | autoColumns?: StyledGridProps['gridAutoColumns'] 19 | templateRows?: StyledGridProps['gridTemplateRows'] 20 | templateAreas?: StyledGridProps['gridTemplateAreas'] 21 | area?: StyledGridProps['gridArea'] 22 | column?: StyledGridProps['gridColumn'] 23 | row?: StyledGridProps['gridRow'] 24 | autoFit?: AutoFillFitOptions | boolean 25 | autoFill?: AutoFillFitOptions | boolean 26 | } 27 | 28 | export type GridProps = Omit< 29 | BoxProps, 30 | | 'templateColumns' 31 | | 'gap' 32 | | 'rowGap' 33 | | 'columnGap' 34 | | 'autoFlow' 35 | | 'autoRows' 36 | | 'autoColumns' 37 | | 'templateRows' 38 | | 'templateAreas' 39 | | 'area' 40 | | 'column' 41 | | 'row' 42 | > & 43 | GridOptions 44 | 45 | const Grid = forwardRef((props, ref) => { 46 | const { 47 | templateColumns, 48 | gap, 49 | rowGap, 50 | columnGap, 51 | autoFlow, 52 | autoRows, 53 | autoColumns, 54 | templateRows, 55 | templateAreas, 56 | area, 57 | column, 58 | row, 59 | autoFit, 60 | autoFill, 61 | sx, 62 | display = 'grid', 63 | ...rest 64 | } = props 65 | const styles = { 66 | ...(autoFit && 67 | typeof autoFit === 'boolean' && 68 | autoFit === true && { 69 | gridTemplateColumns: `repeat(auto-fit, minmax(100px, 1fr))`, 70 | }), 71 | ...(autoFit && 72 | typeof autoFit === 'object' && { 73 | gridTemplateColumns: `repeat(auto-fit, minmax(${autoFit.min ?? '100px'}, ${autoFit.max ?? '1fr'}))`, 74 | }), 75 | ...(autoFill && 76 | typeof autoFill === 'boolean' && 77 | autoFill === true && { 78 | gridTemplateColumns: `repeat(auto-fill, minmax(100px, 1fr))`, 79 | }), 80 | ...(autoFill && 81 | typeof autoFill === 'object' && { 82 | gridTemplateColumns: `repeat(auto-fill, minmax(${autoFill.min ?? '100px'}, ${autoFill.max ?? '1fr'}))`, 83 | }), 84 | } 85 | return ( 86 | 107 | ) 108 | }) 109 | 110 | Grid.displayName = 'Grid' 111 | 112 | export default Grid as PolymorphicComponent 113 | -------------------------------------------------------------------------------- /src/styles/theme/typography.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | export const LETTER_SPACINGS = { 3 | tighter: '-0.05em', 4 | tight: '-0.025em', 5 | normal: '0', 6 | wide: '0.025em', 7 | wider: '0.05em', 8 | widest: '0.1em', 9 | } as const 10 | 11 | export enum KleeLetterSpacing { 12 | Tighter = 'tighter', 13 | Tight = 'tight', 14 | Normal = 'normal', 15 | Wide = 'wide', 16 | Wider = 'wider', 17 | Widest = 'widest', 18 | } 19 | 20 | export type ThemeLetterSpacingsValues = keyof typeof LETTER_SPACINGS | (string & {}) | (number & {}) 21 | 22 | export const LINE_HEIGHTS = { 23 | normal: 'normal', 24 | none: '1', 25 | shorter: '1.25', 26 | short: '1.375', 27 | base: '1.5', 28 | tall: '1.625', 29 | taller: '2', 30 | } as const 31 | 32 | export enum KleeLineHeight { 33 | Normal = 'normal', 34 | None = 'none', 35 | Shorter = 'shorter', 36 | Short = 'short', 37 | Base = 'base', 38 | Tall = 'tall', 39 | Taller = 'taller', 40 | } 41 | 42 | export type ThemeLineHeightsValues = keyof typeof LINE_HEIGHTS | (string & {}) | (number & {}) 43 | 44 | export const FONT_WEIGHTS = { 45 | hairline: 100, 46 | thin: 200, 47 | light: 300, 48 | normal: 400, 49 | medium: 500, 50 | semibold: 600, 51 | bold: 700, 52 | extrabold: 800, 53 | black: 900, 54 | } as const 55 | 56 | export enum KleeFontWeight { 57 | Hairline = 'hairline', 58 | Thin = 'thin', 59 | Light = 'light', 60 | Normal = 'normal', 61 | Medium = 'medium', 62 | Semibold = 'semibold', 63 | Bold = 'bold', 64 | Extrabold = 'extrabold', 65 | Black = 'black', 66 | } 67 | 68 | export type ThemeFontWeightsValues = keyof typeof FONT_WEIGHTS | (string & {}) | (number & {}) 69 | 70 | export const FONT_FAMILIES = { 71 | heading: `Nunito, Montserrat, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`, 72 | body: `Nunito, Zilla Slab, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`, 73 | mono: `SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace`, 74 | } as const 75 | 76 | export enum KleeFontFamily { 77 | Heading = 'heading', 78 | Body = 'body', 79 | Mono = 'mono', 80 | } 81 | 82 | export type ThemeFontFamiliesValue = keyof typeof FONT_FAMILIES | (string & {}) 83 | 84 | export const FONT_SIZES = { 85 | xs: '0.75rem', 86 | sm: '0.875rem', 87 | md: '1rem', 88 | lg: '1.125rem', 89 | xl: '1.25rem', 90 | '2xl': '1.5rem', 91 | '3xl': '1.875rem', 92 | '4xl': '2.25rem', 93 | '5xl': '3rem', 94 | '6xl': '4rem', 95 | } as const 96 | 97 | export enum KleeFontSize { 98 | Xs = 'xs', 99 | Sm = 'sm', 100 | Md = 'md', 101 | Lg = 'lg', 102 | Xl = 'xl', 103 | Xl2 = '2xl', 104 | Xl3 = '3xl', 105 | Xl4 = '4xl', 106 | Xl5 = '5xl', 107 | Xl6 = '6xl', 108 | } 109 | 110 | export type ThemeFontSizesValues = keyof typeof FONT_SIZES | (string & {}) | (number & {}) 111 | 112 | const typography = { 113 | letterSpacings: LETTER_SPACINGS, 114 | lineHeights: LINE_HEIGHTS, 115 | fontWeights: FONT_WEIGHTS, 116 | fonts: FONT_FAMILIES, 117 | fontSizes: FONT_SIZES, 118 | } as const 119 | 120 | export default typography 121 | -------------------------------------------------------------------------------- /src/components/icon/Icon.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | import type { IconType } from 'react-icons' 4 | import * as FaIcons from 'react-icons/fa' 5 | import * as FiIcons from 'react-icons/fi' 6 | import * as MdIcons from 'react-icons/md' 7 | import * as VscIcons from 'react-icons/vsc' 8 | 9 | import { KleeRadius, KleeShadow } from '../../styles/theme' 10 | import { KleeFontSize, KleeFontWeight } from '../../styles/theme/typography' 11 | import Flex from '../layout/Flex' 12 | import Grid from '../layout/Grid' 13 | import Box from '../primitives/Box' 14 | import Heading from '../typography/Heading' 15 | import Text from '../typography/Text' 16 | import Icon, { IconProps } from './Icon' 17 | 18 | const meta: Meta = { 19 | title: 'Library/Icon', 20 | component: Icon, 21 | args: { 22 | color: 'rgb(0,0,0)', 23 | size: 'md', 24 | }, 25 | argTypes: { 26 | size: { 27 | control: { 28 | type: 'select', 29 | options: ['xs', 'sm', 'md', 'lg'], 30 | }, 31 | }, 32 | color: { 33 | control: { 34 | type: 'color', 35 | }, 36 | }, 37 | title: { table: { disable: true }, control: { disable: true } }, 38 | icons: { table: { disable: true }, control: { disable: true } }, 39 | }, 40 | parameters: { 41 | controls: { expanded: true }, 42 | }, 43 | } 44 | 45 | export default meta 46 | 47 | const MAX_ICONS = 100 48 | 49 | const Template: Story }> = ({ title, icons, ...args }) => ( 50 | 51 | 52 | {title}{' '} 53 | 54 | (showing first {MAX_ICONS} icons) 55 | 56 | 57 | 58 | {Object.entries(icons) 59 | .slice(0, MAX_ICONS) 60 | .map(([moduleName, icon]) => ( 61 | 62 | 70 | 71 | 72 | 73 | {moduleName} 74 | 75 | 76 | ))} 77 | 78 | 79 | ) 80 | 81 | export const MaterialDesign = Template.bind({}) 82 | 83 | MaterialDesign.args = { 84 | title: 'Library/Material Design', 85 | icons: MdIcons, 86 | } 87 | 88 | export const FontAwesome = Template.bind({}) 89 | 90 | FontAwesome.args = { 91 | title: 'Library/Font Awesome', 92 | icons: FaIcons, 93 | } 94 | 95 | export const FeatherIcons = Template.bind({}) 96 | 97 | FeatherIcons.args = { 98 | title: 'Library/Feather Icons', 99 | icons: FiIcons, 100 | } 101 | 102 | export const VisualStudioCode = Template.bind({}) 103 | 104 | VisualStudioCode.args = { 105 | title: 'Library/Visual Studio Code', 106 | icons: VscIcons, 107 | } 108 | -------------------------------------------------------------------------------- /src/components/input/Input.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | import get from 'lodash/get' 3 | import React, { ComponentProps, forwardRef, PropsWithoutRef } from 'react' 4 | import { useFormContext } from 'react-hook-form' 5 | import { variant } from 'styled-system' 6 | 7 | import { BASE_FOCUS } from '../../styles/modules/mixins' 8 | import { KleeBorder, KleeRadius } from '../../styles/theme' 9 | import { KleeFontFamily, KleeFontSize } from '../../styles/theme/typography' 10 | import { CssVars } from '../../utils' 11 | import { useFormControl } from '../form/FormControl.context' 12 | import { Box, BoxProps } from '../primitives' 13 | 14 | type FilteredInputProps = Omit, 'css'> 15 | type Props = FilteredInputProps & Omit 16 | type VariantSize = 'sm' | 'md' | 'lg' 17 | type Variant = 'outline' | 'flushed' | 'blank' 18 | 19 | export const DEFAULT_INPUT_RADIUS = KleeRadius.Md 20 | 21 | export interface InputProps extends PropsWithoutRef { 22 | readonly variantSize?: VariantSize 23 | readonly variant?: Variant 24 | } 25 | 26 | const InputInner = styled(Box)({ 27 | transition: 'box-shadow 0.2s, border 0.2s', 28 | }) 29 | 30 | const variants = [ 31 | variant({ 32 | variants: { 33 | blank: { 34 | borderRadius: DEFAULT_INPUT_RADIUS, 35 | border: 0, 36 | '&:focus': { 37 | ...BASE_FOCUS, 38 | }, 39 | '&[aria-invalid="true"]': { 40 | boxShadow: `var(${CssVars.InvalidFocusBorderColor}) 0px 0px 0px 2px`, 41 | }, 42 | }, 43 | flushed: { 44 | border: 0, 45 | borderRadius: 0, 46 | borderTopLeftRadius: DEFAULT_INPUT_RADIUS, 47 | borderTopRightRadius: DEFAULT_INPUT_RADIUS, 48 | borderBottom: KleeBorder.Xs, 49 | '&:focus': { 50 | ...BASE_FOCUS, 51 | boxShadow: 'none', 52 | borderBottom: KleeBorder.Sm, 53 | borderColor: `var(${CssVars.FocusBorderColor})`, 54 | }, 55 | '&[aria-invalid="true"]': { 56 | borderColor: `var(${CssVars.InvalidFocusBorderColor})`, 57 | }, 58 | }, 59 | outline: { 60 | borderRadius: DEFAULT_INPUT_RADIUS, 61 | border: KleeBorder.Sm, 62 | '&:focus': { 63 | ...BASE_FOCUS, 64 | }, 65 | '&[aria-invalid="true"]': { 66 | borderColor: `var(${CssVars.InvalidFocusBorderColor})`, 67 | }, 68 | }, 69 | }, 70 | }), 71 | variant({ 72 | prop: 'variantSize', 73 | variants: { 74 | sm: { 75 | px: 2, 76 | height: 8, 77 | fontSize: KleeFontSize.Sm, 78 | }, 79 | md: { 80 | px: 2, 81 | height: 10, 82 | fontSize: KleeFontSize.Md, 83 | }, 84 | lg: { 85 | px: 3, 86 | height: 12, 87 | fontSize: KleeFontSize.Lg, 88 | }, 89 | }, 90 | }), 91 | ] 92 | 93 | export const Input = forwardRef( 94 | ({ id, name, disabled, type = 'text', _invalid, ...rest }, ref) => { 95 | const context = useFormControl() 96 | const formContext = useFormContext() 97 | let errors = null 98 | if (formContext && formContext.formState) { 99 | const { 100 | formState: { errors: formErrors }, 101 | } = formContext 102 | errors = formErrors 103 | } 104 | const hasError = !!get(errors, `${name}.message`) 105 | return ( 106 | 124 | ) 125 | }, 126 | ) 127 | 128 | Input.defaultProps = { 129 | variant: 'outline', 130 | variantSize: 'md', 131 | } 132 | 133 | Input.displayName = 'Input' 134 | -------------------------------------------------------------------------------- /src/components/layout/Flex.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@emotion/react' 2 | import { themeGet } from '@styled-system/theme-get' 3 | import React, { cloneElement, forwardRef, ReactElement, ReactNode } from 'react' 4 | import { FlexboxProps } from 'styled-system' 5 | 6 | import { cleanChildren, hasProps } from '../../utils/jsx' 7 | import Box, { BoxProps, PolymorphicComponent } from '../primitives/Box' 8 | 9 | export interface FlexOptions { 10 | readonly align?: FlexboxProps['alignItems'] 11 | readonly justify?: FlexboxProps['justifyContent'] 12 | readonly wrap?: FlexboxProps['flexWrap'] 13 | readonly direction?: FlexboxProps['flexDirection'] 14 | readonly basis?: FlexboxProps['flexBasis'] 15 | readonly grow?: FlexboxProps['flexGrow'] 16 | readonly shrink?: FlexboxProps['flexShrink'] 17 | readonly spacing?: number | string 18 | } 19 | 20 | export type FlexProps = Omit< 21 | BoxProps, 22 | 'flexDirection' | 'alignItems' | 'justifyContent' | 'flexWrap' | 'flexBasis' | 'flexGrow' | 'spacing' 23 | > & 24 | FlexOptions 25 | 26 | const getChildren = (children: ReactNode) => { 27 | const style = { 28 | marginLeft: 0, 29 | marginTop: 0, 30 | } 31 | if (!children) return null 32 | if (typeof children === 'string') { 33 | return children 34 | } 35 | 36 | if (Array.isArray(children)) { 37 | return cleanChildren(children).map((c, i) => 38 | cloneElement(c as ReactElement, { 39 | ...(i === 0 ? { style } : {}), 40 | }), 41 | ) 42 | } 43 | 44 | if (hasProps(children)) { 45 | return cloneElement(children as ReactElement, { style }) 46 | } 47 | 48 | return children 49 | } 50 | 51 | export const Flex = forwardRef((props, ref) => { 52 | const { 53 | direction = 'row', 54 | align, 55 | justify, 56 | wrap, 57 | basis, 58 | grow, 59 | sx, 60 | spacing: userSpacing, 61 | display = 'flex', 62 | children, 63 | ...rest 64 | } = props 65 | const theme = useTheme() 66 | const spacing = themeGet(`space.${userSpacing}`, userSpacing ?? 0)({ theme }) as unknown as number 67 | const styles = { 68 | ...(spacing 69 | ? { 70 | '& > *': { 71 | ...(typeof direction === 'string' && direction === 'row' && { marginLeft: spacing }), 72 | ...(typeof direction === 'string' && direction === 'column' && { marginTop: spacing }), 73 | // While `gap` for flex is not supported by a mojority of browser, 74 | // we prefer this approach to have a broader compatibility, and also to support 75 | // responsive values 🔥🥵🔥 76 | ...(Array.isArray(direction) && 77 | direction.reduce( 78 | (acc, _, index) => { 79 | return { 80 | ...acc, 81 | [`@media screen and (min-width: ${theme.breakpoints[index]})`]: 82 | direction[index + 1] === undefined 83 | ? {} 84 | : { 85 | ...((direction[index + 1] ?? 'row') === 'row' 86 | ? { marginLeft: spacing, marginTop: 0 } 87 | : { marginTop: spacing, marginLeft: 0 }), 88 | }, 89 | } 90 | }, 91 | { 92 | ...(direction[0] === 'row' && { 93 | marginLeft: spacing, 94 | marginTop: 0, 95 | }), 96 | ...(direction[0] === 'column' && { 97 | marginTop: spacing, 98 | marginLeft: 0, 99 | }), 100 | }, 101 | )), 102 | }, 103 | } 104 | : {}), 105 | } 106 | 107 | return ( 108 | 123 | {userSpacing ? getChildren(children) : children} 124 | 125 | ) 126 | }) 127 | 128 | Flex.displayName = 'Flex' 129 | 130 | export default Flex as PolymorphicComponent 131 | -------------------------------------------------------------------------------- /src/components/avatar/Avatar.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | 3 | /** @jsx jsx */ 4 | import { css, jsx } from '@emotion/react' 5 | import styled from '@emotion/styled' 6 | import ColorHash from 'color-hash-ts' 7 | import { FC, Fragment } from 'react' 8 | import { variant } from 'styled-system' 9 | 10 | import { KleeRadius } from '../../styles/theme' 11 | import { KleeFontFamily, KleeFontSize, KleeFontWeight } from '../../styles/theme/typography' 12 | import { colorContrast } from '../../utils/color' 13 | import Flex from '../layout/Flex' 14 | import Box, { BoxProps } from '../primitives/Box' 15 | 16 | const hash = new ColorHash() 17 | 18 | export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' 19 | 20 | export interface AvatarDetails { 21 | readonly name?: string 22 | readonly src?: string 23 | readonly alt?: string 24 | } 25 | 26 | export interface AvatarProps extends BoxProps, AvatarDetails { 27 | readonly size?: AvatarSize 28 | readonly showLastname?: boolean 29 | readonly squared?: boolean 30 | } 31 | 32 | const getInitials = (name?: string, showLastname = false): string => { 33 | if (!name) return '' 34 | const [firstName, lastName] = name.split(' ') 35 | 36 | if (!showLastname) { 37 | return firstName.charAt(0) 38 | } 39 | if (firstName && lastName) { 40 | return `${firstName.charAt(0)}${lastName.charAt(0)}` 41 | } 42 | 43 | return firstName.charAt(0) 44 | } 45 | 46 | const AvatarInner = styled(Flex)( 47 | { 48 | userSelect: 'none', 49 | }, 50 | variant<{}, AvatarSize>({ 51 | variants: { 52 | xs: { 53 | fontSize: KleeFontSize.Xs, 54 | size: 6, 55 | minWidth: 6, 56 | minHeight: 6, 57 | maxWidth: 6, 58 | maxHeight: 6, 59 | }, 60 | sm: { 61 | fontSize: KleeFontSize.Sm, 62 | size: 10, 63 | minWidth: 10, 64 | minHeight: 10, 65 | maxWidth: 10, 66 | maxHeight: 10, 67 | }, 68 | md: { 69 | fontSize: KleeFontSize.Lg, 70 | size: 12, 71 | minWidth: 12, 72 | minHeight: 12, 73 | maxWidth: 12, 74 | maxHeight: 12, 75 | }, 76 | lg: { 77 | fontSize: KleeFontSize.Xl3, 78 | size: 14, 79 | minWidth: 14, 80 | minHeight: 14, 81 | maxWidth: 14, 82 | maxHeight: 14, 83 | }, 84 | xl: { 85 | fontSize: KleeFontSize.Xl5, 86 | size: 16, 87 | minWidth: 16, 88 | minHeight: 16, 89 | maxWidth: 16, 90 | maxHeight: 16, 91 | }, 92 | }, 93 | }), 94 | ) 95 | 96 | export const Avatar: FC = ({ 97 | src, 98 | alt, 99 | children, 100 | bg, 101 | backgroundColor, 102 | color, 103 | squared = false, 104 | showLastname = false, 105 | size = 'md', 106 | name = '', 107 | ...props 108 | }) => { 109 | const shouldDisplayName = src === null || src === undefined 110 | const background = hash.hex(name) 111 | const computedColor = colorContrast(background) 112 | 113 | const render = () => { 114 | if (children) { 115 | return {children} 116 | } 117 | if (!children && shouldDisplayName) { 118 | return getInitials(name, showLastname) 119 | } 120 | if (!shouldDisplayName) { 121 | return ( 122 | 133 | ) 134 | } 135 | return null 136 | } 137 | 138 | return ( 139 | 159 | {render()} 160 | 161 | ) 162 | } 163 | 164 | export default Avatar 165 | -------------------------------------------------------------------------------- /src/components/tabs/Tab.tsx: -------------------------------------------------------------------------------- 1 | import { Theme, useTheme } from '@emotion/react' 2 | import styled from '@emotion/styled' 3 | import { themeGet } from '@styled-system/theme-get' 4 | import { motion } from 'framer-motion' 5 | import React, { FC, useEffect, useRef, useState } from 'react' 6 | import { Tab as BaseTab, TabProps as BaseTabProps } from 'reakit/Tab' 7 | import { variant } from 'styled-system' 8 | 9 | import { BASE_FOCUS } from '../../styles/modules/mixins' 10 | import { KleeRadius } from '../../styles/theme' 11 | import { KleeFontWeight } from '../../styles/theme/typography' 12 | import { Box, BoxProps } from '../primitives/Box' 13 | import { TabsOrientation, TabsVariant, useTabs } from './Tabs.context' 14 | 15 | export interface TabProps extends BoxProps, Pick {} 16 | 17 | const BorderBox = styled(motion(Box))() 18 | 19 | type VariantArgs = { theme: Theme; colorScheme: string } 20 | 21 | const borderBoxVariants = ({ colorScheme, theme, variant: tabVariant }: VariantArgs & { variant: TabsVariant }) => [ 22 | variant<{}, TabsVariant>({ 23 | variants: { 24 | line: { 25 | borderRadius: KleeRadius.None, 26 | bottom: '-2px', 27 | left: '25%', 28 | width: '50%', 29 | height: '2px', 30 | bg: 31 | theme.currentMode === 'light' 32 | ? themeGet(`colors.${colorScheme}.400`, 'text')({ theme }) 33 | : themeGet(`colors.${colorScheme}.500`, 'text')({ theme }), 34 | }, 35 | rounded: { 36 | borderRadius: 'inherit', 37 | left: 0, 38 | right: 0, 39 | bottom: 0, 40 | top: 0, 41 | zIndex: 0, 42 | bg: 43 | theme.currentMode === 'light' 44 | ? themeGet(`colors.${colorScheme}.200`, 'transparent')({ theme }) 45 | : themeGet(`colors.${colorScheme}.400`, 'transparent')({ theme }), 46 | }, 47 | }, 48 | }), 49 | variant<{}, TabsOrientation>({ 50 | prop: 'variantOrientation', 51 | variants: { 52 | horizontal: {}, 53 | vertical: 54 | tabVariant === 'line' 55 | ? { 56 | left: 'unset', 57 | bottom: 0, 58 | right: '-2px', 59 | width: '2px', 60 | height: '100%', 61 | top: 0, 62 | } 63 | : {}, 64 | }, 65 | }), 66 | ] 67 | const tabVariants = variant<{}, TabsVariant>({ 68 | variants: { 69 | line: { 70 | borderRadius: KleeRadius.None, 71 | color: 'text', 72 | }, 73 | rounded: { 74 | borderRadius: KleeRadius.Xxxl, 75 | color: 'text', 76 | }, 77 | }, 78 | }) 79 | 80 | export const Tab: FC = ({ children, _focus, sx, _hover, _selected, _disabled, ...props }) => { 81 | const { tabs, colorScheme, variant, orientation } = useTabs() 82 | const theme = useTheme() 83 | const [id, setId] = useState(undefined) 84 | const $tab = useRef() 85 | useEffect(() => { 86 | if ($tab.current) { 87 | setId($tab.current.id) 88 | } 89 | }, []) 90 | 91 | return ( 92 | 133 | 134 | {children} 135 | 136 | {id === tabs.currentId && ( 137 | 145 | )} 146 | 147 | ) 148 | } 149 | 150 | Tab.displayName = 'Tab' 151 | 152 | export default Tab 153 | -------------------------------------------------------------------------------- /src/components/button/Button.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsOf, Theme } from '@emotion/react' 2 | import styled from '@emotion/styled' 3 | import { themeGet } from '@styled-system/theme-get' 4 | import React, { cloneElement, FC, forwardRef, PropsWithoutRef, ReactElement } from 'react' 5 | import { variant as systemVariant } from 'styled-system' 6 | 7 | import colors from '../../styles/modules/colors' 8 | import { BASE_FOCUS } from '../../styles/modules/mixins' 9 | import { KleeFontFamily, KleeFontSize, KleeFontWeight } from '../../styles/theme/typography' 10 | import { CssVars } from '../../utils' 11 | import Box, { BoxProps, PolymorphicComponent } from '../primitives/Box' 12 | 13 | type Variant = 'primary' | 'secondary' | 'tertiary' | 'danger' | 'semi-transparent' | 'transparent' 14 | type VariantSize = 'sm' | 'md' | 'lg' 15 | 16 | type HTMLButtonProps = Omit>, 'css'> 17 | 18 | export interface ButtonProps extends Omit, HTMLButtonProps { 19 | readonly variant?: Variant 20 | readonly startIcon?: ReactElement 21 | readonly endIcon?: ReactElement 22 | readonly variantSize?: VariantSize 23 | } 24 | 25 | const generateVariant = (color: keyof typeof colors | 'semi-transparent', theme: Theme) => { 26 | if (color === 'transparent') { 27 | return { 28 | bg: 'transparent', 29 | color: 'inherit', 30 | '&:hover': { 31 | bg: `rgba(0, 0, 0, 0.06)`, 32 | }, 33 | '&:focus': { 34 | boxShadow: theme.currentMode === 'light' ? `0 0 0 2px rgba(0,0,0,0.10)` : `0 0 0 2px rgba(255,255,255,0.5)`, 35 | }, 36 | '&:disabled': { 37 | bg: `transparent`, 38 | opacity: 0.6, 39 | }, 40 | } 41 | } 42 | if (color === 'semi-transparent') { 43 | return { 44 | bg: theme.currentMode === 'light' ? 'rgba(0,0,0,0.3)' : 'rgba(255,255,255,0.1)', 45 | '&:hover': { 46 | bg: theme.currentMode === 'light' ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.05)', 47 | }, 48 | '&:focus': { 49 | boxShadow: theme.currentMode === 'light' ? '0 0 0 2px rgba(0,0,0,0.2)' : '0 0 0 2px rgba(255,255,255,0.5)', 50 | }, 51 | '&:disabled': { 52 | bg: `rgba(0, 0, 0, 0.3)`, 53 | }, 54 | } 55 | } 56 | return { 57 | bg: `${color}.500`, 58 | [`${CssVars.FocusBorderColor}`]: themeGet(`colors.${color}.300`, 'rgb(66 153 225 / 60%)')({ theme }), 59 | 60 | '&:hover': { 61 | bg: `${color}.600`, 62 | }, 63 | '&:focus': { 64 | ...BASE_FOCUS, 65 | }, 66 | '&:disabled': { 67 | bg: `${color}.100`, 68 | color: `${color}.300`, 69 | }, 70 | } 71 | } 72 | 73 | const InnerButton = styled(Box)( 74 | { 75 | transition: 'background 0.2s, box-shadow 0.3s', 76 | outline: 'none', 77 | '&:hover': { 78 | cursor: 'pointer', 79 | }, 80 | '&:disabled': { 81 | cursor: 'not-allowed', 82 | }, 83 | }, 84 | ({ theme }) => 85 | systemVariant<{}, Variant>({ 86 | variants: { 87 | primary: generateVariant('indigo', theme), 88 | secondary: generateVariant('cyan', theme), 89 | tertiary: generateVariant('cool-gray', theme), 90 | danger: generateVariant('rose', theme), 91 | transparent: generateVariant('transparent', theme), 92 | 'semi-transparent': generateVariant('semi-transparent', theme), 93 | }, 94 | }), 95 | systemVariant<{}, VariantSize>({ 96 | prop: 'variantSize', 97 | variants: { 98 | sm: { 99 | px: 2, 100 | py: 1, 101 | fontSize: KleeFontSize.Xs, 102 | }, 103 | md: { 104 | px: 4, 105 | py: 2, 106 | fontSize: KleeFontSize.Sm, 107 | }, 108 | lg: { 109 | px: 8, 110 | py: 4, 111 | fontSize: KleeFontSize.Lg, 112 | }, 113 | }, 114 | }), 115 | ) as PolymorphicComponent 116 | 117 | export const Button: FC = forwardRef( 118 | ({ children, startIcon, endIcon, variant = 'primary', variantSize = 'md', ...props }, ref) => { 119 | return ( 120 | 135 | {startIcon && cloneElement(startIcon, { mr: 2 })} 136 | {children} 137 | {endIcon && cloneElement(endIcon, { ml: 2 })} 138 | 139 | ) 140 | }, 141 | ) 142 | 143 | export default Button 144 | -------------------------------------------------------------------------------- /src/components/menu/Menu.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React, { ReactNode, useState } from 'react' 3 | import { FiChevronDown, FiChevronUp, FiEdit, FiLogOut, FiPrinter, FiSettings, FiUser } from 'react-icons/fi' 4 | 5 | import { Button } from '../button' 6 | import { Icon } from '../icon' 7 | import { Modal } from '../modal' 8 | import Text from '../typography/Text' 9 | import Menu, { MenuProps } from './Menu' 10 | 11 | type StoryMenuProps = MenuProps & { menuList: ReactNode } 12 | 13 | const meta: Meta = { 14 | title: 'Library/Menu', 15 | component: Menu, 16 | argTypes: { 17 | menuList: { table: { disable: true }, control: { disable: true } }, 18 | }, 19 | args: { 20 | showOnCreate: true, 21 | }, 22 | parameters: { 23 | controls: { expanded: true }, 24 | }, 25 | } 26 | 27 | export default meta 28 | 29 | const Template: Story = ({ menuList, ...args }) => ( 30 | 31 | 32 | Menu 33 | 34 | {menuList} 35 | 36 | ) 37 | 38 | export const Default = Template.bind({}) 39 | 40 | Default.args = { 41 | menuList: ( 42 | 43 | 44 | 45 | Preferences 46 | 47 | 48 | 49 | Edit (not available yet) 50 | 51 | 52 | 53 | Logout 54 | 55 | 56 | ), 57 | } 58 | 59 | export const WithDivider = Template.bind({}) 60 | 61 | WithDivider.args = { 62 | menuList: ( 63 | 64 | 65 | 66 | My profile 67 | 68 | 69 | 70 | Edit (not available yet) 71 | 72 | 73 | 74 | Print 75 | 76 | 77 | 78 | 79 | Logout 80 | 81 | 82 | ), 83 | } 84 | 85 | export const WithModalOptions = Template.bind({}) 86 | 87 | WithModalOptions.args = { 88 | menuList: ( 89 | 90 | 95 | 96 | Open profile 97 | 98 | } 99 | ariaLabel="profile-modal" 100 | > 101 | User profile 102 | 103 | Hello from your profile 104 | 105 | 106 | 107 | 108 | Edit (not available yet) 109 | 110 | 111 | 112 | Print 113 | 114 | 115 | 116 | 117 | Logout 118 | 119 | 120 | ), 121 | } 122 | 123 | export const WithOptionGroups: Story = args => { 124 | const [sorting, setSorting] = useState('asc') 125 | const [filters, setFilters] = useState(['blue']) 126 | return ( 127 | 128 | 129 | Menu 130 | 131 | 132 | 133 | 134 | Ascending 135 | 136 | 137 | 138 | Descending 139 | 140 | 141 | 142 | 143 | Red 144 | Blue 145 | Green 146 | 147 | 148 | 149 | ) 150 | } 151 | 152 | WithOptionGroups.args = { 153 | closeOnSelect: false, 154 | } 155 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.45.1", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist", 8 | "src" 9 | ], 10 | "private": false, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "engines": { 15 | "node": ">=10" 16 | }, 17 | "scripts": { 18 | "start": "tsdx watch", 19 | "build": "tsdx build", 20 | "ts:check": "tsc --pretty --skipLibCheck --noEmit", 21 | "test": "tsdx test --passWithNoTests", 22 | "lint": "tsdx lint src", 23 | "prepare": "run-s husky:install build", 24 | "size-limit:build": "yarn install --ignore-scripts && yarn run build", 25 | "size": "size-limit", 26 | "analyze": "size-limit --why", 27 | "storybook": "start-storybook -p 6006", 28 | "build-storybook": "build-storybook", 29 | "patch:package": "patch-package", 30 | "husky:install": "husky install", 31 | "release": "np", 32 | "chromatic": "npx chromatic --skip '@(renovate/**|dependabot/**|refs/tags/**)' --project-token CHROMATIC_PROJECT_TOKEN" 33 | }, 34 | "peerDependencies": { 35 | "@emotion/react": "^11.0.0", 36 | "@emotion/styled": "^11.0.0", 37 | "@hookform/resolvers": "^2.0.0", 38 | "@styled-system/css": "^5.0.0", 39 | "framer-motion": ">=6.0.0", 40 | "react": ">=16 || >= 17 || >= 18", 41 | "react-hook-form": "^7.0.0", 42 | "react-icons": "^4.0.0", 43 | "zod": "^3.0.0" 44 | }, 45 | "resolutions": { 46 | "typescript": "4.4.4", 47 | "eslint": "7.32.0", 48 | "prettier": "2.7.1" 49 | }, 50 | "lint-staged": { 51 | "*.md": [ 52 | "npx prettier --parser markdown --write" 53 | ], 54 | "*.css": [ 55 | "npx prettier --parser css --write" 56 | ], 57 | "*.{js,ts,jsx,tsx}": [ 58 | "npx tsdx lint src --fix" 59 | ] 60 | }, 61 | "name": "@liinkiing/klee", 62 | "author": "Omar Jbara ", 63 | "module": "dist/klee.esm.js", 64 | "size-limit": [ 65 | { 66 | "path": "dist/klee.cjs.production.min.js", 67 | "limit": "200 KB" 68 | }, 69 | { 70 | "path": "dist/klee.esm.js", 71 | "limit": "200 KB" 72 | } 73 | ], 74 | "devDependencies": { 75 | "@babel/core": "^7.16.0", 76 | "@emotion/react": "^11.4.0", 77 | "@emotion/styled": "^11.3.0", 78 | "@hookform/resolvers": "^2.5.2", 79 | "@size-limit/preset-small-lib": "^6.0.4", 80 | "@storybook/addon-essentials": "^6.5.9", 81 | "@storybook/addon-info": "^5.3.21", 82 | "@storybook/addon-links": "^6.5.9", 83 | "@storybook/addon-storysource": "^6.5.9", 84 | "@storybook/addon-viewport": "^6.5.9", 85 | "@storybook/addons": "^6.5.9", 86 | "@storybook/builder-webpack5": "^6.5.9", 87 | "@storybook/manager-webpack5": "^6.5.9", 88 | "@storybook/react": "^6.5.9", 89 | "@storybook/theming": "^6.5.9", 90 | "@styled-system/css": "^5.1.5", 91 | "@types/jest": "^27.0.2", 92 | "@types/node": "^16.11.6", 93 | "babel-jest": "^27.3.1", 94 | "babel-loader": "^8.2.3", 95 | "babel-plugin-module-resolver": "^4.1.0", 96 | "chromatic": "^6.0.5", 97 | "eslint-plugin-prettier": "^4.0.0", 98 | "framer-motion": "^6.0.0", 99 | "husky": "^7.0.4", 100 | "np": "^7.5.0", 101 | "npm-run-all": "^4.1.5", 102 | "prettier": "^2.7.1", 103 | "react": "^18.0.0", 104 | "react-dom": "^18.0.0", 105 | "react-hook-form": "^7.8.8", 106 | "react-icons": "^4.2.0", 107 | "size-limit": "^6.0.4", 108 | "storybook-dark-mode": "^1.1.0", 109 | "tsdx": "^0.14.1", 110 | "tslib": "^2.3.1", 111 | "type-fest": "^2.5.2", 112 | "typescript": "^4.4.4", 113 | "webpack": "^5.61.0", 114 | "zod": "^3.7.1" 115 | }, 116 | "dependencies": { 117 | "@emotion/css": "^11.5.0", 118 | "@emotion/serialize": "^1.0.2", 119 | "@liinkiing/react-hooks": "^1.10.3", 120 | "@styled-system/should-forward-prop": "^5.1.5", 121 | "@styled-system/theme-get": "^5.1.2", 122 | "@tippyjs/react": "^4.2.6", 123 | "@types/body-scroll-lock": "^3.1.0", 124 | "@types/react": "^18.0.14", 125 | "@types/react-dom": "^18.0.5", 126 | "@types/styled-system": "^5.1.13", 127 | "@types/styled-system__css": "^5.0.16", 128 | "@types/styled-system__should-forward-prop": "^5.1.2", 129 | "@types/styled-system__theme-get": "^5.0.2", 130 | "body-scroll-lock": "^3.1.5", 131 | "color-hash-ts": "^0.0.7", 132 | "deepmerge": "^4.2.2", 133 | "eslint": "7.32.0", 134 | "font-color-contrast": "^11.1.0", 135 | "lint-staged": "^11.2.6", 136 | "lodash": "^4.17.21", 137 | "lodash-es": "^4.17.21", 138 | "mitt": "^3.0.0", 139 | "patch-package": "^6.4.7", 140 | "polished": "^4.1.3", 141 | "postinstall-postinstall": "^2.1.0", 142 | "react-intersection-observer": "^8.32.2", 143 | "react-no-ssr": "^1.1.0", 144 | "reakit": "^1.3.11", 145 | "styled-system": "^5.1.5", 146 | "tiny-invariant": "^1.2.0", 147 | "tiny-warning": "^1.0.3" 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | concurrency: 4 | group: ci-${{ github.ref }} 5 | cancel-in-progress: true 6 | jobs: 7 | install-cache: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node: ['14.x'] 12 | steps: 13 | - name: Checkout Commit 14 | uses: actions/checkout@v2 15 | - name: Use Node.js ${{ matrix.node }} 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: ${{ matrix.node }} 19 | - name: Cache yarn dependencies 20 | uses: actions/cache@v2 21 | id: cache-dependencies 22 | with: 23 | path: node_modules 24 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 25 | restore-keys: | 26 | ${{ runner.os }}-yarn- 27 | - name: Install Dependencies 28 | if: steps.cache-dependencies.outputs.cache-hit != 'true' 29 | run: | 30 | yarn install --force --non-interactive --ignore-scripts 31 | chromatic-deployment: 32 | runs-on: ubuntu-latest 33 | strategy: 34 | matrix: 35 | node: ['14.x'] 36 | needs: install-cache 37 | steps: 38 | - name: Checkout commit 39 | uses: actions/checkout@v2 40 | with: 41 | fetch-depth: 0 42 | - name: Use Node.js ${{ matrix.node }} 43 | uses: actions/setup-node@v2 44 | with: 45 | node-version: ${{ matrix.node }} 46 | - name: Restore yarn dependencies 47 | uses: actions/cache@v2 48 | id: cache-dependencies 49 | with: 50 | path: node_modules 51 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 52 | restore-keys: | 53 | ${{ runner.os }}-yarn- 54 | - name: Publish to Chromatic 55 | uses: chromaui/action@v1 56 | with: 57 | token: ${{ secrets.GITHUB_TOKEN }} 58 | projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 59 | exitZeroOnChanges: true 60 | lint: 61 | runs-on: ubuntu-latest 62 | strategy: 63 | matrix: 64 | node: ['14.x'] 65 | needs: install-cache 66 | steps: 67 | - name: Checkout Commit 68 | uses: actions/checkout@v2 69 | - name: Use Node.js ${{ matrix.node }} 70 | uses: actions/setup-node@v2 71 | with: 72 | node-version: ${{ matrix.node }} 73 | - name: Restore yarn dependencies 74 | uses: actions/cache@v2 75 | id: cache-dependencies 76 | with: 77 | path: node_modules 78 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 79 | restore-keys: | 80 | ${{ runner.os }}-yarn- 81 | - name: Run lint 82 | run: | 83 | yarn lint 84 | typecheck: 85 | runs-on: ubuntu-latest 86 | strategy: 87 | matrix: 88 | node: ['14.x'] 89 | needs: install-cache 90 | steps: 91 | - name: Checkout Commit 92 | uses: actions/checkout@v2 93 | - name: Use Node.js ${{ matrix.node }} 94 | uses: actions/setup-node@v2 95 | with: 96 | node-version: ${{ matrix.node }} 97 | - name: Restore yarn dependencies 98 | uses: actions/cache@v2 99 | id: cache-dependencies 100 | with: 101 | path: node_modules 102 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 103 | restore-keys: | 104 | ${{ runner.os }}-yarn- 105 | - name: Check types 106 | run: | 107 | yarn ts:check 108 | test: 109 | runs-on: ubuntu-latest 110 | strategy: 111 | matrix: 112 | node: ['14.x'] 113 | needs: install-cache 114 | steps: 115 | - name: Checkout Commit 116 | uses: actions/checkout@v2 117 | - name: Use Node.js ${{ matrix.node }} 118 | uses: actions/setup-node@v2 119 | with: 120 | node-version: ${{ matrix.node }} 121 | - name: Restore yarn dependencies 122 | uses: actions/cache@v2 123 | id: cache-dependencies 124 | with: 125 | path: node_modules 126 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 127 | restore-keys: | 128 | ${{ runner.os }}-yarn- 129 | - name: Run test 130 | run: | 131 | yarn test 132 | build: 133 | runs-on: ubuntu-latest 134 | strategy: 135 | matrix: 136 | node: ['14.x'] 137 | needs: [lint, typecheck, test] 138 | steps: 139 | - name: Checkout Commit 140 | uses: actions/checkout@v2 141 | - name: Use Node.js ${{ matrix.node }} 142 | uses: actions/setup-node@v2 143 | with: 144 | node-version: ${{ matrix.node }} 145 | - name: Restore yarn dependencies 146 | uses: actions/cache@v2 147 | id: cache-dependencies 148 | with: 149 | path: node_modules 150 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 151 | restore-keys: | 152 | ${{ runner.os }}-yarn- 153 | - name: Run build 154 | run: | 155 | yarn build 156 | -------------------------------------------------------------------------------- /src/components/reveal/Reveal.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | import { AnimatePresence, motion, Variant, Variants } from 'framer-motion' 3 | import React, { FC, ReactElement, useEffect } from 'react' 4 | import { useInView } from 'react-intersection-observer' 5 | 6 | import { ease } from '../../utils/motion' 7 | import Box, { BoxProps, PolymorphicComponent } from '../primitives/Box' 8 | 9 | // While tsdx support the new Typescript template litteral types 10 | type Appear = 'from-left' | 'from-top' | 'from-right' | 'from-bottom' 11 | 12 | // type Appear = `from-${'left' | 'top' | 'right' | 'bottom'}` 13 | 14 | export interface RevealProps extends BoxProps { 15 | readonly appear?: Appear 16 | /** 17 | * How much does the children needs to be in the viewport to trigger the reveal? 18 | */ 19 | readonly threshold?: number 20 | readonly delay?: number 21 | readonly duration?: number 22 | /** 23 | * When having multiple children, staggering them means that they will be animated one after one 24 | */ 25 | readonly staggerChildren?: number 26 | readonly initialInView?: boolean 27 | /** 28 | * Does the children will be in a fixed position? 29 | */ 30 | readonly isFixed?: boolean 31 | /** 32 | * If not active, the component will not register any enter/exit in viewport and will no trigger animations 33 | */ 34 | readonly isActive?: boolean 35 | readonly triggerOnce?: boolean 36 | readonly onViewEnter?: () => void 37 | readonly onViewExit?: () => void 38 | } 39 | 40 | const getTransform = (appear: Appear): Variant => { 41 | switch (appear) { 42 | case 'from-left': 43 | return { 44 | y: 0, 45 | x: -20, 46 | } 47 | case 'from-top': 48 | return { 49 | y: -20, 50 | x: 0, 51 | } 52 | case 'from-right': 53 | return { 54 | y: 0, 55 | x: 20, 56 | } 57 | case 'from-bottom': 58 | return { 59 | y: 20, 60 | x: 0, 61 | } 62 | } 63 | } 64 | 65 | const variants: Variants = { 66 | hidden: ({ appear }: { appear: Appear }) => ({ 67 | visibility: 'hidden', 68 | opacity: 0, 69 | ...getTransform(appear), 70 | transition: { 71 | duration: 0, 72 | }, 73 | }), 74 | visible: { 75 | visibility: 'visible', 76 | opacity: 1, 77 | y: 0, 78 | x: 0, 79 | }, 80 | } 81 | 82 | const Container = styled(motion(Box))`` 83 | 84 | // @ts-ignore 85 | export const Reveal: FC = ({ 86 | children, 87 | onViewExit, 88 | onViewEnter, 89 | isFixed = false, 90 | threshold = 0.8, 91 | duration = 1, 92 | delay = 0, 93 | staggerChildren = 0.2, 94 | triggerOnce = true, 95 | initialInView = false, 96 | isActive = true, 97 | appear = 'from-bottom', 98 | ...props 99 | }) => { 100 | const { ref, inView } = useInView({ 101 | triggerOnce, 102 | initialInView, 103 | delay, 104 | threshold, 105 | }) 106 | useEffect(() => { 107 | if (inView) { 108 | onViewEnter?.() 109 | } else { 110 | onViewExit?.() 111 | } 112 | }, [onViewExit, onViewEnter, inView]) 113 | const validChildren = React.Children.toArray(children).filter(c => React.isValidElement(c)) 114 | if (validChildren.length === 0) return null 115 | if (validChildren.length === 1) { 116 | return isActive ? ( 117 | 118 | 139 | {React.cloneElement(validChildren[0] as ReactElement)} 140 | 141 | 142 | ) : ( 143 | children 144 | ) 145 | } 146 | return isActive ? ( 147 | 148 | 163 | {validChildren.map((c, i) => 164 | React.cloneElement( 165 | 166 | {c} 167 | , 168 | { key: i, ...(i === 0 ? { ref } : {}) }, 169 | ), 170 | )} 171 | 172 | 173 | ) : ( 174 | children 175 | ) 176 | } 177 | 178 | export default Reveal as PolymorphicComponent 179 | -------------------------------------------------------------------------------- /src/components/form/Form.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react' 2 | import React from 'react' 3 | import { useForm } from 'react-hook-form' 4 | import { FiGlobe, FiMail, FiPhone, FiUser } from 'react-icons/fi' 5 | import { z } from 'zod' 6 | 7 | import { useZodForm } from '../../hooks' 8 | import { WEBSITE_REGEX } from '../../utils/regex' 9 | import { Icon } from '../icon' 10 | import { Input, InputGroup } from '../input' 11 | import { Flex, VStack } from '../layout' 12 | import { Form } from './Form' 13 | import { FormControl } from './FormControl' 14 | import { SubmitButton } from './SubmitButton' 15 | 16 | const meta: Meta = { 17 | title: 'Library/Forms/Form', 18 | component: Form, 19 | parameters: { 20 | controls: { expanded: true }, 21 | }, 22 | } 23 | 24 | export default meta 25 | 26 | const schema = z.object({ 27 | username: z.string().min(6), 28 | email: z.string().email('The email you entered is invalid'), 29 | website: z.string().regex(WEBSITE_REGEX, 'Please enter a correct url'), 30 | contact: z.object({ 31 | telephone: z 32 | .string() 33 | .regex(/^(?:(?:\+|00)33|0)\s*[1-9](?:[\s.-]*\d{2}){4}$/, 'Veuillez entrer un numéro de téléphone valide'), 34 | }), 35 | }) 36 | 37 | export const Default = () => { 38 | const form = useForm({ 39 | mode: 'all', 40 | }) 41 | return ( 42 | { 46 | alert(JSON.stringify(values, null, 2)) 47 | }} 48 | > 49 | 50 | 51 | Username 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | Email 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Website 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Submit 80 | 81 | 82 | ) 83 | } 84 | 85 | Default.args = {} 86 | 87 | export const WithZodValidation = () => { 88 | const form = useZodForm({ 89 | schema, 90 | mode: 'all', 91 | }) 92 | return ( 93 |
{ 97 | alert(JSON.stringify(values, null, 2)) 98 | }} 99 | > 100 | 101 | 102 | Username 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | Email 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | Website 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | Telephone 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | Submit 140 | 141 |
142 | ) 143 | } 144 | 145 | WithZodValidation.args = {} 146 | -------------------------------------------------------------------------------- /src/components/modal/Modal.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react' 2 | import React from 'react' 3 | 4 | import { Button } from '../button' 5 | import { Heading, Text } from '../typography' 6 | import Modal from './Modal' 7 | import { ModalProps } from './Modal' 8 | 9 | const meta: Meta = { 10 | title: 'Library/Modal', 11 | component: Modal, 12 | argTypes: { 13 | overlay: { table: { disable: true }, control: { disable: true } }, 14 | disclosure: { table: { disable: true }, control: { disable: true } }, 15 | }, 16 | args: { 17 | showOnCreate: true, 18 | }, 19 | parameters: { 20 | controls: { expanded: true }, 21 | }, 22 | } 23 | 24 | export default meta 25 | 26 | const Template: Story = args => ( 27 | Open}> 28 | {({ hide }) => ( 29 | <> 30 | 31 | Hello Klee 32 | 33 | 34 | How are you doing? 35 | 36 | 37 | 46 | 55 | 56 | 57 | )} 58 | 59 | ) 60 | 61 | export const Default = Template.bind({}) 62 | 63 | Default.args = { 64 | ariaLabel: 'Modal example', 65 | } 66 | 67 | export const WithCustomization = Template.bind({}) 68 | 69 | WithCustomization.args = { 70 | ariaLabel: 'Modal example', 71 | bgGradient: ['linear(to top, amber.600, amber.500)', 'linear(to top, cyan.600, cyan.500)'], 72 | color: 'white', 73 | width: ['100%', '300px'], 74 | } 75 | 76 | export const WithLongContent: Story = args => ( 77 | Open}> 78 | 79 | Hello Klee 80 | 81 | 82 | I have something very long to tell you... 83 | 84 | But first, you can change how the modal will be scrolled by using the scrollBehavior prop. 85 | 86 | 87 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias dolorem explicabo, hic libero minima 88 | repellendus. Aut corporis deleniti expedita ipsa libero possimus temporibus voluptatem voluptatibus? Dolorem eum 89 | iure laborum minima. 90 | 91 | 92 | Accusantium aliquid amet commodi delectus deserunt distinctio dolorem est excepturi explicabo fugit laborum 93 | maiores modi molestias mollitia nesciunt nisi officia optio quas qui quos similique, sit sunt tempore ut vero. 94 | 95 | 96 | Consequatur dicta doloremque ea enim ex explicabo, facilis, id inventore itaque labore minima neque pariatur 97 | perferendis, quasi totam! Consequatur harum ipsum laborum minus quasi ratione ullam velit. Asperiores explicabo, 98 | modi. 99 | 100 | 101 | Accusamus aliquam animi beatae consectetur debitis dolorem error esse est expedita fugiat id illum, impedit ipsa 102 | ipsam iusto libero molestias, necessitatibus obcaecati officia praesentium provident quaerat quia quidem 103 | quisquam vero. 104 | 105 | 106 | Expedita iure reiciendis sapiente soluta voluptate. Aspernatur, consequuntur dignissimos dolorem eaque enim hic 107 | illum iure laudantium libero magnam minima minus officia praesentium quisquam, quos repellat soluta veniam, 108 | voluptas voluptates voluptatum? 109 | 110 | 111 | Expedita iure reiciendis sapiente soluta voluptate. Aspernatur, consequuntur dignissimos dolorem eaque enim hic 112 | illum iure laudantium libero magnam minima minus officia praesentium quisquam, quos repellat soluta veniam, 113 | voluptas voluptates voluptatum? 114 | 115 | 116 | Expedita iure reiciendis sapiente soluta voluptate. Aspernatur, consequuntur dignissimos dolorem eaque enim hic 117 | illum iure laudantium libero magnam minima minus officia praesentium quisquam, quos repellat soluta veniam, 118 | voluptas voluptates voluptatum? 119 | 120 | 121 | Expedita iure reiciendis sapiente soluta voluptate. Aspernatur, consequuntur dignissimos dolorem eaque enim hic 122 | illum iure laudantium libero magnam minima minus officia praesentium quisquam, quos repellat soluta veniam, 123 | voluptas voluptates voluptatum? 124 | 125 | 126 | Expedita iure reiciendis sapiente soluta voluptate. Aspernatur, consequuntur dignissimos dolorem eaque enim hic 127 | illum iure laudantium libero magnam minima minus officia praesentium quisquam, quos repellat soluta veniam, 128 | voluptas voluptates voluptatum? 129 | 130 | 131 | 132 | ) 133 | 134 | WithLongContent.args = { 135 | ...Default.args, 136 | } 137 | -------------------------------------------------------------------------------- /src/components/toast/ToastContainer.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | import css from '@styled-system/css' 3 | import { motion } from 'framer-motion' 4 | import * as React from 'react' 5 | import { useEffect, useState } from 'react' 6 | import NoSSR from 'react-no-ssr' 7 | import { Portal } from 'reakit/Portal' 8 | 9 | import { KleeZIndex } from '../../styles/theme' 10 | import { Emitter, UIEvents } from '../../utils/emitter' 11 | import { LAYOUT_TRANSITION_SPRING } from '../../utils/motion' 12 | import { Box } from '../primitives' 13 | import { Toast, ToastProps } from './Toast' 14 | 15 | const ToastWrapper = styled(motion(Box))( 16 | css({ 17 | width: ['100%', 'auto'], 18 | }), 19 | ) 20 | 21 | const ToastContainerInner = styled(Box)( 22 | css({ 23 | position: 'fixed', 24 | pointerEvents: 'none', 25 | zIndex: KleeZIndex.Toast, 26 | }), 27 | ) 28 | 29 | interface FilterByPositionParams { 30 | reverse?: boolean 31 | toasts: ToastProps[] 32 | onHide: ToastProps['onHide'] 33 | placement: ToastProps['placement'] 34 | } 35 | 36 | const filterByPosition = ({ toasts, placement, onHide, reverse = false }: FilterByPositionParams) => { 37 | let results = toasts.filter(n => n.placement === placement) 38 | 39 | if (reverse) { 40 | results = results.reverse() 41 | } 42 | 43 | return results.map((n, i) => ( 44 | 52 | 53 | 54 | )) 55 | } 56 | 57 | export const ToastsContainer = () => { 58 | const [toasts, setToasts] = useState([]) 59 | const clearToast = (id: string) => { 60 | setToasts(v => v.filter(toast => toast.id !== id)) 61 | } 62 | useEffect(() => { 63 | const handler = (toast?: ToastProps) => { 64 | if (!toast) return 65 | setToasts(n => [...n, toast]) 66 | } 67 | 68 | Emitter.on(UIEvents.ToastShow, handler) 69 | 70 | return () => { 71 | Emitter.off(UIEvents.ToastShow, handler) 72 | } 73 | }, []) 74 | 75 | const visible = toasts.length > 0 76 | 77 | return ( 78 | 79 | 80 | 90 | {filterByPosition({ 91 | toasts, 92 | onHide: clearToast, 93 | placement: 'top', 94 | })} 95 | 96 | 106 | {filterByPosition({ 107 | toasts, 108 | onHide: clearToast, 109 | placement: 'top-left', 110 | })} 111 | 112 | 122 | {filterByPosition({ 123 | toasts, 124 | onHide: clearToast, 125 | placement: 'top-right', 126 | })} 127 | 128 | 138 | {filterByPosition({ 139 | toasts, 140 | onHide: clearToast, 141 | placement: 'bottom', 142 | reverse: true, 143 | })} 144 | 145 | 155 | {filterByPosition({ 156 | toasts, 157 | onHide: clearToast, 158 | placement: 'bottom-left', 159 | reverse: true, 160 | })} 161 | 162 | 172 | {filterByPosition({ 173 | toasts, 174 | onHide: clearToast, 175 | placement: 'bottom-right', 176 | reverse: true, 177 | })} 178 | 179 | 180 | 181 | ) 182 | } 183 | 184 | export default ToastsContainer 185 | --------------------------------------------------------------------------------