@flmnh-mgcl/ui v1.0.27
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:
- Accordion
- Badge
- Button
- Button.Group
- Checkmark
- Code
- Datepicker
- Divider
- Dropdown
- FocusTrap
- Form
- Heading
- Input
- Label
- Modal
- Notification
- Portal
- Radio
- Select
- Spinner
- Statistic
- Steps
- Switch
- Table
- Tabs
- Text
- TextArea
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)}
/>
);
}
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago