diff --git a/.gitignore b/.gitignore index 2bd81678..d26a2c97 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ tsconfig.tsbuildinfo # Other modules /*-????????.js* /AbstractTooltip +/Alert /Button /colors /ConfirmationTooltip diff --git a/src/AlertCard/AlertCard.story.mdx b/src/AlertCard/AlertCard.story.mdx new file mode 100644 index 00000000..7c06628a --- /dev/null +++ b/src/AlertCard/AlertCard.story.mdx @@ -0,0 +1,188 @@ +import { AlertCard } from "../AlertCard"; +import { Button } from "../Button"; +import { + Description, + Meta, + Story, + Props, + Preview, +} from "@storybook/addon-docs/blocks"; + + + +# AlertCard + + + +## Info Alert + +Informational alerts present general info about something you are doing or working with. It is neutral and conveys no implications of success or failure. + + + + undefined} + type="info" + heading="Informational alert" + > + This is a general informational message telling you about something. Learn + more + + + + undefined} + type="info" + theme="dark" + heading="Informational alert" + > + This is a general informational message telling you about something. Learn + more + + + + +### Extended + +The extended card variant is for rare use cases where an alert is the most appropriate format for explaining longer instructions. + + + + undefined} + type="info" + heading="Informational alert" + extended={true} + > + In some cases it may be appropriate for the toast card to give a longer + message, or a set of instructions helping users navigate complicated + situations in the workflow. +
+
+ It is typically preferrable to link out to a docs article in situations like + this, but there are cases where you may also need to include a lengthy amount + of content with the card because it will be immediately useful to the user + in their workflow. +
+
+ If a toast card’s content requires more than one paragraph, use the + expanded content toast variant. +
+
+ + undefined} + type="info" + theme="dark" + heading="Informational alert" + extended={true} + > + In some cases it may be appropriate for the toast card to give a longer + message, or a set of instructions helping users navigate complicated + situations in the workflow. +
+
+ It is typically preferrable to link out to a docs article in situations like + this, but there are cases where you may also need to include a lengthy amount + of content with the card because it will be immediately useful to the user + in their workflow. +
+
+ If a toast card’s content requires more than one paragraph, use the + expanded content toast variant. +
+
+
+ +## Warning Alert + +Warning alerts present information about the action that is about to be taken that alerts the user to consequences of the action and asks for confirmation that the action still be taken in light of the consequences. + + + + undefined} + type="warn" + heading="Warning alert" + actions={} + > + exceededSubLimitAt is a deprecated field. You can use it but it may not + work as expected. + + + + undefined} + type="warn" + heading="Warning alert" + theme="dark" + actions={ + + } + > + exceededSubLimitAt is a deprecated field. You can use it but it may not + work as expected. + + + + +## Error Alert + +Error alerts let the user know that something has gone wrong or is blocked in the given workflow they are trying to complete. When possible, error alerts should contain helpful information about what has happened to help unblock the user (such as steps to resolve or a link to a docs article). + + + + undefined} + type="error" + heading="Error alert" + style={{ marginBottom: 20 }} + > + Something is broken. You cannot perform this action at this time. + + + + undefined} + type="error" + heading="Error alert" + theme="dark" + style={{ marginBottom: 20 }} + > + Something is broken. You cannot perform this action at this time. + + + + +## Success Alert + +Success alerts are confirmation that the action the user was trying to take has succeeded. + + + + undefined} type="success" heading="Success alert"> + A new schema registry has been created by timbotnik. + + + + undefined} + type="success" + theme="dark" + heading="Success alert" + > + A new schema registry has been created by timbotnik. + + + + +## Props + +### `Alert` + + diff --git a/src/AlertCard/AlertCard.tsx b/src/AlertCard/AlertCard.tsx new file mode 100644 index 00000000..cc999f5d --- /dev/null +++ b/src/AlertCard/AlertCard.tsx @@ -0,0 +1,230 @@ +/** @jsx jsx */ +import { jsx, ClassNames } from "@emotion/core"; +import React, { CSSProperties, Fragment, useMemo } from "react"; +import PropTypes from "prop-types"; +import classnames from "classnames"; + +import { base } from "../typography"; +import { IconClose } from "../icons/IconClose"; +import { colors } from "../colors"; +import { assertUnreachable } from "../shared/assertUnreachable"; +import { IconInfoSolid } from "../icons/IconInfoSolid"; +import { IconWarningSolid } from "../icons/IconWarningSolid"; +import { IconErrorSolid } from "../icons/IconErrorSolid"; +import { IconSuccessSolid } from "../icons/IconSuccessSolid"; + +interface AlertCardProps { + /** + * color theme for alert + * @default "light" + */ + theme?: "light" | "dark"; + + heading: React.ReactNode; + + /** + * actions could be a button + * or a tooltip or anything the Alert should display after the children + */ + actions?: React.ReactNode; + + /** + * The content of the card, appears below the title + */ + children?: React.ReactNode; + + /** + * Override how the `header` is rendered. You can pass either an intrinisic + * jsx element as a string (like "h1") or a react element (`

