Skip to content

FLMNH-MGCL/ui

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

46 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ui

This repository holds the UI component library used in SpeSQL, developed using React (TypeScript) and Tailwind CSS.

Installation

You can install this library using either npm or yarn

yarn add @flmnh-mgcl/ui
npm install @flmnh-mgcl/ui

Tailwind Config

There is now an installable plugin available. To use the classes defined for the corresponding UI, install the plugin and add it to your own tailwind.config.js file as you would any other plugin.

yarn add @flmnh-mgcl/ui-tailwind-config
// -- tailwind.config.js --

module.exports = {
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  // ADD PLUGIN HERE
  plugins: [require('@flmnh-mgcl/ui-tailwind-config')],
};

Contributing

Feel free to contribute and make this project better! There are definitely areas that can be improved overall!

If you want to contribute, try to tackle any existing issues on GitHub first before attempting any other contributions.

Components Available

This project is still under active development, components will be added / altered throughout the duration of the project. These are the currently available components:

Note: Most components are based off of their counterparts in the Tailwind UI components. While they do not use Tailwind UI components, the styling itself is largely inspired from here.

Accordion

Available Props

export type AccordionItemProps = {
  open: boolean;
  title: string;
  content: string | string[] | React.ReactChild;
  onClick(): void;
};

export type AccordionProps = {
  items: Omit<AccordionItemProps, 'open' | 'onClick'>[];
};

Basic Example

import React from 'react';
import { Accordion } from '@flmnh-mgcl/ui';

const items = [
  {
    title: 'This is a React.ReactNode Accordion Item',
    content: (
      <div className="flex flex-col space-y-3">
        <Text>This is an example of a React.ReactNode Accordion Item.</Text>

        <Text>
          I developed it to allow for strings / string arrays / React.ReactNodes
          to allow for more customization, so that you can do more complex
          Accordion Items if needed.
        </Text>
      </div>
    ),
  },
  {
    title: 'This one is just a basic string content',
    content:
      'It will be rendered as a single <Text> component with {content} as the child',
  },
  {
    title: 'This one is just a string[] content',
    content:
      'It will be rendered as multiple <Text> components, one for each element in the content array, emulating paragraphs',
  },
];

function MyComponent() {
  return <Accordion items={items} />;
}

Badge

The Badge component is based off of Tailwind UIs badge component.

Available Props

The Badge component props has a mutually exclusive option of passing either a label OR a child(ren) ReactNode, as well as an onClick handler and a color selector. You may not pass in a child ReactNode if you pass a label prop or truncate prop, and vice-versa.

type BadgePropsLabel = {
  label: string;
  truncate?: boolean;
};

type BadgePropsChildren = {
  children: React.ReactNode;
};

type BadgeProps = MutuallyExclusive<BadgePropsLabel, BadgePropsChildren> & {
  onClick?(): void;
  color?: keyof typeof BADGE_COLORS; // currently: gray, red
};

Basic Example

import React from 'react';
import { Badge } from '@flmnh-mgcl/ui';

type ExampleProps = {
  useChild?: boolean;
};

function MyComponent({ useChild }: ExampleProps) {
  return useChild ? (
    <Badge>
      <p>This is my badge using a child p-tag</p>
    </Badge>
  ) : (
    <Badge label="This is my labeled badge!" />
  );
}

Button

Button has an exported member Button.Group, as well.

Available Props

Button has all of the props available from a traditional HTML button element, and has the following:

type Props = {
  variant?: keyof typeof BUTTONS; // defaults to 'default'
  fullWidth?: boolean; // w-full flex-1
  rounded?: boolean; // rounded-full
  loading?: boolean;
};

The current running list of variants are: primary, primaryBlue, default, danger, warning, clear, outline, danger_outline, activated

These variants are very subject to change

Basic Example

import React, { useState } from 'react';
import { Button } from '@flmnh-mgcl/ui';

