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