aboutsummaryrefslogtreecommitdiff
path: root/packages/components/src
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-03-10 16:13:04 -0400
committerbobzel <zzzman@gmail.com>2025-03-10 16:13:04 -0400
commitb7989dded8bb001876de6cbca59bf77935f0daf7 (patch)
tree0dba0665674db7bb84770833df0a4100d0520701 /packages/components/src
parent4979415d4604d280e81a162bf9a9d39c731d3738 (diff)
parent5bf944035c0ba94ad15245416f51ca0329a51bde (diff)
Merge branch 'master' into alyssa-starter
Diffstat (limited to 'packages/components/src')
-rw-r--r--packages/components/src/components/Button/Button.scss118
-rw-r--r--packages/components/src/components/Button/Button.stories.tsx94
-rw-r--r--packages/components/src/components/Button/Button.tsx195
-rw-r--r--packages/components/src/components/Button/index.ts1
-rw-r--r--packages/components/src/components/ColorPicker/ColorPicker.scss23
-rw-r--r--packages/components/src/components/ColorPicker/ColorPicker.stories.tsx37
-rw-r--r--packages/components/src/components/ColorPicker/ColorPicker.tsx156
-rw-r--r--packages/components/src/components/ColorPicker/index.ts1
-rw-r--r--packages/components/src/components/Dropdown/Dropdown.scss136
-rw-r--r--packages/components/src/components/Dropdown/Dropdown.stories.tsx84
-rw-r--r--packages/components/src/components/Dropdown/Dropdown.tsx188
-rw-r--r--packages/components/src/components/Dropdown/index.ts1
-rw-r--r--packages/components/src/components/DropdownSearch/DropdownSearch.scss122
-rw-r--r--packages/components/src/components/DropdownSearch/DropdownSearch.stories.tsx72
-rw-r--r--packages/components/src/components/DropdownSearch/DropdownSearch.tsx129
-rw-r--r--packages/components/src/components/DropdownSearch/index.ts1
-rw-r--r--packages/components/src/components/EditableText/EditableText.scss129
-rw-r--r--packages/components/src/components/EditableText/EditableText.stories.tsx34
-rw-r--r--packages/components/src/components/EditableText/EditableText.tsx176
-rw-r--r--packages/components/src/components/EditableText/index.ts1
-rw-r--r--packages/components/src/components/FormInput/FormInput.scss69
-rw-r--r--packages/components/src/components/FormInput/FormInput.stories.tsx21
-rw-r--r--packages/components/src/components/FormInput/FormInput.tsx27
-rw-r--r--packages/components/src/components/FormInput/index.ts1
-rw-r--r--packages/components/src/components/Group/Group.scss15
-rw-r--r--packages/components/src/components/Group/Group.stories.tsx92
-rw-r--r--packages/components/src/components/Group/Group.tsx49
-rw-r--r--packages/components/src/components/Group/index.ts1
-rw-r--r--packages/components/src/components/IconButton/IconButton.scss119
-rw-r--r--packages/components/src/components/IconButton/IconButton.stories.tsx74
-rw-r--r--packages/components/src/components/IconButton/IconButton.tsx153
-rw-r--r--packages/components/src/components/IconButton/index.ts1
-rw-r--r--packages/components/src/components/ListBox/ListBox.scss16
-rw-r--r--packages/components/src/components/ListBox/ListBox.stories.tsx66
-rw-r--r--packages/components/src/components/ListBox/ListBox.tsx44
-rw-r--r--packages/components/src/components/ListBox/index.ts1
-rw-r--r--packages/components/src/components/ListItem/ListItem.scss78
-rw-r--r--packages/components/src/components/ListItem/ListItem.stories.tsx21
-rw-r--r--packages/components/src/components/ListItem/ListItem.tsx97
-rw-r--r--packages/components/src/components/ListItem/index.ts1
-rw-r--r--packages/components/src/components/Modal/Modal.scss46
-rw-r--r--packages/components/src/components/Modal/Modal.stories.tsx21
-rw-r--r--packages/components/src/components/Modal/Modal.tsx36
-rw-r--r--packages/components/src/components/Modal/index.ts1
-rw-r--r--packages/components/src/components/MultiToggle/MultiToggle.scss1
-rw-r--r--packages/components/src/components/MultiToggle/MultiToggle.stories.tsx69
-rw-r--r--packages/components/src/components/MultiToggle/MultiToggle.tsx87
-rw-r--r--packages/components/src/components/MultiToggle/index.ts1
-rw-r--r--packages/components/src/components/NumberDropdown/NumberDropdown.scss16
-rw-r--r--packages/components/src/components/NumberDropdown/NumberDropdown.stories.tsx34
-rw-r--r--packages/components/src/components/NumberDropdown/NumberDropdown.tsx101
-rw-r--r--packages/components/src/components/NumberDropdown/index.ts1
-rw-r--r--packages/components/src/components/NumberInput/NumberInput.scss5
-rw-r--r--packages/components/src/components/NumberInput/NumberInput.stories.tsx20
-rw-r--r--packages/components/src/components/NumberInput/NumberInput.tsx89
-rw-r--r--packages/components/src/components/NumberInput/index.ts1
-rw-r--r--packages/components/src/components/Overlay/Overlay.scss9
-rw-r--r--packages/components/src/components/Overlay/Overlay.tsx12
-rw-r--r--packages/components/src/components/Overlay/index.ts1
-rw-r--r--packages/components/src/components/Popup/Popup.scss30
-rw-r--r--packages/components/src/components/Popup/Popup.stories.tsx53
-rw-r--r--packages/components/src/components/Popup/Popup.tsx164
-rw-r--r--packages/components/src/components/Popup/index.ts1
-rw-r--r--packages/components/src/components/Slider/Slider.scss163
-rw-r--r--packages/components/src/components/Slider/Slider.stories.tsx42
-rw-r--r--packages/components/src/components/Slider/Slider.tsx188
-rw-r--r--packages/components/src/components/Slider/index.ts1
-rw-r--r--packages/components/src/components/Template/Template.scss1
-rw-r--r--packages/components/src/components/Template/Template.stories.tsx20
-rw-r--r--packages/components/src/components/Template/Template.tsx12
-rw-r--r--packages/components/src/components/Template/index.ts1
-rw-r--r--packages/components/src/components/Toggle/Toggle.scss77
-rw-r--r--packages/components/src/components/Toggle/Toggle.stories.tsx35
-rw-r--r--packages/components/src/components/Toggle/Toggle.tsx169
-rw-r--r--packages/components/src/components/Toggle/index.ts1
-rw-r--r--packages/components/src/components/index.ts16
-rw-r--r--packages/components/src/global/globalCssVariables.scss160
-rw-r--r--packages/components/src/global/globalCssVariables.scss.d.ts17
-rw-r--r--packages/components/src/global/globalEnums.tsx52
-rw-r--r--packages/components/src/global/globalTypes.ts76
-rw-r--r--packages/components/src/global/globalUtils.tsx93
-rw-r--r--packages/components/src/global/index.ts3
-rw-r--r--packages/components/src/index.ts2
83 files changed, 4572 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..bbe2e2470
--- /dev/null
+++ b/packages/components/src/components/Button/Button.scss
@@ -0,0 +1,118 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.button-container {
+ position: relative;
+ width: fit-content;
+ padding: global.$padding;
+ cursor: pointer;
+ overflow: hidden;
+ user-select: none;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 5px;
+ font-family: global.$default-font;
+ border-radius: global.$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: global.$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: global.$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..32b912fe5
--- /dev/null
+++ b/packages/components/src/components/ColorPicker/ColorPicker.scss
@@ -0,0 +1,23 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.colorPicker-container {
+ display: flex;
+ border-radius: global.$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..632b470f0
--- /dev/null
+++ b/packages/components/src/components/ColorPicker/ColorPicker.tsx
@@ -0,0 +1,156 @@
+import React, { useRef, 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 toggleRef = useRef<HTMLDivElement | null>(null);
+ const getToggle = () => (
+ <div ref={toggleRef}>
+ {icon && !text ? (
+ <IconButton active={isOpen} tooltip={tooltip} type={type} color={color} size={size} icon={icon} colorPicker={selectedColor} fillWidth={fillWidth} />
+ ) : text ? (
+ <Button active={isOpen} tooltip={tooltip} size={size} type={type} color={color} text={text} icon={icon} align={'flex-start'} iconPlacement={'left'} colorPicker={selectedColor} fillWidth={fillWidth} />
+ ) : (
+ <IconButton active={isOpen} tooltip={tooltip} type={type} color={color} size={size} icon={icon} colorPicker={selectedColor} fillWidth={fillWidth} />
+ )}
+ </div>
+ );
+
+ 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 '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 'Block': return <BlockPicker color={selectedColor} triangle={'hide'} colors={colorPalette} onChange={onChange} onChangeComplete={onChangeComplete} />;
+ case 'Slider': return (
+ <div style={{ width: 200, height: 50 }}>
+ <SliderPicker color={selectedColor} onChange={onChange} onChangeComplete={onChangeComplete} />
+ </div>
+ );
+ } // prettier-ignore
+ };
+ 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) => {
+ const rect = toggleRef.current?.getBoundingClientRect();
+ return rect && rect.left < x && rect.top < y && rect.right > x && rect.bottom > y;
+ };
+
+ 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..f9ea2711a
--- /dev/null
+++ b/packages/components/src/components/Dropdown/Dropdown.scss
@@ -0,0 +1,136 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.dropdown {
+ margin-top: 10px;
+}
+
+.divider {
+ height: 1px;
+ width: 100%;
+ background: global.$medium-gray;
+}
+
+.dropdown-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ min-width: fit-content;
+ width: 100%;
+ border-radius: global.$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: global.$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: global.$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..b9b6f01b8
--- /dev/null
+++ b/packages/components/src/components/Dropdown/Dropdown.tsx
@@ -0,0 +1,188 @@
+import React, { 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;
+ toolTip?: string;
+ closeOnSelect?: boolean;
+ iconProvider?: (active: boolean, placement?: Placement) => JSX.Element;
+ selectedVal?: string;
+ setSelectedVal?: (val: string | number, e?: React.MouseEvent) => 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,
+ toolTip,
+ 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 = (isActive: boolean, caretPlacement: Placement = 'left'): JSX.Element => {
+ if (iconProvider) return iconProvider(isActive, caretPlacement);
+ switch (caretPlacement) {
+ default:
+ case 'bottom':return isActive ? <FaCaretUp />: <FaCaretDown />;
+ case 'right': return isActive ? <FaCaretLeft /> : <FaCaretRight />;
+ case 'top': return isActive ? <FaCaretDown />: <FaCaretUp />;
+ } // prettier-ignore
+ };
+
+ 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 = (isActive: boolean) => {
+ setActive(isActive);
+ props.activeChanged?.(isActive);
+ };
+
+ const dropdown: JSX.Element = (
+ <div className="dropdown-container">
+ <Popup
+ toggle={
+ <Tooltip disableInteractive={true} arrow={true} placement={tooltipPlacement} title={toolTip || (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, e) => {
+ setSelectedVal?.(val, e);
+ 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..d937df540
--- /dev/null
+++ b/packages/components/src/components/DropdownSearch/DropdownSearch.scss
@@ -0,0 +1,122 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.dropdownsearch {
+ margin-top: 10px;
+}
+
+.divider {
+ height: 1px;
+ width: 100%;
+ background: global.$medium-gray;
+}
+
+.dropdownsearch-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ min-width: fit-content;
+ border-radius: global.$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: global.$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: global.$medium-blue;
+ .toggle-background {
+ background: global.$medium-blue;
+ filter: opacity(0);
+ }
+
+ &:hover {
+ .toggle-background {
+ filter: opacity(0.2);
+ }
+ }
+ }
+
+ &.secondary {
+ .toggle-background {
+ background: global.$medium-blue;
+ filter: opacity(0);
+ }
+
+ border: solid 1px global.$medium-blue;
+ color: global.$medium-blue;
+
+ &:hover {
+ .toggle-background {
+ filter: opacity(0.2);
+ }
+ }
+ }
+
+ &.tertiary {
+ color: white;
+
+ .toggle-background {
+ background: global.$medium-blue;
+ }
+
+ &:hover {
+ box-shadow: global.$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..15965e97e
--- /dev/null
+++ b/packages/components/src/components/EditableText/EditableText.scss
@@ -0,0 +1,129 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.editableText-container {
+ position: relative;
+ width: fit-content;
+ border: solid 1px;
+ border-color: transparent;
+ border-radius: global.$standard-border-radius;
+ font-family: global.$default-font;
+ overflow: hidden;
+ padding: global.$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: global.$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..2554cbd01
--- /dev/null
+++ b/packages/components/src/components/FormInput/FormInput.scss
@@ -0,0 +1,69 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.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..7cd3dfd9e
--- /dev/null
+++ b/packages/components/src/components/Group/Group.scss
@@ -0,0 +1,15 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.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..f899dc50f
--- /dev/null
+++ b/packages/components/src/components/IconButton/IconButton.scss
@@ -0,0 +1,119 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.iconButton-container {
+ position: relative;
+ cursor: pointer;
+ overflow: hidden;
+ user-select: none;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-family: global.$default-font;
+ border-radius: global.$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: global.$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: global.$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..954a0ae44
--- /dev/null
+++ b/packages/components/src/components/IconButton/IconButton.tsx
@@ -0,0 +1,153 @@
+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..dc2a44513
--- /dev/null
+++ b/packages/components/src/components/ListBox/ListBox.scss
@@ -0,0 +1,16 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.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;
+}
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..aa5eb6b44
--- /dev/null
+++ b/packages/components/src/components/ListBox/ListBox.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { IListItemProps, ListItem } from '../ListItem';
+import './ListBox.scss';
+import { Colors, IGlobalProps } from '../../global';
+
+export interface IListBoxProps extends IGlobalProps {
+ items: IListItemProps[];
+ filter?: string;
+ selectedVal?: string | number;
+ setSelectedVal?: (val: string | number, e?: React.MouseEvent) => 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} />;
+ };
+ const 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..8d46605d0
--- /dev/null
+++ b/packages/components/src/components/ListItem/ListItem.scss
@@ -0,0 +1,78 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.listItem-container {
+ position: relative;
+ width: 100%;
+ border-radius: global.$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: global.$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: global.$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: global.$padding;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: global.$xsmall-fontSize;
+ font-family: global.$default-font;
+ }
+
+ .caret {
+ grid-area: end;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ justify-self: center;
+ }
+ }
+
+ .listItem-description {
+ font-size: global.$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..e04c6fbee
--- /dev/null
+++ b/packages/components/src/components/ListItem/ListItem.tsx
@@ -0,0 +1,97 @@
+import React, { useState } from 'react';
+import * as fa from 'react-icons/fa';
+import { getFontSize, IGlobalProps, Type, 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, e?: React.MouseEvent) => 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 { 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);
+
+ const listItem: JSX.Element = (
+ <div
+ tabIndex={-1}
+ className="listItem-container"
+ onPointerDown={e => onItemDown?.(e, val) && setSelectedVal?.(val, e)}
+ onClick={(e: React.MouseEvent) => {
+ if (!items) {
+ !inactive && onClick?.();
+ !inactive && onClick && e.stopPropagation();
+ setSelectedVal?.(val, e);
+ }
+ }}
+ 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..a5698432d
--- /dev/null
+++ b/packages/components/src/components/Modal/Modal.scss
@@ -0,0 +1,46 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.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: global.$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: global.$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..854abdca0
--- /dev/null
+++ b/packages/components/src/components/MultiToggle/MultiToggle.scss
@@ -0,0 +1 @@
+@use '../../global/globalCssVariables.scss' as global;
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..b3111623b
--- /dev/null
+++ b/packages/components/src/components/NumberDropdown/NumberDropdown.scss
@@ -0,0 +1,16 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.form-wrapper {
+ .iconButton-label {
+ position: relative;
+ z-index: 2;
+ width: 100%;
+ overflow: hidden;
+ white-space: normal;
+ display: flex;
+ text-align: center;
+ justify-content: center;
+ align-items: center;
+ font-size: global.$xsmall-fontSize !important;
+ }
+}
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..7f12198d5
--- /dev/null
+++ b/packages/components/src/components/NumberDropdown/NumberDropdown.tsx
@@ -0,0 +1,101 @@
+import * as React from 'react';
+import { Colors, INumberProps, Size, 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';
+import './NumberDropdown.scss';
+
+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':
+ {
+ const 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={{ height: '75%', width: fillWidth ? '100%' : 'fit-content' }}>
+ <Popup setOpen={setOpen} placement={'bottom'} size={size} isOpen={isOpen} popup={popup} toggle={toggle} fillWidth={fillWidth} color={color} />
+ </div>
+ );
+
+ return formLabel ? (
+ <div className={`form-wrapper ${formLabelPlacement}`} style={{ height: '100%', width: fillWidth ? '100%' : undefined }}>
+ {numberDropdown}
+ <div className="iconButton-label" style={{ fontSize: getFormLabelSize(size) }}>
+ {formLabel}
+ </div>
+ </div>
+ ) : (
+ numberDropdown
+ );
+};
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..887d64857
--- /dev/null
+++ b/packages/components/src/components/NumberInput/NumberInput.scss
@@ -0,0 +1,5 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.numberInput-container {
+ width: 100%;
+}
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..3087293f1
--- /dev/null
+++ b/packages/components/src/components/Popup/Popup.scss
@@ -0,0 +1,30 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.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 global.$black;
+ border-radius: global.$standard-border-radius;
+ overflow: hidden;
+ background: global.$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..5a58fee29
--- /dev/null
+++ b/packages/components/src/components/Popup/Popup.tsx
@@ -0,0 +1,164 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { Colors, IGlobalProps, Placement, Size, 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 | string;
+ 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<HTMLDivElement | null>(null);
+ const toggleRef = useRef<HTMLDivElement | null>(null);
+
+ let timeout = setTimeout(() => {});
+
+ const handlePointerAwayDown = (e: PointerEvent) => {
+ const rect = popperRef.current?.getBoundingClientRect();
+ const rect2 = toggleRef.current?.getBoundingClientRect();
+ if (
+ (!rect2 || !(rect2.left < e.clientX && rect2.top < e.clientY && rect2.right > e.clientX && rect2.bottom > e.clientY)) &&
+ 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);
+ e.stopPropagation();
+ }
+ };
+
+ 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 ?? (
+ <div ref={toggleRef}>
+ <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>
+ )}
+ </div>
+ <Popper open={isOpen} style={{ zIndex: 20000 }} anchorEl={triggerRef.current} placement={placement} modifiers={[]}>
+ <div
+ className={`popup-container`}
+ ref={popperRef}
+ style={{ width, height, background }}
+ tabIndex={-1}
+ 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..a653f024f
--- /dev/null
+++ b/packages/components/src/components/Slider/Slider.scss
@@ -0,0 +1,163 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.slider-container {
+ display: flex;
+ position: relative;
+ justify-content: center;
+ align-items: center;
+ min-width: 200px;
+ width: 100%;
+ height: 100%;
+ font-family: global.$default-font;
+
+ .selected-range {
+ width: 100%;
+ background: global.$medium-blue;
+ }
+
+ .range {
+ position: absolute;
+ background: global.$light-gray;
+ }
+
+ .box-minmax {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ font-size: 20px;
+ color: global.$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: global.$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: global.$default-font;
+ user-select: none;
+ pointer-events: none;
+ top: 0px;
+ width: fit-content;
+ border-radius: global.$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: global.$xsmall;
+ }
+
+ &::-webkit-slider-thumb {
+ height: global.$xsmall;
+ width: global.$xsmall;
+ border-radius: global.$xsmall;
+ }
+ }
+
+ &.small {
+ &::-webkit-slider-runnable-track {
+ height: global.$small;
+ }
+
+ &::-webkit-slider-thumb {
+ height: global.$small;
+ width: global.$small;
+ border-radius: global.$small;
+ }
+ }
+
+ &.medium {
+ &::-webkit-slider-runnable-track {
+ height: global.$medium;
+ }
+
+ &::-webkit-slider-thumb {
+ height: global.$medium;
+ width: global.$medium;
+ border-radius: global.$medium;
+ }
+ }
+
+ &.large {
+ &::-webkit-slider-runnable-track {
+ height: global.$large;
+ }
+
+ &::-webkit-slider-thumb {
+ height: global.$large;
+ width: global.$large;
+ border-radius: global.$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..5af945383
--- /dev/null
+++ b/packages/components/src/components/Slider/Slider.tsx
@@ -0,0 +1,188 @@
+import React, { useState } from 'react';
+import { Colors, getFontSize, getHeight, 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 || 0), 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 || 0), size),
+ width: getHeight(+(height || 0), 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)) < 0.15 || Math.abs((max - maxval) / (max - min)) < 0.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 = () => {
+ 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={() => document.addEventListener('pointerup', valPointerup, true)}
+ onChange={e => {
+ onchange(+e.target.value);
+ e.stopPropagation();
+ }}
+ />
+ </div>
+ );
+ };
+ const onchange = (val: number) => {
+ // eslint-disable-next-line no-param-reassign
+ 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 ValSlider: (JSX.Element | null)[] = [!multithumb ? null : valSlider('end', endNumberLoc, onendchange, () => setFinalEndNumber?.(lastEndVal)), valSlider('start', valLoc, onchange, () => setFinalNumber?.(lastVal))];
+
+ const slider = (
+ <div
+ className="slider-wrapper"
+ onPointerEnter={() => {
+ lastVal = valLoc;
+ lastEndVal = endNumberLoc;
+ }}
+ style={{
+ padding: `5px 0px ${getHeight(+(height || 0), 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 || 0), size) }}
+ onPointerDown={onPointerDown}>
+ {ValSlider}
+ <div
+ className="selected-range"
+ style={{
+ height: getHeight(+(height || 0), size) / 10,
+ background: multithumb ? Colors.LIGHT_GRAY : color,
+ }}
+ />
+ <div
+ className="range"
+ style={{
+ height: getHeight(+(height || 0), size) / 10,
+ width: getLeftPos(endNumber) - getLeftPos(number),
+ left: getLeftPos(number) + getHeight(+(height || 0), 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..854abdca0
--- /dev/null
+++ b/packages/components/src/components/Template/Template.scss
@@ -0,0 +1 @@
+@use '../../global/globalCssVariables.scss' as global;
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..d65cb8e23
--- /dev/null
+++ b/packages/components/src/components/Toggle/Toggle.scss
@@ -0,0 +1,77 @@
+@use '../../global/globalCssVariables.scss' as global;
+
+.toggle-label {
+ position: relative;
+ bottom: 0;
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: global.$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: global.$default-font;
+ font-size: global.$medium-fontSize;
+ border-radius: 100px;
+ white-space: nowrap;
+ transition: 0.4s ease;
+ border: solid 1px;
+ border-color: global.$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: global.$medium-blue;
+ transition: 0.4s;
+ border-radius: 100px;
+ }
+ }
+
+ .toggle-background {
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ position: absolute;
+ background: global.$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..ebc44106d
--- /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..764fe7422
--- /dev/null
+++ b/packages/components/src/global/globalTypes.ts
@@ -0,0 +1,76 @@
+import { PointerEventHandler } from 'react';
+import { Size } from './globalEnums';
+
+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';
+
+export interface IGlobalProps {
+ // Size
+ size?: Size;
+ height?: number | string;
+ 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;
+}
diff --git a/packages/components/src/global/globalUtils.tsx b/packages/components/src/global/globalUtils.tsx
new file mode 100644
index 000000000..0f1e0adbc
--- /dev/null
+++ b/packages/components/src/global/globalUtils.tsx
@@ -0,0 +1,93 @@
+import { Size } from './globalEnums'
+import Color from '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'