function MyComponent() {
  const [isLoading, setLoading] = useState(true);

  useEffect(() => {
    if (isLoading) {
      setTimeout(() => setLoading(false), 2000);
    }
  }, []);

  return <Button loading={isLoading}>This is my button!</Button>;
}

Button.Group

Button.Group is really just a predefined, wrapping div for Button components. I typically use them in my Table footers or Modal footers.

Available Props

type ButtonGroupProps = {
  children: React.ReactNode;
  className?: string;
  gap?: keyof typeof BUTTON_GAPS; // sm, md, lg
};

Basic Example

import React from 'react';
import { Button } from '@flmnh-mgcl/ui';

function MyComponent() {
  return (
    <Button.Group>
      <Button>Left</Button>
      <Button variant="primary">Right</Button>
    </Button.Group>
  );
}

Checkmark

This is a rewrite of an external component, as it was throwing type errors directly from source. It is an animated checkmark.

Available Props

The props are interpreted directly from the source code linked above.

interface CheckmarkProps {
  size?: keyof typeof namedSizes; // small, medium, large, xLarge, xxLarge
  color?: string;
}

Basic Example

import React from 'react';
import { Checkmark } from '@flmnh-mgcl/ui';

function MyComponent() {
  return <Checkmark size="large" color="red" />;
}

Code

The Code component is a wrapper for the Prism component from react-syntax-hightlighter. I made this a custom, reusable component to inject some of the common changes I make:

Available Props

type ChildProps = {
  children: React.ReactText;
};

type StringProps = {
  codeString: string;
};

export type CodeProps = MutuallyExclusive<ChildProps, StringProps> & {
  rounded?: boolean;
  slim?: boolean;
  language?: string;
  theme?: 'light' | 'dark'; // default: localStorage.theme ?? 'dark'
  maxHeight?: string;
};

Please note: this is another mutually exclusive Prop. You may either use a codeString OR children with this component, but not both.

Basic Example

import React from 'react';
import { Code } from '@flmnh-mgcl/ui';

function MyComponent() {
  return <Code language="sql" codeString="SELECT * FROM myTable;" />;
}

Datepicker

Datepicker is a styled wrapper around react-day-picker. Internally, it utilizes react-hook-form via the rendered Input component that displays the date / date range.

Available Props

export type DateRange = { from?: Date; to?: Date };

export type RangedPickerProps = Omit<SinglePickerProps, 'initalDate'> & {
  initialDate?: DateRange;
};

export type SinglePickerProps = {
  label?: string;
  name?: string;
  futureOnly?: boolean;
  pastOnly?: boolean;
  ranged?: boolean;
  fullWidth?: boolean;
  initialDate?: string | Date;
  placeholder?: string;
} & PropsOf<typeof Form.Input>;

export type DatepickerProps = SinglePickerProps | RangedPickerProps;

Basic Example

import React from 'react';
import { Datepicker } from '@flmnh-mgcl/ui';

function MyComponent() {
  return (
    <div className="flex flex-col space-y-4">
      <Datepicker
        fullWidth
        name="mySinglePicker"
        label="mySinglePicker"
        placeholder={new Date().toISOString().split('T')[0]}
        register={{ validate: validatorFunction }}
        initialDate={new Date()}
      />
      <Datepicker
        fullWidth
        name="mySinglePicker2"
        label="mySinglePicker2"
        placeholder={new Date().toISOString().split('T')[0]}
        register={{ validate: validatorFunction }}
        defaultValue="2021-01-15"
      />
      <Datepicker
        fullWidth
        ranged
        name="myRangedPicker"
        label="myRangedPicker"
        register={{ validate: validatorFunction }}
        initialDate={{ from: new Date(), to: new Date() }}
      />
      {/* NOTE: example 3 throws a type error currently, and will be fixed in a future version */}
    </div>
  );
}

Divider

Divider is a simple horizontal line meant to separate bodies of text or other UI sections.

Available Props

export type DividerProps = {
  text?: string;
};

Basic Example

import React from 'react';
import { Divider } from '@flmnh-mgcl/ui';

function MyComponent() {
  return (
    <div>
      <p>section 1</p>
      <Divider />
      <p>section 2</p>
      <Divider text="and" />
      <p>section 3</p>
    </div>
  );
}

Dropdown

Dropdown is separate from Select components; Select is meant to be a form-based component, whereas Dropdown is meant to be a menu type component.

Dropdown has three internal members: Item, Section and Header. The props for all 3, in addition to the Dropdown component itself, are as follows:

Available Props

export type DropdownItemProps = {
  text: string;
  onClick?(): void;
};

export type DropdownSectionProps = { children: React.ReactNode };

export type DropdownHeaderProps = {
  text: string;
};

export type DropdownProps = {
  open?: boolean; // default: false
  label?: string;
  origin?: 'left' | 'right'; // default: left
  labelIconPosition?: 'left' | 'right'; // default: left
  rounded?: boolean;
  labelIcon?: React.ReactNode;
  icon?: React.ReactNode;
  children: React.ReactNode;
};

Basic Example

In this example, I will use all of the exported members of Dropdown:

import React from 'react';
import { Dropdown } from '@flmnh-mgcl/ui';

function MyComponent() {
  return (
    <Dropdown label="Menu" labelIcon={<MyIcon />}>
      <Dropdown.Header text="Options" />
      <Dropdown.Section>
        <Dropdown.Item text="Option 1" onClick={() => navigate('place-1')} />
        <Dropdown.Item text="Option 2" onClick={() => navigate('place-2')} />
      </Dropdown.Section>
      <Dropdown.Section>
        <Dropdown.Item text="Option 3" onClick={() => doSomething()} />
      </Dropdown.Section>
    </Dropdown>
  );
}

FocusTrap

FocusTrap is used internally by Modal. It will attempt to focus on the first focusable child element.

Available Props

export type FocusTrapProps = {
  children: NonNullable<React.ReactNode>;
  disabled?: boolean;
};

Basic Example

Here is a stripped example of how it is used in the Modal component:

import React from 'react';
import { FocusTrap } from '@flmnh-mgcl/ui';

function MyComponent() {
  return <FocusTrap> ... {children} ... </FocusTrap>;
}

Form

Form has multiple exported members. The default export is the Form component, itself, which internally uses react-hook-form. The exported members are the standard UI components (Select, Input, etc). but altered to be compatible with the Form component.

The available exported members are: Form.Input, Form.Area, Form.Select, Form.Radio and Form.Group

Available Props

The Props for Form, as well as some of the exported members, is as follows:

export type FormProps<T> = {
  onSubmit: SubmitHandler<T>;
  children: React.ReactNode;
  disabled?: boolean;
  defaultValues?: UnpackNestedValue<DeepPartial<T>>;
  mode?: 'onChange' | 'onBlur' | 'onSubmit' | 'onTouched' | 'all';
} & Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;

export type FormGroupProps = {
  children: React.ReactNode;
  flex?: boolean;
  hidden?: boolean;
};

The following Props are injected into the other UI elements of Form:

type InjectedProps<T extends {}> = Omit<T, 'ref' | 'errors' | 'error'> & {
  register?: ValidationRules; // => from react-hook-form
  assignOnChange?: (
    something: Record<string, string>
  ) => Record<string, string>;
};

Basic Example

import React from 'react';
import { Form, FormSubmitValues, SelectOption } from '@flmnh-mgcl/ui';

const options: SelectOption[] = [
  { label: 'Option 1', value: 1 },
  { label: 'Option 2', value: 2 },
  { label: 'Option 3', value: 3 },
];

function MyComponent() {
  function handleSubmit(values: FormSubmitValues) {
    console.log(values); // {fieldOne, fieldTwo, areaField}
  }

  return (
    <Form onSubmit={handleSubmit} id="myForm">
      <Form.Group flex>
        <Form.Input
          name="fieldOne"
          label="Field One"
          register={{ validate: (value: string) => value !== 'test' }}
          fullWidth
        />

        <Form.Select
          name="fieldTwo"
          label="Field Two"
          options={options}
          fullWidth
        />
      </Form.Group>

      <Form.Group flex>
        <Form.Area
          name="areaField"
          label="My Area Field"
          placeholder="Type something!"
          rows={4}
        />
      </Form.Group>

      <Button type="submit">Submit!</Button>
    </Form>
  );
}

Heading

Heading is just an h-tag with some preconfigured styles and options.

Available Props

export type HeadingProps = {
  tag?: keyof typeof HEADINGS;
  size?: keyof typeof TEXT_SIZES;
  className?: string;
  centered?: boolean;
  children: React.ReactNode;
};

Basic Example

import React from 'react';
import { Heading } from '@flmnh-mgcl/ui';

function MyComponent() {
  return <Heading>My Heading!</Heading>;
}

Input

Input is a standard input with additional props.

Available Props

export type InputProps = {
  label?: string;
  fullWidth?: boolean;
  slim?: boolean;
  icon?: keyof typeof INPUT_ICONS; // password, passwordVisible, user, atMention, search
  iconClick?(): void;
} & React.ComponentProps<'input'>;

Basic Example

I would normally use the Form variants for this example, but for demonstrative purposes I will not

import React, { useState } from 'react';
import { Input } from '@flmnh-mgcl/ui';

function MyComponent() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [visible, setVisible] = useState(false);

  function togglePasswordVisibility() {
    if (visible) setVisible(false);
    else setVisible(true);
  }

  return (
    <div className="flex flex-col space-y-3">
      <Input
        label="Username"
        icon="user"
        type="text"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        fullWidth
      />
      <Input
        label="Password"
        icon={visible ? 'passwordVisible' : 'password'}
        iconClick={togglePasswordVisibility}
        value={password}
        type={visible ? 'text' : 'password'}
        onChange={(e) => setPassword(e.target.value)}
        fullWidth
      />
    </div>
  );
}

Label

Label is the same label used in the other UI components that have one internally (Select, Input, etc).

Available Props

export type LabelProps = {
  fullWidth?: boolean;
} & React.ComponentProps<'label'>;

Basic Example

import React from 'react';
import { Label } from '@flmnh-mgcl/ui';

function MyComponent() {
  return (
    <div>
      <Label className="py-2">My Label</Label>
      <p>....</p>
    </div>
  );
}

Modal

Modal is a pop-up modal, utilizing a Portal and Focus Trap internally. It is animated using framer-motion.

You will need to use all exported members to get the styling of the modal correct: Modal.Footer and Modal.Content

Available Props

export type ModalContentProps = {
  title: string | React.ReactNode;
  children: React.ReactNode;
};

export type ModalFooterProps = { children: React.ReactNode };

export type ModalProps = {
  open: boolean;
  size?: keyof typeof MODAL_SIZES;
  onClose(): void;
  children: React.ReactNode;
};

Basic Example

import React, { useState } from 'react';
import { Button, Modal } from '@flmnh-mgcl/ui';

function MyComponent() {
  const [visible, setVisible] = useState(false);

  function on() {
    setVisible(true);
  }

  function off() {
    setVisible(false);
  }

  return (
    <React.Fragment>
      <Modal open={visible} onClose={off}>
        <Modal.Content title="My Modal">This is my basic modal</Modal.Content>
        <Modal.Footer>
          <Button onClick={off}>Okay!</Button>
        </Modal.Footer>
      </Modal>

      <Button onClick={on}>Toggle Modal</Button>
    </React.Fragment>
  );
}

Notification

Notification is a component designed to work with react-notification-system, however it is generalized enough that I included it in the component library. In order to use it as intended, please review the documentation for react-notification-system, however.

Available Props