`) + * + * If you pass a react element, props that we add are spread onto the input. + * + * @default "h2" + */ + headingAs?: React.ReactElement | keyof JSX.IntrinsicElements; + + /** + * callback for handling the clicks on the close button. + */ + onClose: () => void; + + /** + * layout for longer content + * + * @default false + */ + extended?: boolean; + + className?: string; + style?: CSSProperties; + + /** + * Type of alert, this is used to determine the color and icon in the title + */ + type: "info" | "warn" | "error" | "success"; +} + +export const AlertCard: React.FC = ({ + heading, + onClose, + actions, + headingAs = "h2", + children, + theme = "light", + extended = false, + type, + ...otherProps +}) => { + const { Icon, color: colorTemp } = useMemo(() => { + switch (type) { + case "info": + return { color: colors.blue, Icon: IconInfoSolid }; + case "warn": + return { color: colors.orange, Icon: IconWarningSolid }; + case "error": + return { color: colors.red, Icon: IconErrorSolid }; + case "success": + return { color: colors.green, Icon: IconSuccessSolid }; + default: + assertUnreachable(type); + } + }, [type]); + return ( +
+
+ + {({ css, cx }) => { + const headingProps = { + className: cx( + css({ + fontWeight: 600, + marginBottom: 0, + marginTop: 0, + width: "100%", + display: "flex", + color: + theme === "light" + ? colorTemp.darker + : theme === "dark" + ? colorTemp.lighter + : assertUnreachable(theme), + ...base.base, + }) + ), + children: ( + + + {heading} + + ), + }; + + return React.isValidElement(headingAs) + ? React.cloneElement(headingAs, { + ...headingProps, + className: classnames( + headingProps.className, + headingAs.props.className + ), + }) + : React.createElement(headingAs, headingProps); + }} + + +
+ + {extended && ( +
+ )} +
+
+ {children} +
+ + {actions} +
+
+ ); +}; + +AlertCard.propTypes = { + extended: PropTypes.bool, + onClose: PropTypes.func.isRequired, + children: PropTypes.node, + heading: PropTypes.node.isRequired, + actions: PropTypes.node, + type: PropTypes.oneOf(["info", "warn", "error", "success"] as const) + .isRequired, + headingAs: PropTypes.oneOfType([ + PropTypes.element.isRequired, + PropTypes.string.isRequired as any, // Using PropTypes.string to match keyof JSX.IntrinsicElements + ]), +}; diff --git a/src/AlertCard/index.ts b/src/AlertCard/index.ts new file mode 100644 index 00000000..2029fbb9 --- /dev/null +++ b/src/AlertCard/index.ts @@ -0,0 +1 @@ +export * from "./AlertCard"; diff --git a/src/Button/Button.tsx b/src/Button/Button.tsx index 9da18896..a1afba28 100644 --- a/src/Button/Button.tsx +++ b/src/Button/Button.tsx @@ -7,14 +7,10 @@ import tinycolor from "tinycolor2"; import React from "react"; import classnames from "classnames"; import { LoadingSpinner } from "../Loaders"; +import { assertUnreachable } from "../shared/assertUnreachable"; type TLength = string | 0 | number; -/* istanbul ignore next */ -function assertUnreachable(value: never): never { - throw new TypeError(`Unreachable value reached ${value}`); -} - /** * Save a default color so we can check if we used the default or not. The * default color has a few special properties. diff --git a/src/MenuItem/index.tsx b/src/MenuItem/index.tsx index 7c8c5b1f..b20dc4ac 100644 --- a/src/MenuItem/index.tsx +++ b/src/MenuItem/index.tsx @@ -5,14 +5,10 @@ import React from "react"; import { css, jsx } from "@emotion/core"; import { useMenuIconSize, useMenuColor } from "../MenuConfig"; import { useMenuItemClickListener } from "../MenuItemClickListener"; +import { assertUnreachable } from "../shared/assertUnreachable"; import tinycolor from "tinycolor2"; import { colors } from "../colors"; -/* istanbul ignore next */ -function assertUnreachable(value: never): never { - throw new TypeError(`Unreachable value reached ${value}`); -} - function getIconHorizontalPadding( iconSize: ReturnType ): CSS.PaddingProperty { diff --git a/src/Modal/Modal.tsx b/src/Modal/Modal.tsx index f0b4dc5e..5ee5199c 100644 --- a/src/Modal/Modal.tsx +++ b/src/Modal/Modal.tsx @@ -8,6 +8,7 @@ import { colors } from "../colors"; import * as CSS from "csstype"; import classnames from "classnames"; import { useSpaceKitProvider } from "../SpaceKitProvider"; +import { assertUnreachable } from "../shared/assertUnreachable"; interface Props { /** @@ -103,11 +104,6 @@ const modalBackdrop = css` } `; -/* istanbul ignore next */ -function assertUnreachable(value: never): never { - throw new TypeError(`Unreachable value reached ${value}`); -} - type TLength = string | 0 | number; function getModalWidth(size: Props["size"]): CSS.WidthProperty { diff --git a/src/icons/svgs/solids/icon-error-solid.svg b/src/icons/svgs/solids/icon-error-solid.svg index 3393324a..43e02947 100644 --- a/src/icons/svgs/solids/icon-error-solid.svg +++ b/src/icons/svgs/solids/icon-error-solid.svg @@ -1,6 +1,4 @@ - - - - - + + + \ No newline at end of file diff --git a/src/icons/svgs/solids/icon-info-solid-sl.svg b/src/icons/svgs/solids/icon-info-solid-sl.svg index 171cee31..496e1d0c 100644 --- a/src/icons/svgs/solids/icon-info-solid-sl.svg +++ b/src/icons/svgs/solids/icon-info-solid-sl.svg @@ -1 +1,4 @@ -Exported from Streamline App (https://app.streamlineicons.com) \ No newline at end of file + + + + \ No newline at end of file diff --git a/src/icons/svgs/solids/icon-success-solid-sl.svg b/src/icons/svgs/solids/icon-success-solid-sl.svg index c75d01cb..d24e0b80 100644 --- a/src/icons/svgs/solids/icon-success-solid-sl.svg +++ b/src/icons/svgs/solids/icon-success-solid-sl.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/src/shared/assertUnreachable.ts b/src/shared/assertUnreachable.ts new file mode 100644 index 00000000..245b0eb3 --- /dev/null +++ b/src/shared/assertUnreachable.ts @@ -0,0 +1,5 @@ +// @see https://www.typescriptlang.org/docs/handbook/advanced-types.html#exhaustiveness-checking +/* istanbul ignore next */ +export function assertUnreachable(value: never): never { + throw new TypeError(`Unreachable value reached ${value}`); +}