Skip to content

Commit

Permalink
feat(alert): ✨ add alert component (#17)
Browse files Browse the repository at this point in the history
* feat(alert): ✨  add alert icons & boilerplate

* feat(alert): ✨  finish alert first draft
  • Loading branch information
navin-moorthy authored Dec 23, 2020
1 parent 64876a7 commit 51c9b26
Show file tree
Hide file tree
Showing 13 changed files with 353 additions and 2 deletions.
143 changes: 143 additions & 0 deletions src/alert/Alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import * as React from "react";
import { Role, RoleProps } from "reakit";

import {
BoltIcon,
CheckCircleIcon,
ExclamationCircleIcon,
ExclamationTriangleIcon,
InfoCircleIcon,
} from "../icons";
import theme from "../theme";
import { Button, ButtonProps } from "../button";
import { createContext, ocx } from "../utils";

import { forwardRefWithAs, PropsWithAs } from "../utils/types";

const STATUSICONS = {
info: InfoCircleIcon,
success: CheckCircleIcon,
warning: ExclamationTriangleIcon,
error: ExclamationCircleIcon,
offline: BoltIcon,
};

export type AlertStatus = "success" | "warning" | "error" | "info" | "offline";

type AlertContext = {
status: AlertStatus;
};

const [AlertProvider, useAlertContext] = createContext<AlertContext>({
name: "AlertContext",
errorMessage:
"useAlertContext: `context` is undefined. Seems you forgot to wrap alert components in `<Alert />`",
});

export type AlertProps = RoleProps & {
/**
* The status of the alert
*/
status?: AlertStatus;
};

function AlertComponent(
props: PropsWithAs<AlertProps, "div">,
ref: React.Ref<HTMLDivElement>,
) {
const { status = "info", className, ...rest } = props;
const alertStyles = ocx(
theme.alert.base,
theme.alert.status[status].base,
className,
);

return (
<AlertProvider value={{ status }}>
<Role role="alert" className={alertStyles} ref={ref} {...rest} />
</AlertProvider>
);
}

export const Alert = forwardRefWithAs<AlertProps, "div">(AlertComponent);

export type AlertTitleProps = RoleProps & {};

function AlertTitleComponent(
props: PropsWithAs<AlertTitleProps, "div">,
ref: React.Ref<HTMLDivElement>,
) {
const { className, ...rest } = props;
const alertTitleStyles = ocx(theme.alert.title, className);

return <Role className={alertTitleStyles} ref={ref} {...rest} />;
}

export const AlertTitle = forwardRefWithAs<AlertTitleProps, "div">(
AlertTitleComponent,
);

export type AlertDescriptionProps = RoleProps & {};

function AlertDescriptionComponent(
props: PropsWithAs<AlertDescriptionProps, "div">,
ref: React.Ref<HTMLDivElement>,
) {
const { className, ...rest } = props;
const alertDescriptionStyles = ocx(theme.alert.description, className);

return <Role className={alertDescriptionStyles} ref={ref} {...rest} />;
}

export const AlertDescription = forwardRefWithAs<AlertDescriptionProps, "div">(
AlertDescriptionComponent,
);

export type AlertActionButtonProps = ButtonProps & {};

function AlertActionButtonComponent(
props: PropsWithAs<AlertActionButtonProps, "button">,
ref: React.Ref<HTMLButtonElement>,
) {
const { status } = useAlertContext();
const { className, ...rest } = props;
const alertActionButtonStyles = ocx(
theme.alert.actionButton,
theme.alert.status[status].actionButton,
className,
);

return <Button className={alertActionButtonStyles} ref={ref} {...rest} />;
}

export const AlertActionButton = forwardRefWithAs<
AlertActionButtonProps,
"button"
>(AlertActionButtonComponent);

export type AlertIconProps = RoleProps & {};

function AlertIconComponent(
props: PropsWithAs<AlertIconProps, "span">,
ref: React.Ref<HTMLSpanElement>,
) {
const { status } = useAlertContext();
const { className, ...rest } = props;
const Icon = STATUSICONS[status];
const alertIconBaseStyles = ocx(theme.alert.icon.base, className);
const alertIconIconsStyles = ocx(
theme.alert.icon.base,
theme.alert.status[status].icon,
className,
);

return (
<Role as="span" className={alertIconBaseStyles} ref={ref} {...rest}>
<Icon className={alertIconIconsStyles} />
</Role>
);
}

export const AlertIcon = forwardRefWithAs<AlertIconProps, "span">(
AlertIconComponent,
);
1 change: 1 addition & 0 deletions src/alert/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./Alert";
5 changes: 5 additions & 0 deletions src/alert/stories/Alert.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import "tailwindcss/base";

@import "tailwindcss/components";

@import "tailwindcss/utilities";
62 changes: 62 additions & 0 deletions src/alert/stories/Alert.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from "react";
// also exported from '@storybook/react' if you can deal with breaking changes in 6.1
import { Story, Meta } from "@storybook/react/types-6-0";

import "./Alert.css";
import {
Alert,
AlertTitle,
AlertDescription,
AlertIcon,
AlertProps,
} from "../index";
import { Button, ButtonIcon } from "../../button";
import { CrossIcon } from "../../icons";
import { ocx } from "../../utils";
import { AlertActionButton } from "../Alert";
import { Box } from "../../box";

export default {
title: "Alert",
component: Alert,
} as Meta;

const Base: Story<AlertProps> = args => {
const status = args?.status || "info";
const buttonBg = {
info: "bg-blue-200",
success: "bg-green-200",
warning: "bg-orange-200",
error: "bg-red-200",
offline: "bg-purple-200",
};

return (
<Alert {...args} className="flex justify-between">
<Box className="items-center inherit">
<AlertIcon />
<AlertTitle>
We’re here to help you through these tough times.
</AlertTitle>
<AlertDescription>Your experience may be degrated</AlertDescription>
</Box>
<Box className="items-center inherit">
<AlertActionButton>Reach Out</AlertActionButton>
<Button
aria-label="close"
className={ocx(
"h-5 px-0 bg-transparent min-w-5 ml-2",
`hover:${buttonBg[status]}`,
)}
>
<ButtonIcon className="text-gray-800 inherit">
<CrossIcon />
</ButtonIcon>
</Button>
</Box>
</Alert>
);
};

export const Default = Base.bind({});
Default.args = { status: "info" };
10 changes: 10 additions & 0 deletions src/icon/stories/Icon.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import { FaBeer, FaCog } from "react-icons/fa";

import "./icon.css";
import {
BoltIcon,
ClockIcon,
CrossIcon,
WheelIcon,
PhotographIcon,
InfoCircleIcon,
CheckCircleIcon,
ArrowNarrowRightIcon,
ExclamationCircleIcon,
ExclamationTriangleIcon,
} from "../../icons";
import { Icon } from "../index";

Expand Down Expand Up @@ -37,3 +42,8 @@ export const Wheel = () => <WheelIcon as={FaCog} />;
export const ReactIcons = () => (
<Icon as={FaBeer} className="w-8 h-8 text-red-500" />
);
export const Bolt = () => <BoltIcon />;
export const CheckCircle = () => <CheckCircleIcon />;
export const ExclamationCircle = () => <ExclamationCircleIcon />;
export const ExclamationTriangle = () => <ExclamationTriangleIcon />;
export const InfoCircle = () => <InfoCircleIcon />;
15 changes: 15 additions & 0 deletions src/icons/Bolt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from "react";
import { createIcon } from "../icon";

export const BoltIcon = createIcon({
displayName: "BoltIcon",
viewBox: "0 0 16 16",
path: (
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M9.114.04c.174.057.326.17.434.322a.911.911 0 01.166.526v4.445h3.428c.157 0 .311.044.445.128a.877.877 0 01.316.35.916.916 0 01-.058.92l-6 8.889a.861.861 0 01-.426.333.829.829 0 01-.533.006.859.859 0 01-.434-.322.911.911 0 01-.166-.527v-4.444H2.857a.835.835 0 01-.444-.129.877.877 0 01-.316-.349.917.917 0 01.058-.92l6-8.889A.86.86 0 018.58.047a.828.828 0 01.533-.006z"
/>
),
});
15 changes: 15 additions & 0 deletions src/icons/CheckCircle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from "react";
import { createIcon } from "../icon";

export const CheckCircleIcon = createIcon({
displayName: "CheckCircleIcon",
viewBox: "0 0 16 16",
path: (
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M8 16A8 8 0 108-.001 8 8 0 008 16zm3.707-9.293a1 1 0 00-1.414-1.414L7 8.586 5.707 7.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
/>
),
});
15 changes: 15 additions & 0 deletions src/icons/ExclamationCircle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from "react";
import { createIcon } from "../icon";

export const ExclamationCircleIcon = createIcon({
displayName: "ExclamationCircleIcon",
viewBox: "0 0 16 16",
path: (
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M16 8A8 8 0 11-.001 8 8 8 0 0116 8zm-7 4a1 1 0 11-2 0 1 1 0 012 0zM8 3a1 1 0 00-1 1v4a1 1 0 002 0V4a1 1 0 00-1-1z"
/>
),
});
15 changes: 15 additions & 0 deletions src/icons/ExclamationTriangle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from "react";
import { createIcon } from "../icon";

export const ExclamationTriangleIcon = createIcon({
displayName: "ExclamationTriangleIcon",
viewBox: "0 0 16 16",
path: (
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M6.161 1.1a2.088 2.088 0 013.678 0l5.887 10.689C16.517 13.226 15.5 15 13.888 15H2.113C.499 15-.517 13.226.274 11.789l5.887-10.69zm2.894 10.668c0 .286-.111.56-.309.762a1.044 1.044 0 01-.746.316c-.28 0-.548-.114-.746-.316a1.09 1.09 0 010-1.524 1.044 1.044 0 011.492 0c.198.202.309.476.309.762zM8 3.148c-.28 0-.548.113-.746.315a1.09 1.09 0 00-.309.762v3.233c0 .286.111.56.309.762.198.202.466.316.746.316.28 0 .548-.114.746-.316a1.09 1.09 0 00.309-.762V4.225c0-.286-.111-.56-.309-.762A1.044 1.044 0 008 3.148z"
/>
),
});
15 changes: 15 additions & 0 deletions src/icons/InfoCircle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from "react";
import { createIcon } from "../icon";

export const InfoCircleIcon = createIcon({
displayName: "InfoCircleIcon",
viewBox: "0 0 16 16",
path: (
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M16 8A8 8 0 11-.001 8 8 8 0 0116 8zM9 4a1 1 0 11-2 0 1 1 0 012 0zM7 7a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 000-2V8a1 1 0 00-1-1H7z"
/>
),
});
5 changes: 5 additions & 0 deletions src/icons/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export * from "./ArrowNarrowRight";
export * from "./Bolt";
export * from "./CheckCircle";
export * from "./Clock";
export * from "./Cross";
export * from "./ExclamationCircle";
export * from "./ExclamationTriangle";
export * from "./InfoCircle";
export * from "./Photograph";
export * from "./Wheel";
export * from "./GenericAvatar";
41 changes: 39 additions & 2 deletions src/theme.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,46 @@
const theme = {
alert: {
base: "font-sans flex items-center w-full overflow-hidden p-4 bg-blue-50",
title: "text-gray-800 text-sm leading-4 font-semibold mr-2",
description: "inline text-gray-600 text-sm leading-4",
icon: {
base: "inherit flex-shrink-0 mr-3 w-5 h-5",
icons: "w-full h-full",
},
actionButton: "h-5 bg-transparent px-0",
status: {
info: {
base: "bg-blue-50",
icon: "text-blue-400",
actionButton: "text-blue-500",
},
success: {
base: "bg-green-50",
icon: "text-green-400",
actionButton: "text-green-500",
},
warning: {
base: "bg-orange-50",
icon: "text-orange-400",
actionButton: "text-orange-500",
},
error: {
base: "bg-red-50",
icon: "text-red-400",
actionButton: "text-red-500",
},
offline: {
base: "bg-purple-50",
icon: "text-purple-400",
actionButton: "text-purple-500",
},
},
},
button: {
base:
"font-sans font-semibold text-white inline-flex items-center justify-center appearance-none rounded-md transition-all relative whitespace-nowrap align-middle outline-none w-auto select-none disabled:cursor-not-allowed disabled:opacity-40",
prefix: "flex mr-2",
suffix: "flex ml-2",
prefix: "inherit mr-2",
suffix: "inherit ml-2",
spinner: "w-em h-em text-current",
group: "focus:z-1",
span: "inline-block cursor-not-allowed",
Expand Down
Loading

1 comment on commit 51c9b26

@vercel
Copy link

@vercel vercel bot commented on 51c9b26 Dec 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.