export type NotificationProps = {
  title: string;
  message: string;
  level: 'error' | 'success' | 'warning' | 'info';
};

Basic Example

import React from 'react';
import { Notification } from '@flmnh-mgcl/ui';

function MyComponent() {
  return (
    <Notification
      title="Error Occurred"
      message="Please review the appropriate logs"
      level="error"
    />
  );
}

Portal

Portal is just a simple component to render a react-dom portal. You may use it as you would normally (see this for information about portals).

Available Props

export type PortalProps = { children: React.ReactNode };

Basic Example

import React from 'react';
import { Portal } from '@flmnh-mgcl/ui';

function MyComponent() {
  return (
    <Portal>
      <div>... content ...</div>
    </Portal>
  );
}

Radio

Radio is a checkbox component. It may be controlled, or used with a Form via Form.Radio.

Available Props

export type RadioProps = {
  checked?: boolean;
  label?: string;
  stacked?: boolean; // flex-col
} & React.ComponentProps<'input'>;

Basic Example

import React, { useState } from 'react';
import { Radio } from '@flmnh-mgcl/ui';

function MyComponent() {
  const [checked, setChecked] = useState(false);

  function toggle() {
    if (checked) setChecked(false);
    else setChecked(true);
  }
  return (
    <div className="flex flex-col space-y-4">
      <Radio label="Radio 1" />
      <Radio label="Radio 2" stacked />
      <Radio label="Radio 2" checked={checked} onChange={toggle} />
    </div>
  );
}

Select

Select is a UI wrapper for a select html element. It is functional, however needs to be rewritten.

More thorough documentation will be written after this eventual rewrite.

Available Props

export type SelectProps = {
  slim?: boolean;
  searchable?: boolean;
  label?: string;
  fullWidth?: boolean;
  options: SelectOption[];
  updateControlled?(newVal: any): void;
} & React.ComponentProps<'select'>;

Basic Example

import React from 'react';
import { Select, SelectOption } from '@flmnh-mgcl/ui';

const options: SelectOption[] = [
  { label: 'Option 1', value: 1 },
  { label: 'Option 2', value: 2 },
  { label: 'Option 3', value: 3 },
];

function MyComponent() {
  const [selected, setSelected] = useState();

  return (
    <React.Fragment>
      <Form.Select
        name="select"
        options={options}
        value={selected}
        label="Select an option"
        updateControlled={(newVal: any) => {
          setSelected(newVal);
        }}
      />
    </React.Fragment>
  );
}

Spinner

Spinner is a loading component, and may be used inline or absolute.

Available Props

export type SpinnerProps = {
  active?: boolean;
  size?: keyof typeof SPINNER_SIZES; // default: md
  inline?: boolean;
  color?: 'gray' | 'white'; // default: gray
};

Basic Example

import React from 'react';
import PageContainer from './components/PageContainer'
import { Spinner } from '@flmnh-mgcl/ui';

type Props = {...}

function MyComponent({ children, loading }: Props) {
  return (
    <PageContainer>
      <Spinner active={loading} size="lg" />
      {children}
    </PageContainer>
  );
}

Statistic

Statistic is a basic component for rendering number / string pairs. For now, it only supports one configuration: numerical statistic value on top, statistic label underneath.

Available Props

export type StatisticProps = {
  value?: number;
  percent?: boolean;
  unit: string;
};

Basic Example

import React from 'react';
import { Statistic } from '@flmnh-mgcl/ui';

function MyComponent() {
  return <Statistic value={102} unit="people" />;
}

Steps

Steps utilizes a typed wrapper for react-step-progress-bar, however it is used internally in the Steps component and therefore has separate props. I use it for multi-step forms mostly, however it may be used for other purposes.

Available Props

export type StepsProps = {
  steps: number;
  current: number;
};

Basic Example

import React, { useState } from 'react';
import { Steps, Button } from '@flmnh-mgcl/ui';

