diff options
author | Geireann Lindfield Roberts <geireann_lindfield_roberts@brown.edu> | 2025-01-01 22:32:19 -0800 |
---|---|---|
committer | Geireann Lindfield Roberts <geireann_lindfield_roberts@brown.edu> | 2025-01-01 22:32:19 -0800 |
commit | 18774b42e3c8e1e899978fe9f16a4d123adee803 (patch) | |
tree | fe538b147632fcb71dd27403231c6610822e8fbc /packages/components/src | |
parent | 96aff4f0c900a9e13a1a4916c6d4d945c544060c (diff) |
monorepo setup
Diffstat (limited to 'packages/components/src')
83 files changed, 4777 insertions, 0 deletions
diff --git a/packages/components/src/components/Button/Button.scss b/packages/components/src/components/Button/Button.scss new file mode 100644 index 000000000..a31923e6d --- /dev/null +++ b/packages/components/src/components/Button/Button.scss @@ -0,0 +1,118 @@ +@import '../../global/globalCssVariables.scss'; + +.button-container { + position: relative; + width: fit-content; + padding: $padding; + cursor: pointer; + overflow: hidden; + user-select: none; + display: flex; + justify-content: center; + align-items: center; + gap: 5px; + font-family: $default-font; + border-radius: $standard-border-radius; + white-space: nowrap; + transition: 0.4s; + border: solid 1px; + border-color: transparent; + pointer-events: all; + + &.icon { + padding: 0; + gap: 0; + } + + .button-content { + display: flex; + justify-content: center; + align-items: center; + width: fit-content; + height: 100%; + z-index: 1; + gap: 5px; + + .icon { + display: flex; + justify-content: center; + align-items: center; + } + } + + .background { + width: 100%; + height: 100%; + z-index: 0; + left: 0; + top: 0; + position: absolute; + transition: 0.4s; + } + + &.inactive { + &:hover { + .background { + filter: opacity(0) !important; + } + } + } + + &.primary { + .background { + filter: opacity(0); + + &.active { + filter: opacity(0.2) !important; + } + } + + &:hover{ + .background { + filter: opacity(0.2) + } + } + } + + &.secondary { + .background { + filter: opacity(0); + + &.active { + filter: opacity(0.2) !important; + } + } + + &:hover{ + .background { + filter: opacity(0.2) + } + } + } + + &.tertiary { + &:hover{ + box-shadow: $standard-shadow; + } + + .background { + filter: opacity(1) !important; + } + + &:hover{ + .background { + filter: brightness(0.8); + } + } + } + + .label { + position: absolute; + bottom: 0; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + font-size: $xsmall-fontSize; + } +} diff --git a/packages/components/src/components/Button/Button.stories.tsx b/packages/components/src/components/Button/Button.stories.tsx new file mode 100644 index 000000000..3893d9ded --- /dev/null +++ b/packages/components/src/components/Button/Button.stories.tsx @@ -0,0 +1,94 @@ +import { Meta, Story } from '@storybook/react' +import React from 'react' +import * as bi from 'react-icons/bi' +import { Button, IButtonProps } from '..' +import { Colors, Size } from '../../global/globalEnums' +import { Type , getFormLabelSize } from '../../global' + +export default { + title: 'Dash/Button', + component: Button, + argTypes: {}, +} as Meta<typeof Button> + +const Template: Story<IButtonProps> = (args) => <Button {...args} /> + +export const Primary = Template.bind({}) +Primary.args = { + onClick: () => {}, + text: 'Primary', + type: Type.PRIM, + style: { + fontWeight: 600 + }, + tooltip: 'Primary button' +} + +export const Secondary = Template.bind({}) +Secondary.args = { + onClick: () => {}, + text: 'Secondary', + type: Type.SEC, + tooltip: 'Secondary button' +} + +export const Tertiary = Template.bind({}) +Tertiary.args = { + onClick: () => {}, + text: 'Tertiary', + type: Type.TERT, + size: Size.SMALL, +} + +export const Small = Template.bind({}) +Small.args = { + onClick: () => {}, + text: 'Small', + type: Type.PRIM, + size: Size.SMALL, +} + +export const Medium = Template.bind({}) +Medium.args = { + onClick: () => {}, + text: 'Medium', + type: Type.PRIM, + size: Size.MEDIUM, +} + +export const Large = Template.bind({}) +Large.args = { + onClick: () => {}, + text: 'Large', + type: Type.PRIM, + size: Size.LARGE, +} + +export const ButtonWithLeftIcon = Template.bind({}) +ButtonWithLeftIcon.args = { + onClick: () => {}, + text: 'New', + icon: <bi.BiPlus />, + iconPosition: 'left', + type: Type.PRIM, +} + +export const ButtonWithRightIcon = Template.bind({}) +ButtonWithRightIcon.args = { + onClick: () => {}, + text: 'More', + iconPosition: 'right', + icon: <bi.BiMobile />, + type: Type.PRIM, +} + +export const Label = Template.bind({}) +Label.args = { + onClick: () => {}, + text: 'Label', + type: Type.PRIM, + style: { + fontWeight: 600 + }, + tooltip: 'Label button' +}
\ No newline at end of file diff --git a/packages/components/src/components/Button/Button.tsx b/packages/components/src/components/Button/Button.tsx new file mode 100644 index 000000000..a91c74a4c --- /dev/null +++ b/packages/components/src/components/Button/Button.tsx @@ -0,0 +1,195 @@ +import { Tooltip } from '@mui/material' +import React from 'react' +import { Alignment, IGlobalProps, Placement, Type , getFormLabelSize } from '../../global' +import { Colors, Size } from '../../global/globalEnums' +import { getFontSize, getHeight, isDark } from '../../global/globalUtils' +import { IconButton } from '../IconButton' +import './Button.scss' + +export interface IButtonProps extends IGlobalProps { + onClick?: (event: React.MouseEvent) => void + onDoubleClick?: (event: React.MouseEvent) => void + type?: Type + active?: boolean + + // Content + text?: string + icon?: JSX.Element | string + + // Additional stylization + iconPlacement?: Placement + color?: string + colorPicker?: string, + uppercase?: boolean, + align?: Alignment +} + +export const Button = (props: IButtonProps) => { + const { + text, + icon, + onClick, + onDoubleClick, + onPointerDown, + active, + height, + inactive, + type = Type.PRIM, + label, + uppercase = false, + iconPlacement = 'right', + size = Size.SMALL, + color = Colors.MEDIUM_BLUE, + background, + style, + tooltip, + tooltipPlacement = 'top', + colorPicker, + formLabel, + formLabelPlacement, + fillWidth, + align = fillWidth ? 'flex-start' : 'center' + } = props + + if (!text) { + return <IconButton {...props}/> + } + + /** + * Pointer down + * @param e + */ + const handlePointerDown = (e: React.PointerEvent) => { + + if (!inactive && onPointerDown) { + e.stopPropagation(); + e.preventDefault(); + onPointerDown(e) + } + } + + /** + * In the event that there is a single click + * @param e + */ + const handleClick = (e: React.MouseEvent) => { + if (!inactive && onClick) { + e.stopPropagation(); + e.preventDefault(); + onClick(e) + } + } + + /** + * Double click + * @param e + */ + const handleDoubleClick = (e: React.MouseEvent) => { + if (!inactive && onDoubleClick){ + e.stopPropagation(); + e.preventDefault(); + onDoubleClick(e) + } + } + + const getBorderColor = (): Colors | string | undefined => { + switch(type){ + case Type.PRIM: + return undefined; + case Type.SEC: + if (colorPicker) return colorPicker; + return color; + case Type.TERT: + if (colorPicker) return colorPicker; + if (active) return color; + else return color; + } + } + + const getColor = (): Colors | string | undefined => { + if (color && background) return color; + switch(type){ + case Type.PRIM: + if (colorPicker) return colorPicker + return color; + case Type.SEC: + if (colorPicker) return colorPicker + return color; + case Type.TERT: + if (colorPicker) { + if (isDark(colorPicker)) return Colors.WHITE; + else return Colors.BLACK + } + if (isDark(color)) return Colors.WHITE; + else return Colors.BLACK + } + } + + const getBackground = (): Colors | string | undefined => { + if (background) return background; + switch(type) { + case Type.PRIM: + if (colorPicker) return colorPicker + return color; + case Type.SEC: + if (colorPicker) return colorPicker + return color; + case Type.TERT: + if (colorPicker) return colorPicker + else return color + } + } + + const defaultProperties: React.CSSProperties = { + height: getHeight(height, size), + minHeight: getHeight(height, size), + width: fillWidth ? '100%' : 'fit-content', + justifyContent: align ? align : undefined, + padding: fillWidth && align === 'center' ? 0 : undefined, + fontWeight: 500, + fontSize: getFontSize(size), + fontFamily: 'sans-serif', + textTransform: uppercase ? 'uppercase' : undefined, + borderColor: getBorderColor(), + color: getColor(), + } + + const backgroundProperties: React.CSSProperties = { + background: getBackground() + } + + const button: JSX.Element = ( + <Tooltip disableInteractive={true} arrow={true} placement={tooltipPlacement} title={tooltip}> + <div + className={`button-container ${type} ${active && 'active'} ${inactive && 'inactive'}`} + onClick={handleClick} + onDoubleClick={handleDoubleClick} + onPointerDown={handlePointerDown} + style={{...defaultProperties, ...style}} + > + <div className={`button-content`} + style={{justifyContent: align}} + > + {iconPlacement == 'left' && icon ? <div className={`icon`} style={{ + fontSize: getFontSize(size, true) + }}>{icon}</div> : null} + {text} + {iconPlacement == 'right' && icon ? <div className={`icon`} style={{ + fontSize: getFontSize(size, true) + }}>{icon}</div> : null} + </div> + <div className={`background ${active && 'active'}`} style={backgroundProperties}/> + </div> + </Tooltip> + ) + + return ( + formLabel ? + <div className={`form-wrapper ${formLabelPlacement}`} style={{ width: fillWidth ? '100%' : undefined}}> + <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> + {button} + </div> + : + button + ) +} diff --git a/packages/components/src/components/Button/index.ts b/packages/components/src/components/Button/index.ts new file mode 100644 index 000000000..8486fd6d6 --- /dev/null +++ b/packages/components/src/components/Button/index.ts @@ -0,0 +1 @@ +export * from './Button' diff --git a/packages/components/src/components/ColorPicker/ColorPicker.scss b/packages/components/src/components/ColorPicker/ColorPicker.scss new file mode 100644 index 000000000..e3ed32a45 --- /dev/null +++ b/packages/components/src/components/ColorPicker/ColorPicker.scss @@ -0,0 +1,23 @@ +@import '../../global/globalCssVariables.scss'; + +.colorPicker-container { + display: flex; + border-radius: $standard-border-radius; + width: fit-content; + height: fit-content; + position: relative; + + .colorPicker-toggle { + width: 100%; + height: 100%; + position: relative; + cursor: pointer; + } + + .colorPicker-popup { + position: absolute; + top: calc(100% + 5px); + width: fit-content; + height: fit-content; + } +} diff --git a/packages/components/src/components/ColorPicker/ColorPicker.stories.tsx b/packages/components/src/components/ColorPicker/ColorPicker.stories.tsx new file mode 100644 index 000000000..5b9eb93f9 --- /dev/null +++ b/packages/components/src/components/ColorPicker/ColorPicker.stories.tsx @@ -0,0 +1,37 @@ +import { Meta, Story } from '@storybook/react' +import React from 'react' +import * as fa from 'react-icons/fa' +import { Type , getFormLabelSize } from '../../global' +import { ColorPicker, IColorPickerProps } from './ColorPicker' + +export default { + title: 'Dash/Color Picker', + component: ColorPicker, + argTypes: {}, +} as Meta<typeof ColorPicker> + +const Template: Story<IColorPickerProps> = (args) => <ColorPicker {...args} /> + +export const Primary = Template.bind({}) +Primary.args = { + text: 'Background', + icon: <fa.FaPaintBrush />, + type: Type.PRIM, + onChange: (color) => { + console.log(color) + }, + defaultPickerType: "Slider", + color: "black", + tooltip: 'Choose your color' +} + +export const Icon = Template.bind({}) +Icon.args = { + icon: <fa.FaPaintBrush />, + type: Type.SEC, + onChange: (color) => { + console.log(color) + }, + color: "black", + tooltip: 'Choose your color' +} diff --git a/packages/components/src/components/ColorPicker/ColorPicker.tsx b/packages/components/src/components/ColorPicker/ColorPicker.tsx new file mode 100644 index 000000000..51b820a37 --- /dev/null +++ b/packages/components/src/components/ColorPicker/ColorPicker.tsx @@ -0,0 +1,204 @@ +import React, { useState } from 'react' +import { GithubPicker, ChromePicker, BlockPicker, SliderPicker, SketchPicker } from 'react-color' +import { IGlobalProps, Size, Type , getFormLabelSize } from '../../global' +import { Button } from '../Button' +import { IconButton } from '../IconButton' +import { Popup, PopupTrigger } from '../Popup' +import './ColorPicker.scss' +import { Dropdown, DropdownType } from '../Dropdown' + +export const ColorPickerArray= ["Classic", "Chrome", "GitHub", "Block", "Slider"] +export type ColorPickerType= typeof ColorPickerArray[number]; + +export interface IColorPickerProps extends IGlobalProps { + text?: string + icon?: JSX.Element | string + colorPickerType?: ColorPickerType + defaultPickerType?: ColorPickerType + selectedColor?: string + setSelectedColor: (color: any) => unknown + setFinalColor: (color:any) => unknown +} + +export const ColorPicker = (props: IColorPickerProps) => { + const [selectedColorLoc, setSelectedColorLoc] = useState(); + const { defaultPickerType, text, colorPickerType, fillWidth, formLabelPlacement, size = Size.SMALL, type = Type.TERT, icon, selectedColor = selectedColorLoc, setSelectedColor = setSelectedColorLoc, setFinalColor = setSelectedColorLoc, tooltip, color='black', formLabel } = props + const [isOpen, setOpen] = useState<boolean>(false) + const [pickerSelectorOpen, setPickerSelectorOpen] = useState<boolean>(false); + const decimalToHexString = (number: number) => { + if (number < 0) { + number = 0xffffffff + number + 1; + } + return (number < 16 ? '0' : '') + number.toString(16).toUpperCase(); +} + const colorString = (color: any ) => { + return color.hex === 'transparent' ? color.hex: color.hex + (color.rgb.a ? decimalToHexString(Math.round(color.rgb.a * 255)) : 'ff'); + } + const onChange = (color: any) => { + setSelectedColor(colorString(color) as any); + } + const onChangeComplete = (color: any) => { + setFinalColor(colorString(color) as any); + } + const [picker, setPicker] = useState<string>(defaultPickerType ?? "Classic") + + const getToggle = () => { + if (icon && !text) { + return ( + <IconButton + active={isOpen} + tooltip={tooltip} + type={type} + color={color} + size={size} + icon={icon} + colorPicker={selectedColor} + fillWidth={fillWidth} + /> + ) + } else if (text) { + return ( + <Button + active={isOpen} + tooltip={tooltip} + size={size} + type={type} + color={color} + text={text} + icon={icon} + align={'flex-start'} + iconPlacement={'left'} + colorPicker={selectedColor} + fillWidth={fillWidth} + /> + ) + } else { + return ( + <IconButton + active={isOpen} + tooltip={tooltip} + type={type} + color={color} + size={size} + icon={icon} + colorPicker={selectedColor} + fillWidth={fillWidth} + /> + ) + } + } + + const getColorPicker = (pickerType: ColorPickerType):JSX.Element => { + const colorPalette = ["FFFFFF", "#F9F6F2", "#E2E2E2", "#D1D1D1", "#737576", "#4b4a4d", "#222021", + '#EB9694', '#FAD0C3', '#FEF3BD', '#C1E1C5', '#BEDADC', '#C4DEF6', '#BED3F3', '#D4C4FB', "transparent" + ] + switch(pickerType) { + case "Block": + return ( + <BlockPicker + color={selectedColor} + triangle={'hide'} + colors={colorPalette} + onChange={onChange} + onChangeComplete={onChangeComplete} + /> + ); + case "Classic": + return (<SketchPicker + onChange={onChange} + onChangeComplete={onChangeComplete} + presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} + color={selectedColor} + />); + case "Chrome": + default: + return ( + <ChromePicker + color={selectedColor} + onChange={onChange} + onChangeComplete={onChangeComplete} + /> + ); + case "GitHub": + return ( + <GithubPicker + color={selectedColor} + colors={colorPalette} + triangle={'hide'} + onChange={onChange} + onChangeComplete={onChangeComplete} + /> + ); + case "Slider": + return ( + <div style={{width: 200, height: 50}}> + <SliderPicker + color={selectedColor} + onChange={onChange} + onChangeComplete={onChangeComplete} + /> + </div> + ); + } + } + const openChanged = (isOpen:boolean) => setPickerSelectorOpen(isOpen); + + const getPopup = ():JSX.Element => { + if (colorPickerType){ + return getColorPicker(colorPickerType) + } else { + // Todo: this would be much easier if the selectedColor was a Color, not a string. + const newColor = (selectedColor === 'transparent' ? 'white': selectedColor?.startsWith("#") ? selectedColor.substring(0,7): + selectedColor?.startsWith('rgba') ? selectedColor?.replace(/,[0-9]*\)/,"1)") : selectedColor); + return <div style={{height: 'fit-content'}}> + <Dropdown + items={ + ColorPickerArray.map((item) => { + return { + text: item, + val: item, + } + }) + } + activeChanged={openChanged} + placement={'right'} + color={newColor} + type={Type.PRIM} + dropdownType={DropdownType.SELECT} + selectedVal={picker} + setSelectedVal={(val) => setPicker(val as string)} + fillWidth + /> + {getColorPicker(picker)} + </div> + } + } + + const popupContainsPt = (x:number, y:number) => pickerSelectorOpen; + + const colorPicker: JSX.Element = + ( + <Popup + toggle={getToggle()} + trigger={PopupTrigger.CLICK} + isOpen={isOpen} + setOpen={setOpen} + tooltip={tooltip} + size={size} + color={selectedColor} + popup={getPopup()} + popupContainsPt={popupContainsPt} // this should prohbably test to see if the click pt is actually within the picker selector list popup. + /> + ) + + return ( + formLabel ? + <div className={`form-wrapper ${formLabelPlacement}`} +style={{ width: fillWidth ? '100%' : undefined}}> + <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> + {colorPicker} + </div> + : + colorPicker + ) +} diff --git a/packages/components/src/components/ColorPicker/index.ts b/packages/components/src/components/ColorPicker/index.ts new file mode 100644 index 000000000..24ed8bf67 --- /dev/null +++ b/packages/components/src/components/ColorPicker/index.ts @@ -0,0 +1 @@ +export * from './ColorPicker' diff --git a/packages/components/src/components/Dropdown/Dropdown.scss b/packages/components/src/components/Dropdown/Dropdown.scss new file mode 100644 index 000000000..34ed84004 --- /dev/null +++ b/packages/components/src/components/Dropdown/Dropdown.scss @@ -0,0 +1,135 @@ +@import '../../global/globalCssVariables.scss'; + +.dropdown { + margin-top: 10px; +} + +.divider { + height: 1px; + width: 100%; + background: $medium-gray; +} + +.dropdown-container { + display: flex; + flex-direction: column; + justify-content: center; + min-width: fit-content; + width: 100%; + border-radius: $standard-border-radius; + height: 100%; + position: relative; + transition: 0.4s; + + .dropdown-list { + position: absolute; + top: 100%; + width: 100%; + } + .dropdown-toggle-mini, + .dropdown-toggle { + width: calc(100% - 2px); + display: grid; + grid-template-columns: calc(100% - 30px) 30px; + grid-template-areas: 'button end'; + grid-template-rows: 1fr; + position: relative; + align-items: center; + border: solid 1px; + border-color: transparent; + border-radius: $standard-border-radius; + overflow: hidden; + + &.inactive { + filter: opacity(0.5); + pointer-events: none; + cursor: not-allowed; + } + + .background { + width: 100%; + height: 100%; + z-index: 0; + position: absolute; + transition: 0.4s; + } + + &.inactive { + &:hover { + .background { + filter: opacity(0) !important; + } + } + } + + &.primary { + .background { + filter: opacity(0); + + &.active { + filter: opacity(0.2) !important; + } + } + + &:hover{ + .background { + filter: opacity(0.2) + } + } + } + + &.secondary { + .background { + filter: opacity(0); + + &.active { + filter: opacity(0.2) !important; + } + } + + &:hover{ + .background { + filter: opacity(0.2) + } + } + } + + &.tertiary { + &:hover{ + box-shadow: $standard-button-shadow; + } + + &:hover{ + .background { + filter: brightness(0.8); + } + } + } + + .toggle-button { + grid-area: button; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + min-width: 70px; + justify-self: center; + } + + .toggle-caret { + cursor: pointer; + grid-area: end; + display: flex; + justify-content: flex-end; + align-items: center; + justify-self: center; + } + } + .dropdown-toggle-mini { + .toggle-caret { + position: absolute; + top:0; left:0; + } + } +} diff --git a/packages/components/src/components/Dropdown/Dropdown.stories.tsx b/packages/components/src/components/Dropdown/Dropdown.stories.tsx new file mode 100644 index 000000000..820003c99 --- /dev/null +++ b/packages/components/src/components/Dropdown/Dropdown.stories.tsx @@ -0,0 +1,84 @@ +import { Meta, Story } from '@storybook/react' +import React from 'react' +import * as fa from 'react-icons/fa' +import { Dropdown, DropdownType, IDropdownProps } from '..' +import { Colors, Size } from '../../global/globalEnums' +import { IListItemProps } from '../ListItem' +import { Type , getFormLabelSize } from '../../global' + +export default { + title: 'Dash/Dropdown', + component: Dropdown, + argTypes: {}, +} as Meta<typeof Dropdown> + +const Template: Story<IDropdownProps> = (args) => <Dropdown {...args} /> +const dropdownItems: IListItemProps[] = [ + { + text: 'Facebook Marketplace', + val: 'facebook-marketplace', + shortcut: '⌘F', + icon: <fa.FaFacebook />, + description: 'This is the main component that we use in Dash.', + }, + { + text: 'Google', + val: 'google', + }, + { + text: 'Airbnb', + val: 'airbnb', + icon: <fa.FaAirbnb />, + }, + { + text: 'Salesforce', + val: 'salesforce', + icon: <fa.FaSalesforce />, + items: [ + { + text: 'Slack', + val: 'slack', + icon: <fa.FaSlack />, + }, + { + text: 'Heroku', + val: 'heroku', + shortcut: '⌘H', + icon: <fa.FaAirFreshener />, + }, + ], + }, + { + text: 'Microsoft', + val: 'microsoft', + icon: <fa.FaMicrosoft />, + }, +] + +export const Select = Template.bind({}) +Select.args = { + title: 'Select company', + tooltip: "This should be a tooltip", + type: Type.PRIM, + dropdownType: DropdownType.SELECT, + items: dropdownItems, + size: Size.SMALL, + selectedVal: 'facebook-marketplace', + background: 'blue', + color: Colors.WHITE +} + +export const Click = Template.bind({}) +Click.args = { + title: '', + type: Type.TERT, + color: 'red', + background: 'blue', + dropdownType: DropdownType.SELECT, + items: dropdownItems, + closeOnSelect: true, + size: Size.XSMALL, + setSelectedVal: (val) => console.log("SET sel = "+ val), + onItemDown: (e, val) => { console.log("ITEM DOWN" + val); return true; } + //color: Colors.SUCCESS_GREEN +} diff --git a/packages/components/src/components/Dropdown/Dropdown.tsx b/packages/components/src/components/Dropdown/Dropdown.tsx new file mode 100644 index 000000000..d9fec5e9d --- /dev/null +++ b/packages/components/src/components/Dropdown/Dropdown.tsx @@ -0,0 +1,225 @@ +import React, { useEffect, useState } from 'react' +import { FaCaretDown, FaCaretLeft, FaCaretRight, FaCaretUp } from 'react-icons/fa' +import { Popup, PopupTrigger } from '..' +import { Colors, IGlobalProps, Placement, Type, getFontSize, getHeight, isDark , getFormLabelSize } from '../../global' +import { IconButton } from '../IconButton' +import { ListBox } from '../ListBox' +import { IListItemProps, ListItem } from '../ListItem' +import './Dropdown.scss' +import { Tooltip } from '@mui/material' + +export enum DropdownType { + SELECT = "select", + CLICK = "click" +} + +export interface IDropdownProps extends IGlobalProps { + items: IListItemProps[] + placement?: Placement + dropdownType: DropdownType + title?: string + closeOnSelect?: boolean; + iconProvider?: (active:boolean, placement?:Placement) => JSX.Element, + selectedVal?: string, + setSelectedVal?: (val: string | number) => unknown, + maxItems?: number, + uppercase?: boolean, + activeChanged?: (isOpen:boolean) => void, + onItemDown?: (e:React.PointerEvent, val:number | string) => boolean, // returns whether to select item +} + +/** + * + * @param props + * @returns + * + * TODO: add support for isMulti, isSearchable + * Look at: import Select from "react-select"; + */ +export const Dropdown = (props: IDropdownProps) => { + const { + size, + height, + maxItems, + items, + dropdownType, + selectedVal, + setSelectedVal, + iconProvider, + placement = 'bottom-start', + tooltip, + tooltipPlacement = 'top', + inactive, + color = Colors.MEDIUM_BLUE, + background, + closeOnSelect, + title = "Dropdown", + type, + width, + formLabel, + formLabelPlacement, + fillWidth = true, + onItemDown, + uppercase + } = props + + const [active, setActive] = useState<boolean>(false) + const itemsMap = new Map(); + items.forEach((item) => { + itemsMap.set(item.val, item) + }) + + const getBorderColor = (): Colors | string | undefined => { + switch(type){ + case Type.PRIM: + return undefined; + case Type.SEC: + return color; + case Type.TERT: + if (active) return color; + else return color; + } + } + + const defaultProperties: React.CSSProperties = { + height: getHeight(height, size), + width: fillWidth ? '100%' : width, + fontWeight: 500, + fontSize: getFontSize(size), + fontFamily: 'sans-serif', + textTransform: uppercase ? 'uppercase' : undefined, + borderColor: getBorderColor(), + background, + color: color && background? color : type == (Type.TERT) ? isDark(color) ? Colors.WHITE : Colors.BLACK : color + } + + const backgroundProperties: React.CSSProperties = { + background: background ?? color + } + + const getCaretDirection = (active: boolean, placement:Placement = 'left'): JSX.Element => { + if (iconProvider) return iconProvider(active, placement); + switch (placement) { + case 'bottom': + if (active) return <FaCaretUp/> + return <FaCaretDown/> + case 'right': + if (active) return <FaCaretLeft/> + return <FaCaretRight/> + case 'top': + if (active) return <FaCaretDown/> + return <FaCaretUp/> + default: + if (active) return <FaCaretUp/> + return <FaCaretDown/> + } + } + + const getToggle = () => { + switch (dropdownType) { + case DropdownType.SELECT: + return ( + <div + className={`dropdown-toggle${!selectedVal?"-mini":""} ${type} ${inactive && 'inactive'}`} + style={{...defaultProperties, height: getHeight(height, size), width: width }} + > + {selectedVal && ( + <ListItem + size={size} + {...itemsMap.get(selectedVal)} + style={{ color: defaultProperties.color, background: defaultProperties.background}} + inactive + /> + )} + <div className="toggle-caret"> + <IconButton + size={size} + icon={getCaretDirection(active,placement)} + color={defaultProperties.color} + inactive + /> + </div> + <div className={`background ${active && 'active'}`} style={{...backgroundProperties}}/> + </div> + ) + case DropdownType.CLICK: + default: + return ( + <div + className={`dropdown-toggle${!selectedVal?"-mini":""} ${type} ${inactive && 'inactive'}`} + style={{...defaultProperties, height: getHeight(height, size), width: width }} + > + <ListItem val='title' + text={title} + size={size} + style={{ color: defaultProperties.color, background: defaultProperties.backdropFilter}} + inactive + /> + <div className="toggle-caret"> + <IconButton + size={size} + icon={getCaretDirection(active,placement)} + color={defaultProperties.color} + inactive + /> + </div> + <div className={`background ${active && 'active'}`} style={{...backgroundProperties}}/> + </div> + ) + } + } + + const setActiveChanged = (active:boolean) => { + setActive(active); + props.activeChanged?.(active); + } + + const dropdown: JSX.Element = + ( + <div + className="dropdown-container" + > + <Popup + toggle={ + <Tooltip disableInteractive={true} arrow={true} placement={tooltipPlacement} title={itemsMap.get(selectedVal) ? itemsMap.get(selectedVal).text : title}> + {getToggle()} + </Tooltip> + } + placement={placement} + tooltip={tooltip} + tooltipPlacement={tooltipPlacement} + trigger={PopupTrigger.CLICK} + isOpen={active} + setOpen={setActiveChanged} + size={size} + fillWidth={true} + color={color} + popup={ + <ListBox + maxItems={maxItems} + items={items} + color={color} + onItemDown={onItemDown} + selectedVal={selectedVal} + setSelectedVal={val => { + setSelectedVal?.(val); + closeOnSelect && setActive(false); + }} + size={size} + /> + } + /> + </div> + ) + + return ( + formLabel ? + <div className={`form-wrapper ${formLabelPlacement}`} +style={{ width: fillWidth ? '100%' : undefined}}> + <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> + {dropdown} + </div> + : + dropdown + ) +} diff --git a/packages/components/src/components/Dropdown/index.ts b/packages/components/src/components/Dropdown/index.ts new file mode 100644 index 000000000..5cda7b92d --- /dev/null +++ b/packages/components/src/components/Dropdown/index.ts @@ -0,0 +1 @@ +export * from './Dropdown' diff --git a/packages/components/src/components/DropdownSearch/DropdownSearch.scss b/packages/components/src/components/DropdownSearch/DropdownSearch.scss new file mode 100644 index 000000000..e111c822b --- /dev/null +++ b/packages/components/src/components/DropdownSearch/DropdownSearch.scss @@ -0,0 +1,123 @@ +@import '../../global/globalCssVariables.scss'; + +.dropdownsearch { + margin-top: 10px; +} + +.divider { + height: 1px; + width: 100%; + background: $medium-gray; +} + +.dropdownsearch-container { + display: flex; + flex-direction: column; + justify-content: center; + min-width: fit-content; + border-radius: $standard-border-radius; + height: 100%; + position: relative; + transition: 0.4s; + + .dropdownsearch-list { + position: absolute; + top: 100%; + width: 100%; + } + + .dropdownsearch-toggle { + width: 100%; + display: grid; + grid-template-columns: calc(100% - 30px) 30px; + grid-template-areas: 'button end'; + grid-template-rows: 1fr; + position: relative; + align-items: center; + border: solid 1px; + border-color: transparent; + border-radius: $standard-border-radius; + overflow: hidden; + + .toggle-background { + width: 100%; + height: 100%; + z-index: 0; + position: absolute; + transition: 0.4s; + + &.active { + filter: opacity(0.2) !important; + } + } + + &.primary { + color: $medium-blue; + .toggle-background { + background: $medium-blue; + filter: opacity(0); + } + + &:hover{ + .toggle-background { + filter: opacity(0.2) + } + } + + } + + &.secondary { + .toggle-background { + background: $medium-blue; + filter: opacity(0); + } + + border: solid 1px $medium-blue; + color: $medium-blue; + + &:hover{ + .toggle-background { + filter: opacity(0.2) + } + } + } + + &.tertiary { + color: white; + + .toggle-background { + background: $medium-blue; + } + + &:hover{ + box-shadow: $standard-button-shadow; + } + + &:hover{ + .toggle-background { + filter: brightness(0.8); + } + } + } + + .toggle-button { + grid-area: button; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + min-width: 70px; + justify-self: center; + } + + .toggle-caret { + cursor: pointer; + grid-area: end; + display: flex; + justify-content: flex-end; + align-items: center; + justify-self: center; + } + } +} diff --git a/packages/components/src/components/DropdownSearch/DropdownSearch.stories.tsx b/packages/components/src/components/DropdownSearch/DropdownSearch.stories.tsx new file mode 100644 index 000000000..c395c6299 --- /dev/null +++ b/packages/components/src/components/DropdownSearch/DropdownSearch.stories.tsx @@ -0,0 +1,72 @@ +import React from 'react' +import { Story, Meta } from '@storybook/react' +import { Colors, Size } from '../../global/globalEnums' +import * as fa from 'react-icons/fa' +import { DropdownSearch, DropdownSearchType, IDropdownSearchProps} from './DropdownSearch' +import { IListItemProps } from '../ListItem' +import { Type , getFormLabelSize } from '../../global' + +export default { + title: 'Dash/DropdownSearch', + component: DropdownSearch, + argTypes: {}, +} as Meta<typeof DropdownSearch> + +const Template: Story<IDropdownSearchProps> = (args) => <DropdownSearch {...args} /> +const dropdownsearchItems: IListItemProps[] = [ + { + text: 'Facebook', + shortcut: '⌘F', + icon: <fa.FaFacebook />, + }, + { + text: 'Google', + }, + { + text: 'Airbnb', + icon: <fa.FaAirbnb />, + }, + { + text: 'Salesforce', + icon: <fa.FaSalesforce />, + items: [ + { + text: 'Slack', + icon: <fa.FaSlack />, + }, + { + text: 'Heroku', + shortcut: '⌘H', + icon: <fa.FaAirFreshener />, + }, + ], + }, + { + text: 'Microsoft', + icon: <fa.FaMicrosoft />, + }, +] + +export const Select = Template.bind({}) +Select.args = { + title: 'Select company', + type: Type.PRIM, + dropdownsearchType: DropdownSearchType.SELECT, + items: dropdownsearchItems, + size: Size.SMALL, + selected: { + val: 'facebook', + text: 'Facebook', + shortcut: '⌘F', + icon: <fa.FaFacebook />, + }, +} + +export const Click = Template.bind({}) +Click.args = { + title: 'Select company', + type: Type.PRIM, + dropdownsearchType: DropdownSearchType.CLICK, + items: dropdownsearchItems, + size: Size.SMALL, +} diff --git a/packages/components/src/components/DropdownSearch/DropdownSearch.tsx b/packages/components/src/components/DropdownSearch/DropdownSearch.tsx new file mode 100644 index 000000000..5ec01b44e --- /dev/null +++ b/packages/components/src/components/DropdownSearch/DropdownSearch.tsx @@ -0,0 +1,129 @@ +import React, { useState } from 'react' +import * as fa from 'react-icons/fa' +import { EditableText, Popup, PopupTrigger } from '..' +import { IGlobalProps, Placement, Size, getHeight , getFormLabelSize } from '../../global' +import { IconButton } from '../IconButton' +import { ListBox } from '../ListBox' +import { IListItemProps } from '../ListItem' +import './DropdownSearch.scss' + +export enum DropdownSearchType { + SELECT = "select", + CLICK = "click" +} + +export interface IDropdownSearchProps extends IGlobalProps { + items: IListItemProps[] + placement: Placement + dropdownSearchType: DropdownSearchType + title?: string + selectedVal?: string | number + maxItems?: number +} + +/** + * + * @param props + * @returns + * + * TODO: add support for isMulti, isSearchable + * Look at: import Select from "react-select"; + */ +export const DropdownSearch = (props: IDropdownSearchProps) => { + const { + size, + height, + maxItems, + items, + dropdownSearchType, + selectedVal, + // setSelectedVal, + tooltip, + title = "DropdownSearch", + type, + width, + color + } = props + + // const [selectedItem, setSelectedItem] = useState< + // IListItemProps | undefined + // >(selectedVal) + + const [searchTerm, setSearchTerm] = useState<string | undefined>(undefined) + const [isEditing, setIsEditing] = useState<boolean>(false) + const [active, setActive] = useState<boolean>(false) + + const getToggle = () => { + switch (dropdownSearchType) { + case DropdownSearchType.SELECT: + return (<div + className={`dropdownsearch-toggle ${type}`} + style={{ height: getHeight(height, size), width: width }} + onClick={(e) => { + e.stopPropagation() + !isEditing && setIsEditing(true) + }} + > + {/* {selectedItem && !isEditing ? ( + <ListItem {...selectedItem} inactive /> + ) : ( */} + <div className="toggle-button"> + <EditableText + type={type} + val={searchTerm} + placeholder={'...'} + editing={true} + // onEdit={(val) => { + // setSearchTerm(val) + // }} + size={Size.SMALL} + setEditing={setIsEditing} + /> + </div> + {/* )} */} + <div className="toggle-caret"> + <IconButton + size={Size.SMALL} + icon={<fa.FaSearch />} + inactive + /> + </div> + <div className={`toggle-background ${isEditing && 'active'}`}/> + </div>); + case DropdownSearchType.CLICK: + default: + return ( + <div + className={`dropdownsearch-toggle ${type}`} + style={{ height: getHeight(height, size), width: width }} + > + </div> + ) + } + } + + return ( + <div + className="dropdownsearch-container" + > + <Popup + toggle={getToggle()} + trigger={PopupTrigger.CLICK} + isOpen={active} + setOpen={setActive} + size={size} + color={color} + popup={ + <ListBox + maxItems={maxItems} + items={items} + filter={searchTerm} + // selectedVal={selectedVal} + // setSelectedVal={setSelectedItem} + size={size} + /> + } + /> + </div> + ) +} diff --git a/packages/components/src/components/DropdownSearch/index.ts b/packages/components/src/components/DropdownSearch/index.ts new file mode 100644 index 000000000..b233f2cc6 --- /dev/null +++ b/packages/components/src/components/DropdownSearch/index.ts @@ -0,0 +1 @@ +export * from './DropdownSearch' diff --git a/packages/components/src/components/EditableText/EditableText.scss b/packages/components/src/components/EditableText/EditableText.scss new file mode 100644 index 000000000..19e5af2cd --- /dev/null +++ b/packages/components/src/components/EditableText/EditableText.scss @@ -0,0 +1,131 @@ +@import '../../global/globalCssVariables.scss'; + +.editableText-container { + position: relative; + width: fit-content; + border: solid 1px; + border-color: transparent; + border-radius: $standard-border-radius; + font-family: $default-font; + overflow: hidden; + padding: $padding; + + .password { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + right: 0; + top: 0; + } + + .editableText-background { + width: 100%; + height: 100%; + z-index: -1; + position: absolute; + transition: 0.4s; + top: 0; + left: 0; + } + + &.primary { + + &:focus-within { + .editableText-background { + filter: opacity(0.2) !important; + } + } + + .editableText-background { + filter: opacity(0); + + &.active { + filter: opacity(0.2) !important; + } + } + + &:hover{ + .editableText-background { + filter: opacity(0.2) + } + } + } + + &.secondary { + &:focus-within { + .editableText-background { + filter: opacity(0.2) !important; + } + } + + .editableText-background { + filter: opacity(0); + + &.active { + filter: opacity(0.2) !important; + } + } + + &:hover{ + .editableText-background { + filter: opacity(0.2) + } + } + } + + &.tertiary { + &:hover{ + box-shadow: $standard-shadow; + } + + &:hover{ + .editableText-background { + filter: brightness(0.8); + } + } + } + + .editableText { + -webkit-appearance: none; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-size: inherit; + border: none; + outline: none; + margin: 0px !important; + padding: 0px !important; + box-shadow: none !important; + background: transparent; + color: inherit; + z-index: 1; + + &.center { + display: flex; + justify-content: center; + align-items: center; + } + } + + .displayText { + cursor: text !important; + width: 100%; + height: 100%; + white-space: nowrap; + text-overflow: ellipsis; + display: flex; + align-items: center; + font-size: inherit; + color: inherit; + z-index: 1; + + &.center { + display: flex; + justify-content: center; + align-items: center; + } + } +} + diff --git a/packages/components/src/components/EditableText/EditableText.stories.tsx b/packages/components/src/components/EditableText/EditableText.stories.tsx new file mode 100644 index 000000000..1cd75a7eb --- /dev/null +++ b/packages/components/src/components/EditableText/EditableText.stories.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Story, Meta } from '@storybook/react'; +import { Colors, Size } from '../../global/globalEnums'; +import * as fa from 'react-icons/fa' +import { EditableText, IEditableTextProps } from '..'; +import { Type , getFormLabelSize } from '../../global'; + +export default { + title: 'Dash/Editable Text', + component: EditableText, + argTypes: {}, +} as Meta<typeof EditableText>; + +const Template: Story<IEditableTextProps> = (args) => <EditableText {...args}/>; + +export const Primary = Template.bind({}); +Primary.args = { + type: Type.PRIM, + size: Size.MEDIUM, + fillWidth: true, + placeholder: '...', + onchange: (val) => console.log(val), + onEdit: (val) => console.log(val), +}; + +// export const Background = Template.bind({}); +// Background.args = { +// text: 'hello', +// placeholder: '...', +// size: Size.MEDIUM, +// editing: true, +// backgroundColor: Colors.LIGHT_GRAY, +// onEdit: (val) => console.log(val), +// };
\ No newline at end of file diff --git a/packages/components/src/components/EditableText/EditableText.tsx b/packages/components/src/components/EditableText/EditableText.tsx new file mode 100644 index 000000000..c361cf183 --- /dev/null +++ b/packages/components/src/components/EditableText/EditableText.tsx @@ -0,0 +1,176 @@ +import React, { useState } from 'react' +import { Colors, IGlobalProps, Size, TextAlignment, Type, getFontSize, getFormLabelSize, getHeight, isDark } from '../../global' +import './EditableText.scss' +import { Toggle, ToggleType } from '../Toggle' +import { FaEye, FaEyeSlash} from 'react-icons/fa' + +export interface IEditableTextProps extends IGlobalProps { + val?: string | number + setVal?: (newText: string | number) => unknown + onEnter?: (newText: string | number) => unknown + setEditing?: (bool: boolean) => unknown + placeholder?: string + editing?: boolean + size?: Size + height?: number + multiline?: boolean + textAlign?: TextAlignment + password?: boolean +} + +/** + * Editable Text is used for inline renaming of some text. + * It appears as normal UI text but transforms into a text input field when the user clicks on or focuses it. + * @param props + * @returns + */ +export const EditableText = (props: IEditableTextProps) => { + const [valLoc, setValLoc] = useState<string>('') + const [editingLoc, setEditingLoc] = useState<boolean>(false) + const { + height, + size, + val = valLoc, + setVal = setValLoc, + onEnter, + setEditing = setEditingLoc, + color = Colors.MEDIUM_BLUE, + background, + type = Type.PRIM, + placeholder, + width, + multiline, + textAlign = 'left', + formLabel, + formLabelPlacement, + fillWidth, + password, + editing = password ? true : editingLoc, + style + } = props + const [showPassword, setShowPassword] = useState<boolean>(false) + + const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => { + setVal(event.target.value) + } + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + onEnter?.((event.target as HTMLInputElement).value) + } + } + + const getBorderColor = (): Colors | string | undefined => { + switch(type){ + case Type.PRIM: + return undefined; + case Type.SEC: + return color; + case Type.TERT: + if (editing) return color; + else return color; + } + } + + const getColor = (): Colors | string | undefined => { + if (color && background) return color; + switch(type){ + case Type.PRIM: + return color; + case Type.SEC: + return color; + case Type.TERT: + if (isDark(color)) return Colors.WHITE; + else return Colors.BLACK + } + } + + const getBackground = (): Colors | string | undefined => { + if (background) return background; + switch(type){ + case Type.PRIM: + return color; + case Type.SEC: + return color; + case Type.TERT: + return color + } + } + + const defaultProperties: React.CSSProperties = { + height: getHeight(height, size), + minHeight: getHeight(height, size), + width: fillWidth ? '100%' : width, + padding: undefined, + fontWeight: 500, + fontSize: getFontSize(size), + fontFamily: 'sans-serif', + borderColor: getBorderColor(), + color: getColor() + } + + const backgroundProperties: React.CSSProperties = { + background: getBackground() + } + + const editableText: JSX.Element = ( + <div className={`editableText-container ${type}`} + style={{...defaultProperties, ...style}} + onClick={() => setEditing(true)} + > + {editing ? ( + <input + className={`editableText ${type} ${textAlign}`} + style={{ + height: getHeight(height, size), + textAlign: textAlign, + width: fillWidth ? '100%' : width + }} + placeholder={placeholder} + type={password && !showPassword ? 'password' : undefined} + autoFocus + onChange={handleOnChange} + onKeyPress={handleKeyPress} + onBlur={() => { + !password && setEditing(false) + }} + defaultValue={val} + ></input> + ) : ( + <div + className={`displayText ${type} ${textAlign}`} + style={{ + height: getHeight(height, size), + textAlign: textAlign, + width: fillWidth ? '100%' : width + }} + > + {val ? val : placeholder} + </div> + )} + {password && <div className={`password`}> + <Toggle + toggleType={ToggleType.BUTTON} + type={Type.PRIM} + size={size} + color={color} + toggleStatus={showPassword} + onClick={() => setShowPassword(!showPassword)} + tooltip={`${showPassword ? 'Hide' : 'Show'} Password`} + icon={<FaEyeSlash/>} + iconFalse={<FaEye/>} + /> + </div>} + <div className={`editableText-background ${type}`} style={backgroundProperties}/> + </div> + ) + +return ( + formLabel ? + <div className={`form-wrapper ${formLabelPlacement}`}> + <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> + {editableText} + </div> + : + editableText + ) +} diff --git a/packages/components/src/components/EditableText/index.ts b/packages/components/src/components/EditableText/index.ts new file mode 100644 index 000000000..e3367b175 --- /dev/null +++ b/packages/components/src/components/EditableText/index.ts @@ -0,0 +1 @@ +export * from './EditableText' diff --git a/packages/components/src/components/FormInput/FormInput.scss b/packages/components/src/components/FormInput/FormInput.scss new file mode 100644 index 000000000..db04ff8cf --- /dev/null +++ b/packages/components/src/components/FormInput/FormInput.scss @@ -0,0 +1,69 @@ +@import '../../global/globalCssVariables.scss'; + +.formInput-container { + display: flex; + flex-direction: column; + width: 100%; + height: fit-content; + position: relative; + margin-top: 20px; + + .formInput { + font-family: inherit; + width: 100%; + border: 0; + border-bottom: 2px solid black; + outline: 0; + font-size: 1rem; + color: black; + padding: 7px 0; + background: transparent; + transition: border-color 0.2s; + + &::placeholder { + color: transparent; + } + + &:focus { + ~ .formInput-label { + position: absolute; + transform: translate(0px, -13px); + display: block; + transition: 0.2s; + font-size: 1rem; + font-weight: 700; + } + padding-bottom: 6px; + font-weight: 700; + border-width: 3px; + border-image: linear-gradient(to right, black, white); + border-image-slice: 1; + } + + &:valid { + ~ .formInput-label { + position: absolute; + transform: translate(0px, -13px); + display: block; + transition: 0.2s; + font-size: 1rem; + } + } + + &:required, + &:invalid { + box-shadow: none; + } + } + + .formInput-label { + position: absolute; + top: 0; + transform: translate(0px, 8px); + display: block; + transition: 0.2s; + font-size: 1rem; + color: gray; + pointer-events: none; + } +} diff --git a/packages/components/src/components/FormInput/FormInput.stories.tsx b/packages/components/src/components/FormInput/FormInput.stories.tsx new file mode 100644 index 000000000..482a4f9b1 --- /dev/null +++ b/packages/components/src/components/FormInput/FormInput.stories.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Story, Meta } from '@storybook/react'; +import { Colors, Size } from '../../global/globalEnums'; +import * as fa from 'react-icons/fa' +import { IListBoxItemProps } from '../ListItem'; +import { FormInput, IFormInputProps } from './FormInput'; +import { IconButton } from '../IconButton'; + +export default { + title: 'Dash/Form Input', + component: FormInput, + argTypes: {}, +} as Meta<typeof FormInput>; + +const Template: Story<IFormInputProps> = (args) => <FormInput {...args}/>; + +// export const Primary = Template.bind({}); +// Primary.args = { +// title: 'Hello World!', +// initialIsOpen: true, +// };
\ No newline at end of file diff --git a/packages/components/src/components/FormInput/FormInput.tsx b/packages/components/src/components/FormInput/FormInput.tsx new file mode 100644 index 000000000..48fac3489 --- /dev/null +++ b/packages/components/src/components/FormInput/FormInput.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import './FormInput.scss' + +export interface IFormInputProps { + placeholder?: string + value?: string + title?: string + type?: string + onChange: (event: React.ChangeEvent<HTMLInputElement>) => void +} + +export const FormInput = (props: IFormInputProps) => { + const { placeholder, type, value, title, onChange } = props + return ( + <div className="formInput-container"> + <input + className={'formInput'} + type={type ? type : 'text'} + value={value} + onChange={onChange} + placeholder={title} + required={true} + /> + <label className={'formInput-label'}>{title}</label> + </div> + ) +} diff --git a/packages/components/src/components/FormInput/index.ts b/packages/components/src/components/FormInput/index.ts new file mode 100644 index 000000000..1d23aa44c --- /dev/null +++ b/packages/components/src/components/FormInput/index.ts @@ -0,0 +1 @@ +export * from './FormInput' diff --git a/packages/components/src/components/Group/Group.scss b/packages/components/src/components/Group/Group.scss new file mode 100644 index 000000000..885472a5d --- /dev/null +++ b/packages/components/src/components/Group/Group.scss @@ -0,0 +1,16 @@ +@import '../../global/globalCssVariables.scss'; + +.group-wrapper { + overflow: hidden; + + .group-container { + width: fit-content; + display: flex; + flex-flow: row wrap; + height: fit-content; + flex-flow: row; + justify-content: flex-start; + align-items: center; + } +} + diff --git a/packages/components/src/components/Group/Group.stories.tsx b/packages/components/src/components/Group/Group.stories.tsx new file mode 100644 index 000000000..a7bbf098e --- /dev/null +++ b/packages/components/src/components/Group/Group.stories.tsx @@ -0,0 +1,92 @@ +import { Meta, Story } from '@storybook/react' +import React from 'react' +import * as bi from 'react-icons/bi' +import { Dropdown, DropdownType } from '../Dropdown' +import { IconButton } from '../IconButton' +import { Popup, PopupTrigger } from '../Popup' +import { Group, IGroupProps } from './Group' +import { Type , getFormLabelSize } from '../../global' + +export default { + title: 'Dash/Group', + component: Group, + argTypes: {}, +} as Meta<typeof Group> + +const Template: Story<IGroupProps> = (args) => ( + <Group {...args}> + <Dropdown + items={[ + { + text: 'Hello', + description: 'You need to watch out!', + val: '' + }, + { + text: 'Hello', + description: 'You need to watch out!', + val: '' + } + ]} + dropdownType={DropdownType.CLICK} + type={Type.SEC} + /> + <IconButton + icon={<bi.BiAddToQueue />} + type={Type.SEC} + /> + <IconButton + icon={<bi.BiPlus />} + type={Type.SEC} + /> + <Popup + icon={<bi.BiAlarmSnooze />} + type={Type.SEC} + popup={<div>HELLO</div>} + /> + <IconButton + icon={<bi.BiAlarmAdd />} + type={Type.SEC} + fillWidth + /> + <IconButton + icon={<bi.BiAlarmExclamation />} + type={Type.SEC} + fillWidth + /> + <Popup + icon={<bi.BiBookOpen />} + trigger={PopupTrigger.CLICK} + placement={'bottom'} + popup={ + <Group rowGap={5}> + <IconButton + icon={<bi.BiAddToQueue />} + type={Type.SEC} + /> + <IconButton + icon={<bi.BiPlus />} + type={Type.SEC} + /> + <IconButton + icon={<bi.BiAlarmSnooze />} + type={Type.SEC} + /> + <IconButton + icon={<bi.BiAlarmAdd />} + type={Type.SEC} + /> + <IconButton + icon={<bi.BiAlarmExclamation />} + type={Type.SEC} + /> + </Group> + } + /> + </Group> +) + +export const Primary = Template.bind({}) +Primary.args = { + width: '100%' +} diff --git a/packages/components/src/components/Group/Group.tsx b/packages/components/src/components/Group/Group.tsx new file mode 100644 index 000000000..7abe4a1c7 --- /dev/null +++ b/packages/components/src/components/Group/Group.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import './Group.scss' +import { Colors, IGlobalProps, getFontSize, isDark , getFormLabelSize } from '../../global'; + +export interface IGroupProps extends IGlobalProps { + children: any + rowGap?: number; + columnGap?: number; + padding?: number | string; +} + +export const Group = (props: IGroupProps) => { + const { + children, + width = '100%', + rowGap = 5, + columnGap = 5, + padding = 0, + formLabel, + formLabelPlacement, + size, + style, + color, + fillWidth + } = props + + const group: JSX.Element = + ( + <div + className="group-wrapper" + style={{ width, padding: padding, ...style }} + > + <div className={`group-container`} + style={{ rowGap, columnGap }} + >{children}</div> + </div> + ) + + return ( + formLabel ? + <div className={`form-wrapper ${formLabelPlacement}`} + style={{ width: fillWidth ? '100%' : undefined}}> + <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> + {group} + </div> + : + group + ) +} diff --git a/packages/components/src/components/Group/index.ts b/packages/components/src/components/Group/index.ts new file mode 100644 index 000000000..e3ad05435 --- /dev/null +++ b/packages/components/src/components/Group/index.ts @@ -0,0 +1 @@ +export * from './Group' diff --git a/packages/components/src/components/IconButton/IconButton.scss b/packages/components/src/components/IconButton/IconButton.scss new file mode 100644 index 000000000..9a0b53c0f --- /dev/null +++ b/packages/components/src/components/IconButton/IconButton.scss @@ -0,0 +1,121 @@ +@import '../../global/globalCssVariables.scss'; + +.iconButton-container { + position: relative; + cursor: pointer; + overflow: hidden; + user-select: none; + display: flex; + justify-content: center; + align-items: center; + font-family: $default-font; + border-radius: $standard-border-radius; + white-space: nowrap; + transition: 0.4s; + border: solid 1px; + border-color: transparent; + pointer-events: all; + + + .iconButton-content { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + z-index: 1; + font-family: Verdana, sans-serif; + font-weight: 500; + } + + .icon { + z-index: 1; + display: flex; + justify-content: center; + align-items: center; + } + + .background { + width: 100%; + height: 100%; + z-index: 0; + position: absolute; + transition: 0.4s; + } + + &.inactive { + .background { + filter: opacity(0) !important; + } + } + + &.primary { + .background { + filter: opacity(0); + + &.active { + filter: opacity(0.2) !important; + } + } + + &:hover{ + .background { + filter: opacity(0.2) + } + } + + } + + &.secondary { + .background { + filter: opacity(0); + + &.active { + filter: opacity(0.2) !important; + } + } + + &:hover{ + .background { + filter: opacity(0.2) + } + } + } + + &.tertiary { + &:hover{ + box-shadow: $standard-button-shadow; + } + + &:hover{ + .background { + filter: brightness(0.8); + } + } + } + + .color { + position: relative; + width: 70%; + height: 15%; + z-index: 3; + margin-top: 2px; + border-radius: 10px; + outline: solid 0.3px; + outline-offset: -0.3px; + } + + .iconButton-label { + position: relative; + z-index: 2; + max-width: 100%; + overflow: hidden; + white-space: normal; + display: flex; + text-align: center; + justify-content: center; + align-items: center; + font-size: $xsmall-fontSize; + } +} diff --git a/packages/components/src/components/IconButton/IconButton.stories.tsx b/packages/components/src/components/IconButton/IconButton.stories.tsx new file mode 100644 index 000000000..242bdd696 --- /dev/null +++ b/packages/components/src/components/IconButton/IconButton.stories.tsx @@ -0,0 +1,74 @@ +import { Meta, Story } from '@storybook/react' +import React from 'react' +import * as bi from 'react-icons/bi' +import { IButtonProps, IconButton } from '..' +import { Type, Size , getFormLabelSize } from '../../global' + +export default { + title: 'Dash/Icon Button', + component: IconButton, + argTypes: {}, +} as Meta<typeof IconButton> + +const Template: Story<IButtonProps> = (args) => <IconButton {...args} /> + +export const Primary = Template.bind({}) +Primary.args = { + onClick: () => {}, + icon: <bi.BiAngry/>, + type: Type.PRIM, +} + +export const Secondary = Template.bind({}) +Secondary.args = { + onClick: () => {}, + icon: <bi.BiAngry/>, + type: Type.SEC +} + +export const Tertiary = Template.bind({}) +Tertiary.args = { + onClick: () => {}, + icon: <bi.BiAngry/>, + type: Type.TERT +} + +export const Label = Template.bind({}) +Label.args = { + onClick: () => {}, + icon: <bi.BiAngry/>, + type: Type.TERT, + label: "Button Label" +} + +export const XSmall = Template.bind({}) +XSmall.args = { + onClick: () => {}, + icon: <bi.BiAngry/>, + type: Type.SEC, + size: Size.XSMALL, +} + +export const Small = Template.bind({}) +Small.args = { + onClick: () => {}, + icon: <bi.BiAngry/>, + type: Type.PRIM, + size: Size.SMALL, +} + +export const Medium = Template.bind({}) +Medium.args = { + onClick: () => {}, + icon: <bi.BiAngry/>, + type: Type.PRIM, + size: Size.MEDIUM, +} + +export const Large = Template.bind({}) +Large.args = { + onClick: () => {}, + icon: <bi.BiAngry/>, + type: Type.PRIM, + size: Size.LARGE, +}
\ No newline at end of file diff --git a/packages/components/src/components/IconButton/IconButton.tsx b/packages/components/src/components/IconButton/IconButton.tsx new file mode 100644 index 000000000..7d273e23f --- /dev/null +++ b/packages/components/src/components/IconButton/IconButton.tsx @@ -0,0 +1,157 @@ +import { Tooltip } from '@mui/material' +import React from 'react' +import { Colors, Size, Type, getFontSize, getHeight, isDark, getFormLabelSize } from '../../global' +import { IButtonProps } from '../Button' +import './IconButton.scss' + +export interface IIconButtonProps extends IButtonProps {} + +export const IconButton = (props: IButtonProps) => { + const { + active, + icon, + onClick, + onDoubleClick, + onPointerDown, + inactive, + type = Type.PRIM, + color = Colors.MEDIUM_BLUE, + background, + label, + height, + size = Size.SMALL, + style, + tooltip, + tooltipPlacement = 'top', + colorPicker, + formLabel, + formLabelPlacement, + hideLabel, + fillWidth + } = props + + /** + * Pointer down + * @param e + */ + const handlePointerDown = (e: React.PointerEvent) => { + + if (!inactive && onPointerDown) { + e.stopPropagation(); + e.preventDefault(); + onPointerDown(e) + } + } + + /** + * In the event that there is a single click + * @param e + */ + const handleClick = (e: React.MouseEvent) => { + if (!inactive && onClick) { + e.stopPropagation(); + e.preventDefault(); + onClick(e) + } + } + + /** + * Double click + * @param e + */ + const handleDoubleClick = (e: React.MouseEvent) => { + if (!inactive && onDoubleClick){ + e.stopPropagation(); + e.preventDefault(); + onDoubleClick(e) + } + } + + const getBorderColor = (): Colors | string | undefined => { + switch(type){ + case Type.PRIM: + return undefined; + case Type.SEC: + return color; + case Type.TERT: + if (colorPicker) return colorPicker; + if (active) return color; + else return color; + } + } + + const getColor = (): Colors | string | undefined => { + if (color && background) return color; + switch(type){ + case Type.PRIM: + return color; + case Type.SEC: + return color; + case Type.TERT: + if (colorPicker) { + if (isDark(colorPicker)) return Colors.WHITE; + else return Colors.BLACK + } + if (isDark(color)) return Colors.WHITE; + else return Colors.BLACK + } + } + + const getBackground = (): Colors | string | undefined => { + if(background) return background; + switch(type){ + case Type.PRIM: + return color; + case Type.SEC: + return color; + case Type.TERT: + if (colorPicker) return colorPicker + else return color + } + } + + const defaultProperties: React.CSSProperties = { + height: getHeight(height, size), + width: fillWidth ? '100%' : getHeight(height, size), + minWidth: getHeight(height, size), + fontWeight: 500, + fontSize: getFontSize(size, true), + borderColor: getBorderColor(), + color: getColor() + } + + const backgroundProperties: React.CSSProperties = { + background: getBackground() + } + + const iconButton: JSX.Element = ( + <Tooltip disableInteractive={true} arrow={true} placement={tooltipPlacement} title={tooltip}> + <div + className={`iconButton-container ${type} ${inactive && 'inactive'}`} + onClick={handleClick} + onDoubleClick={handleDoubleClick} + onPointerDown={handlePointerDown} + style={{...defaultProperties, ...style}} + tabIndex={-1} + > + <div className="iconButton-content"> + {icon} + {colorPicker && type !== (Type.TERT) && <div className={`color`} style={{background: colorPicker, outlineColor: defaultProperties.color}}/>} + {label && !hideLabel && <div className={'iconButton-label'} style={{color: defaultProperties.color}}>{label}</div>} + </div> + <div className={`background ${active && 'active'} ${inactive && 'inactive'}`} style={backgroundProperties}/> + </div> + </Tooltip> + ) + + return ( + formLabel ? + <div className={`form-wrapper ${formLabelPlacement}`} +style={{ width: fillWidth ? '100%' : undefined}}> + <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> + {iconButton} + </div> + : + iconButton + ) +} diff --git a/packages/components/src/components/IconButton/index.ts b/packages/components/src/components/IconButton/index.ts new file mode 100644 index 000000000..a37a7fc4a --- /dev/null +++ b/packages/components/src/components/IconButton/index.ts @@ -0,0 +1 @@ +export * from './IconButton' diff --git a/packages/components/src/components/ListBox/ListBox.scss b/packages/components/src/components/ListBox/ListBox.scss new file mode 100644 index 000000000..dc449c943 --- /dev/null +++ b/packages/components/src/components/ListBox/ListBox.scss @@ -0,0 +1,16 @@ +@import '../../global/globalCssVariables.scss'; + +.listBox-container { + position: relative; + width: fit-content; + max-height: 50vh; + overflow: scroll; + height: fit-content; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + max-width: 300px; + padding: 5px; + gap: 2px; +}
\ No newline at end of file diff --git a/packages/components/src/components/ListBox/ListBox.stories.tsx b/packages/components/src/components/ListBox/ListBox.stories.tsx new file mode 100644 index 000000000..e1332ae2f --- /dev/null +++ b/packages/components/src/components/ListBox/ListBox.stories.tsx @@ -0,0 +1,66 @@ +import { Meta, Story } from '@storybook/react' +import React from 'react' +import * as fa from 'react-icons/fa' +import { IListItemProps } from '../ListItem' +import { IListBoxProps, ListBox } from './ListBox' + +export default { + title: 'Dash/List Box', + component: ListBox, + argTypes: {}, +} as Meta<typeof ListBox> + +const dropdownItems: IListItemProps[] = [ + { + text: 'Facebook', + val: "", + shortcut: '⌘F', + icon: <fa.FaFacebook />, + description: 'A hopeless company.' + }, + { + text: 'Google', + val: "", + shortcut: '⌘G', + icon: <fa.FaGoogle /> + }, + { + text: 'Airbnb', + val: "", + icon: <fa.FaAirbnb />, + description: 'A housing service that does not work anymore.' + }, + { + text: 'Salesforce', + val: "", + icon: <fa.FaSalesforce />, + items: [ + { + text: 'Slack', + val: "", + icon: <fa.FaSlack />, + }, + { + text: 'Heroku', + val: "", + shortcut: '⌘H', + icon: <fa.FaAirFreshener />, + description: 'A product that used to be brilliant - absolutely fantastic - but then decided to remove its free service.' + }, + ], + }, + { + text: 'Microsoft', + val: "", + icon: <fa.FaMicrosoft />, + }, +] + +const Template: Story<IListBoxProps> = (args) => ( + <ListBox {...args}/> +) + +export const Primary = Template.bind({}) +Primary.args = { + items: dropdownItems +} diff --git a/packages/components/src/components/ListBox/ListBox.tsx b/packages/components/src/components/ListBox/ListBox.tsx new file mode 100644 index 000000000..abdfd38f3 --- /dev/null +++ b/packages/components/src/components/ListBox/ListBox.tsx @@ -0,0 +1,76 @@ +import React, { ReactText } from 'react' +import { IListItemProps, ListItem } from '../ListItem' +import './ListBox.scss' +import { Colors, IGlobalProps, isDark , getFormLabelSize } from '../../global' + +export interface IListBoxProps extends IGlobalProps { + items: IListItemProps[] + filter?: string + selectedVal?: string | number + setSelectedVal?: (val: string | number) => unknown + maxItems?: number + onItemDown?: (e:React.PointerEvent, val:number|string) => void +} + +/** + * + * @param props + * @returns + * + * TODO: add support for isMulti, isSearchable + * Look at: import Select from "react-select"; + */ +export const ListBox = (props: IListBoxProps) => { + const { + items, + selectedVal, + setSelectedVal, + filter, + onItemDown, + color = Colors.MEDIUM_BLUE + } = props + + const getListItem = ( + item: IListItemProps, + ind: number, + selected: boolean + ): JSX.Element => { + return ( + <ListItem + key={ind} + ind={ind} + onItemDown={onItemDown} + selected={selected} + color={color} + setSelectedVal={setSelectedVal} + onClick={item.onClick} + {...item} + /> + ) + } + let itemElements: JSX.Element[] = [] + items.forEach((item, ind) => { + if (filter) { + if ( + filter.toLowerCase() === + item.text?.substring(0, filter.length).toLowerCase() + ) { + itemElements.push( + getListItem(item, ind, item.val === selectedVal) + ) + } + } else { + itemElements.push( + getListItem(item, ind, item.val === selectedVal) + ) + } + }) + return ( + <div + className="listBox-container" + style={{ color: color }} + > + {itemElements} + </div> + ) +} diff --git a/packages/components/src/components/ListBox/index.ts b/packages/components/src/components/ListBox/index.ts new file mode 100644 index 000000000..242ed7889 --- /dev/null +++ b/packages/components/src/components/ListBox/index.ts @@ -0,0 +1 @@ +export * from './ListBox'
\ No newline at end of file diff --git a/packages/components/src/components/ListItem/ListItem.scss b/packages/components/src/components/ListItem/ListItem.scss new file mode 100644 index 000000000..736078360 --- /dev/null +++ b/packages/components/src/components/ListItem/ListItem.scss @@ -0,0 +1,78 @@ +@import '../../global/globalCssVariables.scss'; + +.listItem-container { + position: relative; + width: 100%; + border-radius: $standard-border-radius; + display: flex; + justify-content: center; + align-items: flex-start; + flex-direction: column; + cursor: pointer; + font-family: Verdana, sans-serif; + overflow: hidden; + text-align: left; + + .listItem-background { + position: absolute; + width: 100%; + height: 100%; + background: $medium-blue; + filter: opacity(0); + transition: 0.4s; + } + + .listItem-top { + display: flex; + height: 30px; + width: 100%; + justify-content: space-between; + align-items: center; + gap: 20px; + + .content { + display: flex; + justify-content: flex-start; + align-items: center; + padding: $padding; + width: 100%; + height: 100%; + z-index: 1; + gap: 5px; + font-weight: 500; + + .text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + } + } + + .shortcut { + grid-area: end; + padding: $padding; + display: flex; + justify-content: center; + align-items: center; + font-size: $xsmall-fontSize; + font-family: $default-font; + } + + .caret { + grid-area: end; + display: flex; + justify-content: flex-end; + align-items: center; + justify-self: center; + } + } + + .listItem-description { + font-size: $small-fontSize; + display: flex; + padding: 0px 5px 10px 5px; + justify-content: flex-start; + width: calc(100% - 10px); + } +} diff --git a/packages/components/src/components/ListItem/ListItem.stories.tsx b/packages/components/src/components/ListItem/ListItem.stories.tsx new file mode 100644 index 000000000..7bbd2d1f7 --- /dev/null +++ b/packages/components/src/components/ListItem/ListItem.stories.tsx @@ -0,0 +1,21 @@ +import { Meta, Story } from '@storybook/react' +import React from 'react' +import { IListItemProps, ListItem } from './ListItem' + +export default { + title: 'Dash/List Item', + component: ListItem, + argTypes: {}, +} as Meta<typeof ListItem> + +const Template: Story<IListItemProps> = (args) => ( + <ListItem {...args}/> +) + +export const Primary = Template.bind({}) +Primary.args = { + text: 'Hello World!', + description: 'This is a description...', + shortcut: '%4', + +} diff --git a/packages/components/src/components/ListItem/ListItem.tsx b/packages/components/src/components/ListItem/ListItem.tsx new file mode 100644 index 000000000..d76c84b3e --- /dev/null +++ b/packages/components/src/components/ListItem/ListItem.tsx @@ -0,0 +1,134 @@ +import React, { useState } from 'react' +import * as fa from 'react-icons/fa' +import { getFontSize, IGlobalProps, Type , getFormLabelSize, getHeight } from '../../global' +import { Size } from '../../global/globalEnums' +import { IconButton } from '../IconButton' +import { ListBox } from '../ListBox' +import { Popup, PopupTrigger } from '../Popup' +import './ListItem.scss' + +export interface IListItemProps extends IGlobalProps { + ind?: number + text?: string + val: string | number + icon?: JSX.Element + description?: string + shortcut?: string + items?: IListItemProps[] + selected?: boolean + setSelectedVal?: (val: string | number) => unknown + onClick?: () => void + onItemDown?: (e:React.PointerEvent, val:string| number) => void + uppercase?: boolean +} + +/** + * + * @param props + * @returns + * + * TODO: add support for isMulti, isSearchable + * Look at: import Select from "react-select"; + */ +export const ListItem = (props: IListItemProps) => { + const { + ind, + val, + description, + text, + shortcut, + items, + icon, + selected, + setSelectedVal, + onClick, + onItemDown, + inactive, + size = Size.SMALL, + style, + color, + background, + uppercase + } = props + + const [isHovered, setIsHovered] = useState<boolean>(false); + + let listItem:JSX.Element = ( + <div + tabIndex={-1} + className="listItem-container" + onPointerDown={(e) => onItemDown?.(e, val) && setSelectedVal?.(val)} + onClick={(e: React.MouseEvent) => { + if (!items) { + !inactive && onClick?.() + !inactive && onClick && e.stopPropagation() + setSelectedVal?.(val) + } + }} + style={{ + minHeight: getHeight(undefined, size), + userSelect: 'none', + ...style + }} + onPointerEnter={() => { + setIsHovered(true) + }} + onPointerLeave={() => { + setIsHovered(false) + }} + > + <div className="listItem-top"> + <div className="content" + style={{ + fontSize: getFontSize(size), + color: style?.color ? style.color : color + }}> + {icon} + <div className="text" style={{ + textTransform: uppercase ? 'uppercase' : undefined + }}>{text}</div> + </div> + {shortcut && !inactive && ( + <div + className="shortcut" + color={style?.color ? style.color : color} + > + {shortcut} + </div> + )} + {items && !inactive && ( + <IconButton + type={Type.PRIM} + size={Size.SMALL} + icon={<fa.FaCaretRight/>} + color={style?.color ? style.color : color} + background={background} + inactive + /> + )} + </div> + {description && !inactive && ( + <div className="listItem-description">{description}</div> + )} + <div className="listItem-background" + style={{ + background: background ? background : style?.color ? style.color : color, + filter: selected ? 'opacity(0.3)' : isHovered && !inactive ? 'opacity(0.2)' : 'opacity(0)' + }} + /> + </div> +) + + if (items && !inactive) return <Popup + placement={'right'} + toggle={listItem} + color={color} + background={background} + trigger={PopupTrigger.CLICK} + popup={ + <ListBox color={color} background={background} items={items}/> + } + fillWidth={true} + /> + else return <>{listItem}</> +} diff --git a/packages/components/src/components/ListItem/index.ts b/packages/components/src/components/ListItem/index.ts new file mode 100644 index 000000000..645a3a487 --- /dev/null +++ b/packages/components/src/components/ListItem/index.ts @@ -0,0 +1 @@ +export * from './ListItem'
\ No newline at end of file diff --git a/packages/components/src/components/Modal/Modal.scss b/packages/components/src/components/Modal/Modal.scss new file mode 100644 index 000000000..c0667ed26 --- /dev/null +++ b/packages/components/src/components/Modal/Modal.scss @@ -0,0 +1,46 @@ +@import '../../global/globalCssVariables.scss'; + +.modal-container { + top: 0px; + left: 0px; + width: 100vw; + height: 100vh; + position: fixed; + display: flex; + justify-content: center; + align-items: center; + z-index: 100; + + .modal-popup { + position: relative; + display: flex; + flex-direction: column; + align-items: left; + z-index: 10; + width: 400px; + height: fit-content; + padding: 20px; + border-radius: $standard-border-radius; + font-weight: bold; + font-size: 1.5rem; + + .modal-closeButton { + top: -15px; + right: -15px; + position: absolute; + width: fit-content; + height: fit-content; + } + } +} + +.modal-background { + z-index: 9; + position: absolute; + top: -10vh; + left: -10vw; + backdrop-filter: blur(15px); + width: 200vw; + height: 200vh; + background: $modal-background; +} diff --git a/packages/components/src/components/Modal/Modal.stories.tsx b/packages/components/src/components/Modal/Modal.stories.tsx new file mode 100644 index 000000000..bccb49483 --- /dev/null +++ b/packages/components/src/components/Modal/Modal.stories.tsx @@ -0,0 +1,21 @@ +import { Meta, Story } from '@storybook/react'; +import React from 'react'; +import { IModalProps, Modal } from './Modal'; + +export default { + title: 'Dash/Modal', + component: Modal, + argTypes: {}, +} as Meta<typeof Modal>; + +const Template: Story<IModalProps> = (args) => + <Modal {...args}> + <div> HELLO WORLD! </div> + </Modal> +; + +export const Primary = Template.bind({}); +Primary.args = { + title: 'Hello World!', + initialIsOpen: true, +};
\ No newline at end of file diff --git a/packages/components/src/components/Modal/Modal.tsx b/packages/components/src/components/Modal/Modal.tsx new file mode 100644 index 000000000..f4e08f642 --- /dev/null +++ b/packages/components/src/components/Modal/Modal.tsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import { FaTimes } from 'react-icons/fa'; +import { Colors, Size, Type , getFormLabelSize } from '../../global'; +import { IconButton } from '../IconButton'; +import './Modal.scss'; + +export interface IModalProps { + children: JSX.Element + initialIsOpen: boolean + title?: string + backgroundColor?: string +} + +export const Modal = (props: IModalProps) => { + const { children, initialIsOpen, title, backgroundColor } = props + + const [ isOpen, setIsOpen ] = useState<boolean>(initialIsOpen) + + if (!isOpen) return null + return ( + <div className="modal-container"> + <div className={'modal-popup'} style={{backgroundColor: backgroundColor ? backgroundColor : Colors.WHITE}}> + {children} + <div className={'modal-closeButton'}> + <IconButton + size={Size.SMALL} + type={Type.TERT} + onClick={() => setIsOpen(false)} + icon={<FaTimes />} + /> + </div> + </div> + <div className={'modal-background'} onClick={() => setIsOpen(false)} /> + </div> + ) +} diff --git a/packages/components/src/components/Modal/index.ts b/packages/components/src/components/Modal/index.ts new file mode 100644 index 000000000..8d3bcd7a0 --- /dev/null +++ b/packages/components/src/components/Modal/index.ts @@ -0,0 +1 @@ +export * from './Modal' diff --git a/packages/components/src/components/MultiToggle/MultiToggle.scss b/packages/components/src/components/MultiToggle/MultiToggle.scss new file mode 100644 index 000000000..2522549e9 --- /dev/null +++ b/packages/components/src/components/MultiToggle/MultiToggle.scss @@ -0,0 +1,5 @@ +@import '../../global/globalCssVariables.scss'; + +.multiToggle-container { + +}
\ No newline at end of file diff --git a/packages/components/src/components/MultiToggle/MultiToggle.stories.tsx b/packages/components/src/components/MultiToggle/MultiToggle.stories.tsx new file mode 100644 index 000000000..e71423d7a --- /dev/null +++ b/packages/components/src/components/MultiToggle/MultiToggle.stories.tsx @@ -0,0 +1,69 @@ +import { Meta, Story } from '@storybook/react' +import React from 'react' +import { IMultiToggleProps, MultiToggle } from './MultiToggle' +import { FaAlignLeft, FaAlignCenter, FaAlignJustify, FaAlignRight } from 'react-icons/fa' + +export default { + title: 'Dash/MultiToggle', + component: MultiToggle, + argTypes: {}, +} as Meta<typeof MultiToggle> + +const MultiToggleStory: Story<IMultiToggleProps> = (args) => <MultiToggle {...args} /> +export const MultiToggleOne = MultiToggleStory.bind({}) +MultiToggleOne.args = { + tooltip: "Text alignment", + label: "Alignment", + defaultSelectedItems: "center", + toggleStatus: true, + isToggle: false, + items: [ + { + icon: <FaAlignLeft/>, + tooltip: 'Align left', + val: "left" + }, + { + icon: <FaAlignCenter/>, + tooltip: 'Align center', + val: "center" + }, + { + icon: <FaAlignRight/>, + tooltip: 'Align right', + val: "right" + }, + { + icon: <FaAlignJustify/>, + tooltip: 'Justify', + val: "justify" + }, + ] +} + +export const MultiToggleTwo = MultiToggleStory.bind({}) +MultiToggleTwo.args = { + tooltip: "Text Tags", + label: "Tags", + defaultSelectedItems : ["left"], + background: "green", + color: 'white', + multiSelect: true, + items: [ + { + icon: <FaAlignLeft/>, + tooltip: 'Like', + val: "left" + }, + { + icon: <FaAlignCenter/>, + tooltip: 'Todo', + val: "center" + }, + { + icon: <FaAlignRight/>, + tooltip: 'Idea', + val: "right" + }, + ] +} diff --git a/packages/components/src/components/MultiToggle/MultiToggle.tsx b/packages/components/src/components/MultiToggle/MultiToggle.tsx new file mode 100644 index 000000000..7fff12c8e --- /dev/null +++ b/packages/components/src/components/MultiToggle/MultiToggle.tsx @@ -0,0 +1,87 @@ +import * as React from 'react' +import { useState } from 'react' +import { Colors, IGlobalProps, Type } from '../../global' +import { Group } from '../Group' +import { IconButton } from '../IconButton' +import { Popup } from '../Popup' +import { IToggleProps, Toggle, ToggleType } from '../Toggle' + +export interface IToggleItemProps extends IToggleProps { + val: string +} + +export interface IMultiToggleProps extends IGlobalProps { + items: IToggleItemProps[] + multiSelect?: boolean; + defaultSelectedItems?: (string|number) | ((string|number)[]), + selectedItems?: (string | number) | ((string|number)[]), + onSelectionChange?: (val: (string|number) | (string|number)[], added: boolean) => unknown, + isToggle?: boolean; + toggleStatus?: boolean; +} + +function promoteToArrayOrUndefined(d : (string|number)[]|(string|number)|undefined) { + return d instanceof Array || d === undefined ? d: [d]; +} +function promoteToArray(d : (string|number)[]|(string|number)|undefined) { + return promoteToArrayOrUndefined(d) ?? []; +} + +export const MultiToggle = (props: IMultiToggleProps) => { + let init = true; + const initVal = (!init ? undefined : promoteToArrayOrUndefined(props.defaultSelectedItems)) ?? promoteToArrayOrUndefined(props.selectedItems) ?? []; + init = false; + + const [selectedItemsLocal, setSelectedItemsLocal] = useState(initVal as (string|number) | ((string|number)[])); + const { items, selectedItems = selectedItemsLocal, tooltip, tooltipPlacement = 'top', onSelectionChange, color, background } = props; + const itemsMap = new Map(); + items.forEach((item) => itemsMap.set(item.val, item)); + return <div className={`multiToggle-container`}> + <Popup + toggle={props.isToggle? undefined : <div style={{position: "relative"}}> + <IconButton + color={color} + borderColor={background ? color : undefined} + label={props.label} + active={props.toggleStatus} + background={background} + type={color && background ? Type.TERT : undefined} + {...(itemsMap.get(promoteToArray(selectedItems)[0]) ?? {})} + tooltip={tooltip} + tooltipPlacement={tooltipPlacement} + /> + {promoteToArray(selectedItems).length < 2 ? null : + <div style={{position: "absolute", top: "0", left: "0", color: color ?? Colors.MEDIUM_BLUE}}> + + + </div>} + </div>} + isToggle={props.isToggle} + toggleFunc={() => { + const selItem = items.find(item => promoteToArray(selectedItems).includes(item.val)); + selItem && setSelectedItemsLocal([selItem.val]); + }} + type={props.type} + label={props.isToggle ? props.label : undefined} + toggleStatus={props.isToggle ? props.toggleStatus : undefined} + color={color} + popup={<Group padding={5} color={color} columnGap={0} style={{overflow: 'hidden'}}> + {items.map((item, i) => + <Toggle key={i} color={color} icon={item.icon} tooltip={item.tooltip} + toggleStatus={promoteToArray(selectedItems).includes(item.val)} + type={Type.PRIM} + toggleType={ToggleType.BUTTON} + onClick={e => { + const selected = new Set<string|number>(); + promoteToArray(selectedItems).forEach(val => val && selected.add(val)); + const toAdd = !props.multiSelect || !selected.has(item.val) + if (!toAdd) selected.delete(item.val); + else item.val && selected.add(item.val); + onSelectionChange?.(item.val, toAdd); + setSelectedItemsLocal(props.multiSelect ? Array.from(selected) : item.val); + e.stopPropagation(); + }}/> + )} + </Group>} + /> + </div> +}
\ No newline at end of file diff --git a/packages/components/src/components/MultiToggle/index.ts b/packages/components/src/components/MultiToggle/index.ts new file mode 100644 index 000000000..ac079f76e --- /dev/null +++ b/packages/components/src/components/MultiToggle/index.ts @@ -0,0 +1 @@ +export * from './MultiToggle'
\ No newline at end of file diff --git a/packages/components/src/components/NumberDropdown/NumberDropdown.scss b/packages/components/src/components/NumberDropdown/NumberDropdown.scss new file mode 100644 index 000000000..0999afb98 --- /dev/null +++ b/packages/components/src/components/NumberDropdown/NumberDropdown.scss @@ -0,0 +1,5 @@ +@import '../../global/globalCssVariables.scss'; + +.numberDropdown-container { + +}
\ No newline at end of file diff --git a/packages/components/src/components/NumberDropdown/NumberDropdown.stories.tsx b/packages/components/src/components/NumberDropdown/NumberDropdown.stories.tsx new file mode 100644 index 000000000..669228e88 --- /dev/null +++ b/packages/components/src/components/NumberDropdown/NumberDropdown.stories.tsx @@ -0,0 +1,34 @@ +import { Meta, Story } from '@storybook/react' +import React, { useState } from 'react' +import { INumberDropdownProps, NumberDropdown } from './NumberDropdown' +import { Size , getFormLabelSize } from '../../global' + +export default { + title: 'Dash/NumberDropdown', + component: NumberDropdown, + argTypes: {}, +} as Meta<typeof NumberDropdown> + +// const [number, setNumber] = useState<number>(0) + +const Template: Story<INumberDropdownProps> = (args) => <NumberDropdown {...args} setNumber={val => console.log(val)} /> +export const NumberInputOne = Template.bind({}) +NumberInputOne.args = { + min: 0, + max: 50, + step: 1, + // number: number, + // setNumber: setNumber, + width: 100, + height: 100, + size: Size.SMALL, + numberDropdownType: 'slider' +} + +export const NumberInputTwo = Template.bind({}) +NumberInputTwo.args = { + min: 0, + max: 50, + step: 2, + numberDropdownType: 'dropdown' +} diff --git a/packages/components/src/components/NumberDropdown/NumberDropdown.tsx b/packages/components/src/components/NumberDropdown/NumberDropdown.tsx new file mode 100644 index 000000000..a26cd71ab --- /dev/null +++ b/packages/components/src/components/NumberDropdown/NumberDropdown.tsx @@ -0,0 +1,137 @@ +import * as React from 'react' +import { Colors, IGlobalProps, INumberProps, Size, Type, getFontSize , getFormLabelSize } from '../../global' +import { Popup } from '../Popup' +import { Toggle, ToggleType } from '../Toggle' +import { useState } from 'react' +import { Slider } from '../Slider' +import { ListBox } from '../ListBox' +import { IListItemProps } from '../ListItem' +import { Group } from '../Group' +import { IconButton } from '../IconButton' +import * as fa from 'react-icons/fa' + + +export type NumberDropdownType = 'slider' | 'dropdown' | 'input' + +export interface INumberDropdownProps extends INumberProps { + numberDropdownType: NumberDropdownType, + showPlusMinus?: boolean +} + +export const NumberDropdown = (props: INumberDropdownProps) => { + const [numberLoc, setNumberLoc] = useState<number>(0) + const { + fillWidth, + numberDropdownType = false, + color = Colors.MEDIUM_BLUE, + type, + formLabelPlacement, + showPlusMinus, + min, + max, + unit, + step = 1, + number = numberLoc, + setNumber = setNumberLoc, + size, + formLabel, + tooltip } = + props; + const [isOpen, setOpen] = useState<boolean>(false); + let toggleText = number.toString(); + if (unit) toggleText = toggleText + unit + let toggle = <Toggle + tooltip={tooltip} + color={color} + fillWidth={fillWidth} + type={type} + size={size} + align={'center'} + text={toggleText} + toggleType={ToggleType.BUTTON} + toggleStatus={isOpen} + onPointerDown={() => setOpen(!isOpen)} + />; + + if (showPlusMinus) { + toggle = <Group columnGap={0} style={{overflow: 'hidden'}}> + <IconButton + size={size} + icon={<fa.FaMinus/>} + color={color} + onClick={(e) => { + e.stopPropagation(); + setNumber(number - step); + }} + fillWidth={fillWidth} + tooltip={`Subtract ${step}${unit}`} + /> + {toggle} + <IconButton + size={size} + icon={<fa.FaPlus/>} + color={color} + onClick={(e) => { + e.stopPropagation(); + setNumber(number + step); + }} + fillWidth={fillWidth} + tooltip={`Add ${step}${unit}`} + /> + </Group> + } + + let popup; + switch (numberDropdownType) { + case 'dropdown': + let items: IListItemProps[] = []; + for (let i = min; i <= max; i += step) { + let text = i.toString() + if (unit) text = i.toString() + unit + items.push( + { + text: text, + val: i, + style: { textAlign: 'center' } + } + ) + } + popup = <ListBox + color={color} + selectedVal={number} + setSelectedVal={(num) => setNumber(num as number)} + items={items} + /> + break; + case 'slider': + default: + popup = <Slider size={Size.SMALL} unit={unit} multithumb={false} min={min} max={max} step={step} number={number} setNumber={setNumber}/> + break; + case 'input': + popup = <Slider multithumb={false} min={min} max={max} step={step} number={number}/> + break; + } + + const numberDropdown: JSX.Element = <div className={`numberDropdown-container`} style={{width: fillWidth ? '100%' : 'fit-content'}}> + <Popup + setOpen={setOpen} + placement={'bottom'} + isOpen={isOpen} + popup={popup} + toggle={toggle} + fillWidth={fillWidth} + color={color} + /> + </div> + + return ( + formLabel ? + <div className={`form-wrapper ${formLabelPlacement}`} +style={{ width: fillWidth ? '100%' : undefined}}> + {numberDropdown} + <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> + </div> + : + numberDropdown + ) +}
\ No newline at end of file diff --git a/packages/components/src/components/NumberDropdown/index.ts b/packages/components/src/components/NumberDropdown/index.ts new file mode 100644 index 000000000..f075b0950 --- /dev/null +++ b/packages/components/src/components/NumberDropdown/index.ts @@ -0,0 +1 @@ +export * from './NumberDropdown'
\ No newline at end of file diff --git a/packages/components/src/components/NumberInput/NumberInput.scss b/packages/components/src/components/NumberInput/NumberInput.scss new file mode 100644 index 000000000..2a562d395 --- /dev/null +++ b/packages/components/src/components/NumberInput/NumberInput.scss @@ -0,0 +1,5 @@ +@import '../../global/globalCssVariables.scss'; + +.numberInput-container { + width: 100%; +}
\ No newline at end of file diff --git a/packages/components/src/components/NumberInput/NumberInput.stories.tsx b/packages/components/src/components/NumberInput/NumberInput.stories.tsx new file mode 100644 index 000000000..85c91b12a --- /dev/null +++ b/packages/components/src/components/NumberInput/NumberInput.stories.tsx @@ -0,0 +1,20 @@ +import { Meta, Story } from '@storybook/react' +import React from 'react' +import { INumberInputProps, NumberInput } from './NumberInput' + +export default { + title: 'Dash/NumberInput', + component: NumberInput, + argTypes: {}, +} as Meta<typeof NumberInput> + +const NumberInputStory: Story<INumberInputProps> = (args) => <NumberInput {...args} /> +export const NumberInputOne = NumberInputStory.bind({}) +NumberInputOne.args = { + +} + +export const NumberInputTwo = NumberInputStory.bind({}) +NumberInputTwo.args = { + +} diff --git a/packages/components/src/components/NumberInput/NumberInput.tsx b/packages/components/src/components/NumberInput/NumberInput.tsx new file mode 100644 index 000000000..33573d000 --- /dev/null +++ b/packages/components/src/components/NumberInput/NumberInput.tsx @@ -0,0 +1,89 @@ +import * as React from 'react' +import { Colors, INumberProps , Type, getFormLabelSize, getHeight } from '../../global' +import './NumberInput.scss' +import { useState } from 'react' +import { Group } from '../Group' +import { Toggle, ToggleType } from '../Toggle' +import { IconButton } from '../IconButton' +import * as fa from 'react-icons/fa' +import { EditableText } from '../EditableText' + +export interface INumberInputProps extends INumberProps { + showPlusMinus?: boolean +} + +export const NumberInput = (props: INumberInputProps) => { + const [numberLoc, setNumberLoc] = useState<number>(10) + const { + color = Colors.MEDIUM_BLUE, + type, + formLabelPlacement, + showPlusMinus, + min, + max, + unit = '', + width, + fillWidth = width ? true : false, + step = 1, + number = numberLoc, + setNumber = setNumberLoc, + size, + formLabel, + tooltip } = + props; + + let input = <EditableText + color={color} + type={type} + size={size} + val={number.toString() + unit} + // width={getHeight(undefined, size)} + textAlign={'center'} + fillWidth={fillWidth} + width={width && width - (showPlusMinus ? getHeight(undefined, size) * 4 : 0)} + setVal={val => setNumber(!isNaN(Number(val)) ? Number(val) : number)} + />; + + if (showPlusMinus) { + input = <Group columnGap={0} style={{overflow: 'hidden'}}> + {input} + <IconButton + size={size} + icon={<fa.FaMinus/>} + color={color} + onClick={(e) => { + e.stopPropagation(); + setNumber(number - step); + }} + inactive={number - step < min} + tooltip={`Subtract ${step}${unit}`} + /> + <IconButton + size={size} + icon={<fa.FaPlus/>} + color={color} + onClick={(e) => { + e.stopPropagation(); + setNumber(number + step); + }} + inactive={number + step > max} + tooltip={`Add ${step}${unit}`} + /> + </Group> + } + + + return ( + formLabel ? + <div className={`form-wrapper ${formLabelPlacement}`} style={{ width: fillWidth ? '100%' : undefined}}> + <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> + <div className={`numberInput-container`} style={{width: fillWidth ? '100%' : 'fit-content'}}> + {input} + </div> + </div> + : + <div className={`numberInput-container`} style={{width: fillWidth ? '100%' : 'fit-content'}}> + {input} + </div> + ) +}
\ No newline at end of file diff --git a/packages/components/src/components/NumberInput/index.ts b/packages/components/src/components/NumberInput/index.ts new file mode 100644 index 000000000..326ccfa4f --- /dev/null +++ b/packages/components/src/components/NumberInput/index.ts @@ -0,0 +1 @@ +export * from './NumberInput'
\ No newline at end of file diff --git a/packages/components/src/components/Overlay/Overlay.scss b/packages/components/src/components/Overlay/Overlay.scss new file mode 100644 index 000000000..5ba4f802c --- /dev/null +++ b/packages/components/src/components/Overlay/Overlay.scss @@ -0,0 +1,9 @@ +.overlay-container { + width: 100vw; + height: 100vh; + top: 0; + left: 0; + background: pink; + position: absolute; + pointer-events: none; +}
\ No newline at end of file diff --git a/packages/components/src/components/Overlay/Overlay.tsx b/packages/components/src/components/Overlay/Overlay.tsx new file mode 100644 index 000000000..949436e05 --- /dev/null +++ b/packages/components/src/components/Overlay/Overlay.tsx @@ -0,0 +1,12 @@ +import React from "react" +import "./Overlay.scss" + +export interface IOverlayProps { + elementMap?: Map<string, JSX.Element> +} + +export const Overlay = (props: IOverlayProps) => { + return <div id="browndashComponents-overlay" className="overlay-container"> + + </div> +}
\ No newline at end of file diff --git a/packages/components/src/components/Overlay/index.ts b/packages/components/src/components/Overlay/index.ts new file mode 100644 index 000000000..7aa6e9479 --- /dev/null +++ b/packages/components/src/components/Overlay/index.ts @@ -0,0 +1 @@ +export * from './Overlay' diff --git a/packages/components/src/components/Popup/Popup.scss b/packages/components/src/components/Popup/Popup.scss new file mode 100644 index 000000000..39dd2c947 --- /dev/null +++ b/packages/components/src/components/Popup/Popup.scss @@ -0,0 +1,30 @@ +@import '../../global/globalCssVariables.scss'; + +.popup-wrapper { + width: fit-content; + + &.fillWidth { + width: 100%; + } + + .trigger-container { + width: fit-content; + height: fit-content; + + &.fillWidth { + width: 100%; + } + } +} + +.popup-container { + display: flex; + height: fit-content; + min-width: fit-content; + width: fit-content; + position: relative; + border: solid 1px $black; + border-radius: $standard-border-radius; + overflow: hidden; + background: $white; +} diff --git a/packages/components/src/components/Popup/Popup.stories.tsx b/packages/components/src/components/Popup/Popup.stories.tsx new file mode 100644 index 000000000..8baa1a387 --- /dev/null +++ b/packages/components/src/components/Popup/Popup.stories.tsx @@ -0,0 +1,53 @@ +import { Meta, Story } from '@storybook/react' +import React from 'react' +import * as fa from 'react-icons/fa' +import { Colors, Size } from '../../global/globalEnums' +import { IPopupProps, Popup, PopupTrigger } from './Popup' +import { Overlay } from '../Overlay' + +export default { + title: 'Dash/Popup', + component: Popup, + argTypes: {}, +} as Meta<typeof Popup> + +const Template: Story<IPopupProps> = (args) => ( + <div> + <Popup {...args} >HELLO WORLD!</Popup> + </div> +) + +export const Primary = Template.bind({}) +Primary.args = { + icon: <fa.FaEllipsisH />, + title: 'Select company', + tooltip: 'Popup tooltip', + size: Size.SMALL, + popup: <div style={{background: "pink", padding: 10}}> + Hello world. + </div> +} + +export const Text = Template.bind({}) +Text.args = { + icon: <fa.FaEllipsisH />, + text: 'More', + tooltip: 'Popup', + size: Size.SMALL, + popup: <div style={{background: "blue", padding: 10}}> + This is a popup element. + </div> +} + +export const Hover = Template.bind({}) +Hover.args = { + icon: <fa.FaEllipsisH />, + trigger: PopupTrigger.HOVER, + text: 'More', + tooltip: 'Popup', + placement: 'right', + size: Size.SMALL, + popup: <div style={{background: "blue", padding: 10}}> + This is a popup element. + </div> +} diff --git a/packages/components/src/components/Popup/Popup.tsx b/packages/components/src/components/Popup/Popup.tsx new file mode 100644 index 000000000..5a1179c69 --- /dev/null +++ b/packages/components/src/components/Popup/Popup.tsx @@ -0,0 +1,167 @@ +import React, { useEffect, useRef, useState } from 'react' +import { Colors, IGlobalProps, Placement, Size , getFormLabelSize, isDark } from '../../global' +import { Toggle, ToggleType } from '../Toggle' +import './Popup.scss' +import { Popper } from '@mui/material' + +export enum PopupTrigger { + CLICK = "click", + HOVER = "hover", + HOVER_DELAY = "hover_delay" +} + +export interface IPopupProps extends IGlobalProps { + text?: string + icon?: JSX.Element | string, + iconPlacement?: Placement, + placement?: Placement, + size?: Size + height?: number + toggle?: JSX.Element; + popup: JSX.Element | string | (() => JSX.Element) + trigger?: PopupTrigger + toggleStatus?: boolean; + isOpen?: boolean; + setOpen?: (b: boolean) => void; + background?: string, + isToggle?: boolean; + toggleFunc?: () => void; + popupContainsPt?: (x:number, y:number) => boolean; +} + +/** + * + * @param props + * @returns + * + * TODO: add support for isMulti, isSearchable + * Look at: import Select from "react-select"; + */ +export const Popup = (props: IPopupProps) => { + + const [locIsOpen, locSetOpen] = useState<boolean>(false) + + const { + text, + size, + icon, + popup, + type, + color, + isOpen = locIsOpen, + setOpen = locSetOpen, + toggle, + tooltip, + trigger = PopupTrigger.CLICK, + placement = 'bottom-start', + width, + height, + fillWidth, + iconPlacement = 'left', + background = isDark(color) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY, + popupContainsPt + } = props + + const triggerRef = useRef(null); + const popperRef = useRef(null); + + let timeout = setTimeout(() => {}); + + const handlePointerAwayDown = (e: PointerEvent) => { + const rect = (popperRef.current as any)?.getBoundingClientRect(); + if (rect && !(rect.left < e.clientX && rect.top < e.clientY && rect.right > e.clientX && rect.bottom > e.clientY) && + !popupContainsPt?.(e.clientX, e.clientY)) { + e.preventDefault(); + setOpen(false); + } + } + + useEffect(() => { + if (isOpen) { + window.removeEventListener("pointerdown", handlePointerAwayDown, {capture:true}) + window.addEventListener("pointerdown", handlePointerAwayDown, {capture:true}); + return () => { + window.removeEventListener("pointerdown", handlePointerAwayDown, {capture:true}); + } + }}, [isOpen, popupContainsPt]) + + return ( + <div className={`popup-wrapper ${fillWidth && 'fillWidth'}`} > + <div + className={`trigger-container ${fillWidth && 'fillWidth'}`} + ref={triggerRef} + onClick={() => { + if (trigger === PopupTrigger.CLICK) setOpen (!isOpen) + }} + onPointerEnter={() => { + if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) { + clearTimeout(timeout); + setOpen(true) + } + }} + onPointerLeave={() => { + if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) { + timeout = setTimeout(() => setOpen(false), 1000); + } + }} + > + {toggle + ? + toggle + : + <Toggle + tooltip={tooltip} + size={size} + type={type} + color={color} + background={props.isToggle ? undefined : background} + toggleType={ToggleType.BUTTON} + icon={icon} + iconPlacement={iconPlacement} + text={text} + label={props.label} + toggleStatus={isOpen || props.toggleStatus} + onClick={() => { + if (trigger === PopupTrigger.CLICK) { + if (!props.isToggle || props.toggleStatus) { + setOpen(!isOpen) + } + props.toggleFunc?.(); + } + }} + fillWidth={fillWidth} + /> + } + </div> + <Popper + open={isOpen} + style={{zIndex: 20000}} + anchorEl={triggerRef.current} + placement={placement} + modifiers={[ + ]} + > + <div className={`popup-container`} ref={popperRef} + style={{width, height, background}} + onPointerDown={(e) => { + e.stopPropagation(); + }} + onPointerEnter={() => { + if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) { + clearTimeout(timeout); + setOpen(true); + } + }} + onPointerLeave={() => { + if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) { + timeout = setTimeout(() => setOpen(false), 200); + } + }} + > + {!isOpen ? (null): typeof popup === 'function' ? popup() : popup} + </div> + </Popper> + </div> + ) +} + diff --git a/packages/components/src/components/Popup/index.ts b/packages/components/src/components/Popup/index.ts new file mode 100644 index 000000000..5775e5ada --- /dev/null +++ b/packages/components/src/components/Popup/index.ts @@ -0,0 +1 @@ +export * from './Popup' diff --git a/packages/components/src/components/Slider/Slider.scss b/packages/components/src/components/Slider/Slider.scss new file mode 100644 index 000000000..9a9fc6172 --- /dev/null +++ b/packages/components/src/components/Slider/Slider.scss @@ -0,0 +1,168 @@ +@import '../../global/globalCssVariables.scss'; + +.slider-container { + display: flex; + position: relative; + justify-content: center; + align-items: center; + min-width: 200px; + width: 100%; + height: 100%; + font-family: $default-font; + + .selected-range { + width: 100%; + background: $medium-blue; + } + + .range { + position: absolute; + background: $light-gray; + } + + .box-minmax{ + width: 100%; + display: flex; + justify-content: space-between; + font-size: 20px; + color: $medium-blue; + position: absolute; + top: 110%; + } + .range-slider { + margin: 0px; + position: absolute; + width: 100%; + height: 100%; + top: 0px; + left: 0px; + + .rs-label-container { + display: flex; + position: absolute; + justify-content: center; + align-items: center; + overflow: visible; + border-radius: $standard-border-radius; + z-index: 45; + pointer-events: none; + + .rs-label { + display: flex; + font-size: smaller; + white-space: nowrap; + border-radius: 100%; + text-align: center; + text-wrap: wrap; + word-break: break-all; + justify-content: center; + align-items: center; + font-family: $default-font; + user-select: none; + pointer-events: none; + top: 0px; + width: fit-content; + border-radius: $standard-border-radius; + z-index: 40; + } + + } + + .rs-range { + width: 100%; + position: relative; + background: transparent; + pointer-events: none; + -webkit-appearance: none; + margin: 0px; + z-index: 20; + + &:focus { + outline: none; + } + + &::-webkit-slider-runnable-track { + width: 100%; + background: none; + cursor: pointer; + box-shadow: none; + -webkit-appearance: none; + pointer-events: none; + } + &::-moz-range-track { + width: 100%; + cursor: pointer; + box-shadow: none; + -webkit-appearance: none; + pointer-events: none; + } + + &::-webkit-slider-thumb { + cursor: ew-resize; + -webkit-appearance: none; + pointer-events: auto; + } + &::-moz-range-thumb { + cursor: pointer; + -webkit-appearance: none; + pointer-events: auto; + } + + &::-moz-focus-outer { + border: 0; + } + + &.xsmall { + &::-webkit-slider-runnable-track { + height: $xsmall; + } + + &::-webkit-slider-thumb { + height: $xsmall; + width: $xsmall; + border-radius: $xsmall; + } + } + + &.small { + &::-webkit-slider-runnable-track { + height: $small; + } + + &::-webkit-slider-thumb { + height: $small; + width: $small; + border-radius: $small; + } + } + + &.medium { + &::-webkit-slider-runnable-track { + height: $medium; + } + + &::-webkit-slider-thumb { + height: $medium; + width: $medium; + border-radius: $medium; + } + } + + &.large { + &::-webkit-slider-runnable-track { + height: $large; + } + + &::-webkit-slider-thumb { + height: $large; + width: $large; + border-radius: $large; + } + } + } + } + +} + + + diff --git a/packages/components/src/components/Slider/Slider.stories.tsx b/packages/components/src/components/Slider/Slider.stories.tsx new file mode 100644 index 000000000..bc7d52c09 --- /dev/null +++ b/packages/components/src/components/Slider/Slider.stories.tsx @@ -0,0 +1,42 @@ +import { Meta, Story } from '@storybook/react' +import React from 'react' +import { ISliderProps, Slider } from './Slider' + +export default { + title: 'Dash/Slider', + component: Slider, + argTypes: {}, +} as Meta<typeof Slider> + +const Template: Story<ISliderProps> = (args) => <Slider {...args} /> +export const Value = Template.bind({}) +Value.args = { + multithumb: false, + min: -1100.34234234234, + max: -100.2323423423423, + number: -190, + autorangeMinVal: 1, + autorange: 500, + decimals: 0, + step: 1, + onPointerDown: (e) => console.log("Slider Down"), + setNumber: (e) => console.log("Set num", e), + setFinalNumber: (v) => console.log("Slider final:" + v) +} + +export const MultiThumb = Template.bind({}) +MultiThumb.args = { + multithumb: true, + value: 33.333, + min: 0.3242342, + max: 100.234234234, + step: 0.1111, + decimals: 1, + minDiff: 15, + autorangeMinVal: 1, + autorangeMin: 100, + autorangeMultiplier: 2, + onPointerDown: (e) => console.log("Slider Down"), + setFinalNumber: (v) => console.log("Slider final:" + v), + setFinalEndNumber: (v) => console.log("Slider end final:" + v) +} diff --git a/packages/components/src/components/Slider/Slider.tsx b/packages/components/src/components/Slider/Slider.tsx new file mode 100644 index 000000000..3ca51efed --- /dev/null +++ b/packages/components/src/components/Slider/Slider.tsx @@ -0,0 +1,178 @@ +import React, { useEffect, useRef, useState } from 'react' +import { Colors, getFontSize, getHeight, IGlobalProps, Size , getFormLabelSize, isDark, INumberProps } from '../../global' +import './Slider.scss' + +export interface ISliderProps extends INumberProps { + multithumb: boolean + autorangeMinVal?: number // minimimum value that min can have when autoranging + autorangeMinSize?: number // minimum difference between min and max when autoranging + autorange?: number // automatically adjust min/max to be +/- autorange/2 around the current value when the thumb is 15% from the min/max, or when the multithumbs are within 20% of the range and the range is bigger than autorange + endNumber?: number + setEndNumber?: (newVal: number) => void + setFinalNumber?: (newVal: number) => void + setFinalEndNumber?: (newVal: number) => void + decimals?: number; + step?: number + minDiff?: number +} + +let lastVal = 0; // bcz: WHY do I have to do this?? the pointerdown event locks in the value of 'valLoc' when it's created so need some other way to get the current value to that old handler... +let lastEndVal = 0; + +export const Slider = (props: ISliderProps) => { + const [width, setWidth] = useState<number>(100); + const [valLoc, setNumberLoc] = useState<number>(props.number??(props.min + (props.max-props.min)/2)); + const [endNumberLoc, setEndNumberLoc] = useState<number>(props.endNumber??(props.min + 2*(props.max-props.min)/3)); + const [min, setMin] = useState<number>(props.min); + const [max, setMax] = useState<number>(props.max); + const { + formLabel, + formLabelPlacement, + multithumb, + autorange, + autorangeMinVal, + autorangeMinSize, + decimals, + step = 1, + number = valLoc, + endNumber = endNumberLoc, + minDiff = (max-min)/20, + size = Size.SMALL, + height, + unit, + onPointerDown, + setNumber, + setEndNumber, + setFinalNumber, + setFinalEndNumber, + color = Colors.MEDIUM_BLUE, + fillWidth + } = props + + const toDecimal = (num:number) => decimals !== undefined ? Math.round(num*Math.pow(10,decimals))/Math.pow(10,decimals): num; + + const getLeftPos = (locVal: number) => { + const dragger = getHeight(height,size) + return (((locVal-min)/ (max-min)) * (width-dragger)) + } + + const getValueLabel = (locVal: number): JSX.Element => { + return (<div className="rs-label-container" + style={{ + left: `${getLeftPos(locVal)}px`, + background: color, + color: isDark(color) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY, + fontSize: getFontSize(size), + height: getHeight(height, size), + width: getHeight(height, size), + top: 0 + }} + > + <span className="rs-label"> + {toDecimal(locVal)} + </span> + </div>) + } + const checkAutorange = () => { + if (autorange) { + const minval = multithumb ? Math.min(lastVal, lastEndVal) : lastVal; + const maxval = multithumb ? Math.max(lastVal, lastEndVal) : lastVal; + const autosize = Math.max(autorangeMinSize??0,(autorange ?? (maxval-minval)))/2; + if ((Math.abs((minval - min)/(max-min)) < .15) || (Math.abs((max - maxval)/(max-min)) < .15) || + (multithumb && maxval - minval < (max-min)/5 && autosize < max-min) + ) { + const newminval = autorangeMinVal !== undefined && minval-autosize < autorangeMinVal? autorangeMinVal : minval-autosize; + setMin(newminval) + setMax(newminval !== minval ? Math.max(maxval + autosize, newminval +autosize): maxval+autosize ) + } + } + } + + const valSlider = (which: string, val:number, onchange: (val:number) => void, setFinal: () => void) => { + const valPointerup = (e:PointerEvent) => { + document.removeEventListener('pointerup', valPointerup, true) + setFinal(); + checkAutorange(); + } + return (<div key={which} className={`range-slider ${size}`}> + {getValueLabel(val)} + <input + className={`rs-range ${size}`} + type="range" + color={color} + min={min} + max={max} + step={step} + value={val} + onPointerDown={e => document.addEventListener('pointerup', valPointerup, true)} + onChange={e => { + onchange(+e.target.value); + e.stopPropagation(); + }} + /> + </div>); + } + const onchange = (val:number) => { + if (autorangeMinVal && val < autorangeMinVal) val = autorangeMinVal; + setNumber?.(lastVal = Math.min(multithumb ? endNumber - (minDiff??0):Number.MAX_VALUE, val)) + setNumberLoc(lastVal = Math.min(multithumb ? endNumber - (minDiff??0):Number.MAX_VALUE, val)) + } + const onendchange = (val:number) => { + setEndNumber?.(lastEndVal = Math.max(number + (minDiff??0), val)) + setEndNumberLoc(lastEndVal = Math.max(number + (minDiff??0), val)) + } + const Slider:(JSX.Element|null)[] = [ + !multithumb ? (null) : valSlider("end", endNumberLoc,onendchange, () => setFinalEndNumber?.(lastEndVal)), + valSlider("start", valLoc, onchange, () => setFinalNumber?.(lastVal)) + ]; + + const slider: JSX.Element = ( + <div className={`slider-wrapper`} + onPointerEnter={e => { + lastVal = valLoc; + lastEndVal = endNumberLoc; + }} + style={{ + padding: `5px 0px ${getHeight(height, size)}px 0px`, + width: fillWidth ? '100%' : 'fit-content' + }}> + <div className="slider-container" + ref={r => { + r && new ResizeObserver(() => setWidth(+(r?.clientWidth??100))).observe(r); + setWidth(+(r?.clientWidth??100)); + }} + style={{height: getHeight(height, size)}} + onPointerDown={onPointerDown} + > + {Slider} + <div className="selected-range" style={{ + height: getHeight(height, size) / 10, + background: multithumb ? Colors.LIGHT_GRAY : color + + }}/> + <div className="range" style={{ + height: getHeight(height, size) / 10, + width: getLeftPos(endNumber) - getLeftPos(number), + left: getLeftPos(number) + getHeight(height, size), + display: multithumb ? undefined: 'none', + background: color, + }}/> + <div className="box-minmax" style={{ fontSize: getFontSize(size), color }}> + <span>{toDecimal(min)}{unit}</span> + <span>{toDecimal(max)}{unit}</span> + </div> + </div> + </div> + ) + + return ( + formLabel ? + <div className={`form-wrapper ${formLabelPlacement}`}> + <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> + {slider} + </div> + : + slider +) +} + diff --git a/packages/components/src/components/Slider/index.ts b/packages/components/src/components/Slider/index.ts new file mode 100644 index 000000000..fc56c48ea --- /dev/null +++ b/packages/components/src/components/Slider/index.ts @@ -0,0 +1 @@ +export * from './Slider'
\ No newline at end of file diff --git a/packages/components/src/components/Template/Template.scss b/packages/components/src/components/Template/Template.scss new file mode 100644 index 000000000..c91147200 --- /dev/null +++ b/packages/components/src/components/Template/Template.scss @@ -0,0 +1,5 @@ +@import '../../global/globalCssVariables.scss'; + +.template-container { + +}
\ No newline at end of file diff --git a/packages/components/src/components/Template/Template.stories.tsx b/packages/components/src/components/Template/Template.stories.tsx new file mode 100644 index 000000000..b2d687fae --- /dev/null +++ b/packages/components/src/components/Template/Template.stories.tsx @@ -0,0 +1,20 @@ +import { Meta, Story } from '@storybook/react' +import React from 'react' +import { ITemplateProps, Template } from './Template' + +export default { + title: 'Dash/Template', + component: Template, + argTypes: {}, +} as Meta<typeof Template> + +const TemplateStory: Story<ITemplateProps> = (args) => <Template {...args} /> +export const TemplateOne = TemplateStory.bind({}) +TemplateOne.args = { + +} + +export const TemplateTwo = TemplateStory.bind({}) +TemplateTwo.args = { + +} diff --git a/packages/components/src/components/Template/Template.tsx b/packages/components/src/components/Template/Template.tsx new file mode 100644 index 000000000..6c6b26516 --- /dev/null +++ b/packages/components/src/components/Template/Template.tsx @@ -0,0 +1,12 @@ +import * as React from 'react' +import { IGlobalProps , getFormLabelSize } from '../../global' + +export interface ITemplateProps extends IGlobalProps { + +} + +export const Template = (props: ITemplateProps) => { + return <div className={`template-container`}> + Template Component + </div> +}
\ No newline at end of file diff --git a/packages/components/src/components/Template/index.ts b/packages/components/src/components/Template/index.ts new file mode 100644 index 000000000..36b5f3f46 --- /dev/null +++ b/packages/components/src/components/Template/index.ts @@ -0,0 +1 @@ +export * from './Template'
\ No newline at end of file diff --git a/packages/components/src/components/Toggle/Toggle.scss b/packages/components/src/components/Toggle/Toggle.scss new file mode 100644 index 000000000..b2faa8d99 --- /dev/null +++ b/packages/components/src/components/Toggle/Toggle.scss @@ -0,0 +1,77 @@ +@import '../../global/globalCssVariables.scss'; + +.toggle-label { + position: relative; + bottom: 0; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + font-size: $xsmall-fontSize; +} + +.toggle-container { + position: relative; + width: fit-content; + cursor: pointer; + overflow: hidden; + user-select: none; + display: flex; + justify-content: center; + align-items: center; + gap: 5px; + font-family: $default-font; + font-size: $medium-fontSize; + border-radius: 100px; + white-space: nowrap; + transition: 0.4s ease; + border: solid 1px; + border-color: $medium-blue; + + &:hover { + .toggle-background { + filter: opacity(0.2); + } + } + + &.switch { + &:hover { + .toggle-background { + filter: opacity(0); + } + } + } + + .toggle-content { + position: absolute; + display: flex; + align-items: center; + width: 100%; + height: 100%; + z-index: 1; + text-transform: uppercase; + font-family: Verdana, sans-serif; + font-weight: 500; + transition: 0.4s; + + .toggle-switch { + background: $medium-blue; + transition: 0.4s; + border-radius: 100px; + } + } + + .toggle-background { + width: 100%; + height: 100%; + z-index: 0; + position: absolute; + background: $medium-blue; + transition: 0.4s ease; + filter: opacity(0); + + &.active { + filter: opacity(0.4) !important; + } + } +} diff --git a/packages/components/src/components/Toggle/Toggle.stories.tsx b/packages/components/src/components/Toggle/Toggle.stories.tsx new file mode 100644 index 000000000..28ab2e712 --- /dev/null +++ b/packages/components/src/components/Toggle/Toggle.stories.tsx @@ -0,0 +1,35 @@ +import { Meta, Story } from '@storybook/react' +import React from 'react' +import * as bi from 'react-icons/bi' +import { IToggleProps, Toggle, ToggleType } from './Toggle' +import { Type , getFormLabelSize } from '../../global' + +export default { + title: 'Dash/Toggle', + component: Toggle, + argTypes: {}, +} as Meta<typeof Toggle> + +const Template: Story<IToggleProps> = (args) => <Toggle {...args} /> + +export const Button = Template.bind({}) +Button.args = { + // text: 'Button', + type: Type.TERT, + icon: <bi.BiAbacus/>, + toggleType: ToggleType.BUTTON, + tooltip: 'Test tooltip' +} + +export const Checkbox = Template.bind({}) +Checkbox.args = { + type: Type.SEC, + toggleType: ToggleType.CHECKBOX +} + +export const Switch = Template.bind({}) +Switch.args = { + text: 'Button', + type: Type.SEC, + toggleType: ToggleType.SWITCH +}
\ No newline at end of file diff --git a/packages/components/src/components/Toggle/Toggle.tsx b/packages/components/src/components/Toggle/Toggle.tsx new file mode 100644 index 000000000..5cc2ae339 --- /dev/null +++ b/packages/components/src/components/Toggle/Toggle.tsx @@ -0,0 +1,169 @@ +import { Tooltip } from '@mui/material' +import React, { useState } from 'react' +import * as bi from 'react-icons/bi' +import { IGlobalProps, Placement, Type , getFormLabelSize } from '../../global' +import { Size } from '../../global/globalEnums' +import { getFontSize, getHeight } from '../../global/globalUtils' +import { Button, IButtonProps } from '../Button' +import { IconButton } from '../IconButton' +import './Toggle.scss' + +export enum ToggleType { + BUTTON = "button", + CHECKBOX = "checkbox", + SWITCH = "switch", +} + +export interface IToggleProps extends IButtonProps { + toggleStatus?: boolean // true -> selected, false -> unselected + toggleType?: ToggleType + iconFalse?: JSX.Element | string +} + +export const Toggle = (props: IToggleProps) => { + const [toggleStatusLoc, setToggleStatusLoc] = useState<boolean>(true); + const { + toggleStatus = toggleStatusLoc, + toggleType = ToggleType.CHECKBOX, + type = Type.SEC, + style, + color, + background, + text, + icon, + iconFalse = icon, + height, + inactive, + label, + iconPlacement, + onPointerDown, + onClick, + tooltip, + tooltipPlacement = 'top', + size = Size.SMALL, + formLabel, + formLabelPlacement, + fillWidth, + align + } = props + + /** + * Pointer down + * @param e + */ + const handlePointerDown = (e: React.PointerEvent) => { + if (!inactive && onPointerDown){ + e.stopPropagation(); + e.preventDefault(); + onPointerDown(e) + } + } + + /** + * Single click + * @param e + */ + const handleClick = (e: React.MouseEvent) => { + if (toggleStatus === toggleStatusLoc) { + setToggleStatusLoc(!toggleStatus) + } + + if (!inactive && onClick) { + e.stopPropagation(); + e.preventDefault(); + onClick(e); + } + } + + const defaultProperties = { + height: getHeight(height, size), + borderColor: color + } + + let toggleElement: JSX.Element; + + switch(toggleType) { + case ToggleType.BUTTON: + toggleElement = ( + <Button + text={text} + tooltip={tooltip} + icon={toggleStatus ? icon : iconFalse} + onPointerDown={handlePointerDown} + onClick={handleClick} + active={toggleStatus} + type={type} + size={size} + iconPlacement={iconPlacement} + color={color} + background={background} + label={label} + fillWidth={fillWidth} + align={align} + /> + ); + break; + case ToggleType.CHECKBOX: + toggleElement = ( + <IconButton + icon={ + toggleStatus ? <bi.BiCheck/> : undefined + } + tooltip={tooltip} + onPointerDown={handlePointerDown} + onClick={handleClick} + active={toggleStatus} + type={type} + size={size} + color={color} + background={background} + label={label} + fillWidth={fillWidth} + align={align} + /> + ); + break; + case ToggleType.SWITCH: + default: + toggleElement = ( + <Tooltip disableInteractive={true} arrow={true} placement={tooltipPlacement} title={tooltip}> + <div + className={`toggle-container ${toggleType}`} + onPointerDown={handlePointerDown} + onClick={handleClick} + style={{ + width: 2*getHeight(height, size), + ...defaultProperties + }} + > + <div className="toggle-content" style={{ + fontSize: getFontSize(size), + borderColor: color, + left: toggleStatus ? '0%' : `calc(100% - ${getHeight(height, size)}px)` + }}> + <div className="toggle-switch" style={{ + width: getHeight(height, size), + height: getHeight(height, size), + background: color + }}></div> + </div> + <div className={`toggle-background ${toggleStatus && 'active'}`} + style={{ background: color}} + /> + </div> + </Tooltip> + ); + break; + } + + + return ( + formLabel ? + <div className={`form-wrapper ${formLabelPlacement}`}> + <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> + {toggleElement} + </div> + : + toggleElement + ) +} diff --git a/packages/components/src/components/Toggle/index.ts b/packages/components/src/components/Toggle/index.ts new file mode 100644 index 000000000..dce7f3909 --- /dev/null +++ b/packages/components/src/components/Toggle/index.ts @@ -0,0 +1 @@ +export * from './Toggle' diff --git a/packages/components/src/components/index.ts b/packages/components/src/components/index.ts new file mode 100644 index 000000000..c490ad550 --- /dev/null +++ b/packages/components/src/components/index.ts @@ -0,0 +1,16 @@ +export * from './Button' +export * from './ColorPicker' +export * from './Dropdown' +export * from './EditableText' +export * from './MultiToggle' +export * from './IconButton' +export * from './ListBox' +export * from './Popup' +export * from './Modal' +export * from './Group' +export * from './Slider' +export * from './Toggle' +export * from './ListItem' +export * from './Overlay' +export * from './NumberDropdown' +export * from './NumberInput' diff --git a/packages/components/src/global/globalCssVariables.scss b/packages/components/src/global/globalCssVariables.scss new file mode 100644 index 000000000..1ac2ef45c --- /dev/null +++ b/packages/components/src/global/globalCssVariables.scss @@ -0,0 +1,160 @@ +// colors +$white: #ffffff; +$off-white: #fdfdfd; +$light-gray: #dfdfdf; +$medium-gray: #9f9f9f; +$dark-gray: #323232; +$black: #000000; +$light-blue: #bdddf5; +$light-blue-transparent: #bdddf590; +$medium-blue: #4476f7; + +$medium-blue-transparent: #4477f733; + +$medium-blue-alt: #4476f73d; +$pink: #e0217d; +$yellow: #f5d747; + +$close-red: #e48282; + +$drop-shadow: '#32323215'; + +//popup +$success-green: #4bb543; +$error-red: #ff9494; + +// background +$hover-background: rgba(0, 0, 0, 0.2); +$modal-background: rgba(0, 0, 0, 0.3); + +// sizes +$xsmall: 20px; +$small: 30px; +$medium: 40px; +$large: 50px; + +// text-sizes +$icon-fontSize: 15px; +$large-fontSize: 15px; +$medium-fontSize: 11px; +$small-fontSize: 9px; +$xsmall-fontSize: 7px; + +// fonts +$default-font: 'Roboto', Verdana, sans-serif; + +//padding +$minimum-padding: 4px; +$medium-padding: 16px; +$large-padding: 32px; + +//icon sizes +$icon-size: 28px; + +// fonts +$sans-serif: 'Roboto', sans-serif; +$large-header: 16px; +$body-text: 12px; +$small-text: 9px; +// $sans-serif: "Roboto Slab", sans-serif; + +// misc values +$search-thumnail-size: 130; +$topbar-height: 50px; +$antimodemenu-height: 36px; + +// dragged items +$contextMenu-zindex: 100000; // context menu shows up over everything +$radialMenu-zindex: 100000; // context menu shows up over everything + +// borders +$standard-border: solid 1px #9f9f9f; +$padding: 0px 5px; +// border radius +$standard-border-radius: 5px; + +// shadow +$standard-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); +$standard-button-shadow: rgb(0 0 0 / 20%) 0px 2px 4px -1px, + rgb(0 0 0 / 14%) 0px 4px 5px 0px, rgb(0 0 0 / 12%) 0px 1px 10px 0px; + +$dashboardselector-height: 32px; +$mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc +$docDecorations-zindex: 998; // then doc decorations appear over everything else +$remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right? +$COLLECTION_BORDER_WIDTH: 0; +$SCHEMA_DIVIDER_WIDTH: 4; +$MINIMIZED_ICON_SIZE: 24; +$MAX_ROW_HEIGHT: 44px; +$DFLT_IMAGE_NATIVE_DIM: 900px; +$LEFT_MENU_WIDTH: 60px; +$TREE_BULLET_WIDTH: 20px; + +:export { + contextMenuZindex: $contextMenu-zindex; + SCHEMA_DIVIDER_WIDTH: $SCHEMA_DIVIDER_WIDTH; + COLLECTION_BORDER_WIDTH: $COLLECTION_BORDER_WIDTH; + MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE; + MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT; + SEARCH_THUMBNAIL_SIZE: $search-thumnail-size; + ANTIMODEMENU_HEIGHT: $antimodemenu-height; + DASHBOARD_SELECTOR_HEIGHT: $dashboardselector-height; + DFLT_IMAGE_NATIVE_DIM: $DFLT_IMAGE_NATIVE_DIM; + LEFT_MENU_WIDTH: $LEFT_MENU_WIDTH; + TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH; +} + +.form-wrapper { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + gap: 0px; + padding-bottom: 5px; + + + .formLabel { + display: flex; + font-family: $default-font; + text-transform: uppercase; + opacity: 0.8; + min-width: 'fit-content' + } + + &.left { + flex-direction: row; + align-items: center; + gap: 3px; + + .formLabel { + text-align: left; + } + } + + &.right { + flex-direction: row-reverse; + justify-content: flex-end; + align-items: center; + gap: 3px; + + .formLabel { + text-align: right; + } + } + + &.top { + flex-direction: column; + gap: 1px; + } + + &.top-start { + flex-direction: column; + align-items: flex-start; + } + + &.top-end { + flex-direction: column; + align-items: flex-end; + } +} + diff --git a/packages/components/src/global/globalCssVariables.scss.d.ts b/packages/components/src/global/globalCssVariables.scss.d.ts new file mode 100644 index 000000000..59c2b3585 --- /dev/null +++ b/packages/components/src/global/globalCssVariables.scss.d.ts @@ -0,0 +1,17 @@ + +interface IGlobalScss { + contextMenuZindex: string; // context menu shows up over everything + SCHEMA_DIVIDER_WIDTH: string; + COLLECTION_BORDER_WIDTH: string; + MINIMIZED_ICON_SIZE: string; + MAX_ROW_HEIGHT: string; + SEARCH_THUMBNAIL_SIZE: string; + ANTIMODEMENU_HEIGHT: string; + DASHBOARD_SELECTOR_HEIGHT: string; + DFLT_IMAGE_NATIVE_DIM: string; + LEFT_MENU_WIDTH: string; + TREE_BULLET_WIDTH: string; +} +declare const globalCssVariables: IGlobalScss; + +export = globalCssVariables;
\ No newline at end of file diff --git a/packages/components/src/global/globalEnums.tsx b/packages/components/src/global/globalEnums.tsx new file mode 100644 index 000000000..bdeacccdb --- /dev/null +++ b/packages/components/src/global/globalEnums.tsx @@ -0,0 +1,52 @@ +export enum Colors { + BLACK = "#000000", + DARK_GRAY = "#323232", + MEDIUM_GRAY = "#9F9F9F", + LIGHT_GRAY = "#DFDFDF", + WHITE = "#FFFFFF", + MEDIUM_BLUE = "#4476F7", + MEDIUM_BLUE_ALT = "#4476f73d", // REDUCED OPACITY + LIGHT_BLUE = "#BDDDF5", + PINK = "#E0217D", + YELLOW = "#F5D747", + DROP_SHADOW = "#32323215", + ERROR_RED = "#FF9494", + SUCCESS_GREEN = "#4BB543", + TRANSPARENT = "transparent" +} + +export enum FontSize { + JUMBO_ICON = "5rem", + ICON = "3rem", + HEADER = "1.6rem", + DEFAULT = "1rem", + SECONDARY = "1.3rem", + LABEL = "0.6rem" +} + +export enum Padding { + MINIMUM_PADDING = "4px", + SMALL_PADDING = "8px", + MEDIUM_PADDING = "16px", + LARGE_PADDING = "32px", +} + +export enum IconSizes { + ICON_SIZE = "28px", +} + +export enum Borders { + STANDARD = "solid 1px #9F9F9F", + STANDARD_BORDER_RADIUS = '5px' +} + +export enum Shadows { + STANDARD_SHADOW = "0px 3px 4px rgba(0, 0, 0, 0.3)" +} + +export enum Size { + XSMALL = "xsmall", + SMALL = "small", + MEDIUM = "medium", + LARGE = "large" +}
\ No newline at end of file diff --git a/packages/components/src/global/globalTypes.ts b/packages/components/src/global/globalTypes.ts new file mode 100644 index 000000000..aa8451a9c --- /dev/null +++ b/packages/components/src/global/globalTypes.ts @@ -0,0 +1,87 @@ +import { PointerEventHandler } from "react" +import { Size } from "./globalEnums" + +export interface IGlobalProps { + // Size + size?: Size + height?: number + width?: number + fillWidth?: boolean + color?: string + background?: string + + // Type + type?: Type + + // Status + inactive?: boolean + + // Content + tooltip?: string + tooltipPlacement?: Placement + + // Label + label?: string + hideLabel?: boolean + + // Label when used in forms + formLabel?: string + formLabelPlacement?: Placement + + // Custom style + style?: React.CSSProperties + + // Global pointer events + onPointerDown?: PointerEventHandler | undefined; + onPointerDownCapture?: PointerEventHandler | undefined; + onPointerMove?: PointerEventHandler | undefined; + onPointerMoveCapture?: PointerEventHandler | undefined; + onPointerUp?: PointerEventHandler | undefined; + onPointerUpCapture?: PointerEventHandler | undefined; + onPointerCancel?: PointerEventHandler | undefined; + onPointerCancelCapture?: PointerEventHandler | undefined; + onPointerEnter?: PointerEventHandler | undefined; + onPointerEnterCapture?: PointerEventHandler | undefined; + onPointerLeave?: PointerEventHandler | undefined; + onPointerLeaveCapture?: PointerEventHandler | undefined; + onPointerOver?: PointerEventHandler | undefined; + onPointerOverCapture?: PointerEventHandler | undefined; + onPointerOut?: PointerEventHandler | undefined; + onPointerOutCapture?: PointerEventHandler | undefined; + onGotPointerCapture?: PointerEventHandler | undefined; + onGotPointerCaptureCapture?: PointerEventHandler | undefined; + onLostPointerCapture?: PointerEventHandler | undefined; + onLostPointerCaptureCapture?: PointerEventHandler | undefined; +} + +export interface INumberProps extends IGlobalProps { + min: number, + max: number, + step?: number, + number: number + setNumber?: (num: number) => unknown, + unit?: string +} + +export enum Type { + PRIM = "primary", + SEC = "secondary", + TERT = "tertiary", +} + +export type Placement = 'bottom-end' + | 'bottom-start' + | 'bottom' + | 'left-end' + | 'left-start' + | 'left' + | 'right-end' + | 'right-start' + | 'right' + | 'top-end' + | 'top-start' + | 'top' + +export type Alignment = 'flex-start' | 'flex-end' | 'center' + +export type TextAlignment = 'center' | 'left' | 'right'
\ No newline at end of file diff --git a/packages/components/src/global/globalUtils.tsx b/packages/components/src/global/globalUtils.tsx new file mode 100644 index 000000000..05648a863 --- /dev/null +++ b/packages/components/src/global/globalUtils.tsx @@ -0,0 +1,93 @@ +import { Colors, Size } from './globalEnums' +const Color = require('color') + +export interface ILocation { + top: number + left: number + width: number + height: number + override?: 'left' | 'bottom' | 'top' | 'right' +} + +export const getFormLabelSize = ( + size: Size | undefined, +) => { + switch (size) { + case Size.XSMALL: + return '7px' + case Size.SMALL: + return '10px' + case Size.MEDIUM: + return '13px' + case Size.LARGE: + return '14px' + default: + return '10px' + } +} + +export const getFontSize = ( + size: Size | undefined, + icon?: boolean +) => { + switch (size) { + case Size.XSMALL: + if (icon) return '11px' + return '9px' + case Size.SMALL: + if (icon) return '15px' + return '11px' + case Size.MEDIUM: + if (icon) return '17px' + return '14px' + case Size.LARGE: + if (icon) return '22px' + return '17px' + default: + if (icon) return '15px' + return '12px' + } +} + +export const getHeight = ( + height: number | undefined, + size: Size | undefined +) => { + if (height) return height + switch (size) { + case Size.XSMALL: + return 20 + case Size.SMALL: + return 30 + case Size.MEDIUM: + return 40 + case Size.LARGE: + return 50 + default: + return 30 + } +} + +export const colorConvert = (color: any) => { + try { + return color ? Color(color.toLowerCase()) : Color('transparent') + } catch (e) { + console.log('COLOR error:', e) + return Color('red') + } +} + +export const isDark = (color: any): boolean => { + if (color === undefined) return false + if (color === 'transparent') return false + if (color.startsWith?.('linear')) return false + const nonAlphaColor = color.startsWith('#') + ? (color as string).substring(0, 7) + : color.startsWith('rgba') + ? color.replace(/,.[^,]*\)/, ')').replace('rgba', 'rgb') + : color + const col = colorConvert(nonAlphaColor).rgb() + const colsum = col.red() + col.green() + col.blue() + if (colsum / col.alpha() > 400 || col.alpha() < 0.25) return false + else return true +} diff --git a/packages/components/src/global/index.ts b/packages/components/src/global/index.ts new file mode 100644 index 000000000..46fba143d --- /dev/null +++ b/packages/components/src/global/index.ts @@ -0,0 +1,3 @@ +export * from './globalEnums' +export * from './globalUtils' +export * from './globalTypes'
\ No newline at end of file diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts new file mode 100644 index 000000000..a4676022b --- /dev/null +++ b/packages/components/src/index.ts @@ -0,0 +1,2 @@ +export * from './components' +export * from './global' |