function MyComponent({ pages }: Props) {
  const [page, setPage] = useState(0);

  function paginateForward() {
    // ....
  }

  function paginateBackward() {
    // ....
  }

  function renderPage() {
    // ...
  }

  return (
    <div>
      <Steps steps={pages.length} current={page} />

      {renderPage()}

      <div>
        <Button.Group>
          <Button onClick={paginateBackward}>Back</Button>
          <Button onClick={paginateForward}>Continue</Button>
        </Button.Group>
      </div>
    </div>
  );
}

Switch

Switch component is a toggle slider, it is styled after TailwindUI.

Available Props

export type SwitchProps = {
  enabled: boolean;
  onToggle(): void;
};

Basic Example

import React, { useState } from 'react';
import { Switch, Label } from '@flmnh-mgcl/ui';

function MyComponent() {
  const [theme, setTheme] = useState(localStorage.theme ?? 'dark');

  function toggle() {
    // ...
  }

  return (
    <div className="flex space-x-2 items-center">
      <Label>Light Theme</Label>
      <Switch enabled={theme === 'dark'} onToggle={toggle} />
      <Label>Dark Theme</Label>
    </div>
  );
}

Table

Table is still currently incomplete. As of now, it can render a simple, non-virtualized table with static headers. It will support more dynamic tables and table features in the future.

Available Props

the type of TableProps is the union of TableProps from react-virtualized, with height and width omitted, and the following definition following the '&':

export type TableProps = Omit<VTableProps, 'width' | 'height'> & {
  basic?: boolean;
  data: Object[];
  activeIndex?: number;
  headers: string[];
  loading?: boolean;
  sortable?: boolean;
  containerClassname?: string;
};

width and height are omitted because Table uses an autosizer internally for the virtualized variant.

Basic Example

As the virtualized table is not quite ready, only the basic table will be used in the example below.

import React from 'react';
import { Table } from '@flmnh-mgcl/ui';

function MyComponent() {
  // BASIC TABLE
  return (
    <Table
      basic
      data={[
        {
          1: 'value',
          2: 'value',
        },
        {
          1: 'value2',
          2: 'value2',
        },
        {
          1: 'value3',
          2: 'value3',
        },
      ]}
      headers={['1', '2']}
    />
  );
}

Tabs

Tabs is a tab menu component. Currently, it does not support linked tabs (i.e. router basic tab menu), however this is something that is planned.

Available Props

export type TabProps = {
  text: string;
  onClick(): void;
  fullWidth?: boolean;
  active?: boolean;
};

Basic Example

Setting the tab on change is handled internally, all it needs is the state dispatch function:

import React, { useState } from 'react';
import { Tabs } from '@flmnh-mgcl/ui';

function SettingsPage() {
  const [tab, setTab] = useState(0);

  function renderTab() {
    // ...
  }

  return (
    <div>
      <Tabs
        fullWidth
        tabs={['General', 'Profile']}
        selectedIndex={tab}
        onChange={setTab}
      />

      <div>{renderTab()}</div>
    </div>
  );
}

Text

Text is a pre-styled p-tag component. It does support onClick events, and has the following additional props:

Available Props

export type TextProps = {
  variant?: keyof typeof TEXT;
  size?: 'sm' | 'md' | 'lg' | 'xl';
  centered?: boolean;
  onClick?(): void;
} & PropsOf<'p'>;

Basic Example

import React from 'react';
import { Text } from '@flmnh-mgcl/ui';

function MyComponent() {
  return <Text size="sm">This is my paragraph</Text>;
}

TextArea

TextArea is a standard textarea with additional props.

Available Props

export type TextAreaProps = {
  label?: string;
  fullWidth?: boolean;
} & React.ComponentProps<'textarea'>;

Basic Example

import React, { useState } from 'react';
import { TextArea } from '@flmnh-mgcl/ui';

function MyComponent() {
  const [value, setValue] = useState('');

  return (
    <TextArea
      name="myValue"
      label="Value"
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
}