From 8850dcbdb1f3f1486df4b36ea7251b0fc2c15072 Mon Sep 17 00:00:00 2001 From: Suleiman Mohammed Date: Fri, 15 Apr 2022 06:12:07 +0000 Subject: [PATCH 01/35] All component extracted --- components/.gitkeep | 1 - components/Alert/Alert.pcss | 213 ++++++++++++++++++ components/Alert/Alert.stories.tsx | 22 ++ components/Alert/index.tsx | 89 ++++++++ components/Checkbox/Checkbox.pcss | 13 ++ components/Checkbox/index.tsx | 26 +++ components/ColorPicker/ColorPicker.pcss | 41 ++++ components/ColorPicker/index.tsx | 83 +++++++ components/Dashicon/index.tsx | 284 ++++++++++++++++++++++++ components/FilteredListField/index.tsx | 48 ++++ components/Flex/index.tsx | 47 ++++ components/HelpTooltip/HelpTooltip.pcss | 60 +++++ components/HelpTooltip/index.tsx | 42 ++++ components/ListField/index.tsx | 128 +++++++++++ components/OrSeparator/OrSeperator.pcss | 14 ++ components/OrSeparator/index.tsx | 13 ++ components/RadioGroup/RadioGroup.pcss | 15 ++ components/RadioGroup/index.tsx | 43 ++++ components/Select/Select.scss | 13 ++ components/Select/index.tsx | 133 +++++++++++ components/Square/Square.pcss | 18 ++ components/Square/index.tsx | 15 ++ components/TextArea/index.tsx | 17 ++ components/TextField/index.tsx | 20 ++ components/Tooltip/Tooltip.pcss | 117 ++++++++++ components/Tooltip/index.tsx | 117 ++++++++++ hooks/.gitkeep | 1 - hooks/useDetectOutsideClick.ts | 29 +++ hooks/useDetectTabOut.ts | 20 ++ hooks/useEventListener.ts | 73 ++++++ styles/.gitkeep | 0 styles/theme.pcss | 6 + utils/.gitkeep | 1 - utils/classes.ts | 3 + utils/colorToString.ts | 15 ++ utils/mergeRefs.ts | 13 ++ 36 files changed, 1790 insertions(+), 3 deletions(-) delete mode 100644 components/.gitkeep create mode 100644 components/Alert/Alert.pcss create mode 100644 components/Alert/Alert.stories.tsx create mode 100644 components/Alert/index.tsx create mode 100644 components/Checkbox/Checkbox.pcss create mode 100644 components/Checkbox/index.tsx create mode 100644 components/ColorPicker/ColorPicker.pcss create mode 100644 components/ColorPicker/index.tsx create mode 100644 components/Dashicon/index.tsx create mode 100644 components/FilteredListField/index.tsx create mode 100644 components/Flex/index.tsx create mode 100644 components/HelpTooltip/HelpTooltip.pcss create mode 100644 components/HelpTooltip/index.tsx create mode 100644 components/ListField/index.tsx create mode 100644 components/OrSeparator/OrSeperator.pcss create mode 100644 components/OrSeparator/index.tsx create mode 100644 components/RadioGroup/RadioGroup.pcss create mode 100644 components/RadioGroup/index.tsx create mode 100644 components/Select/Select.scss create mode 100644 components/Select/index.tsx create mode 100644 components/Square/Square.pcss create mode 100644 components/Square/index.tsx create mode 100644 components/TextArea/index.tsx create mode 100644 components/TextField/index.tsx create mode 100644 components/Tooltip/Tooltip.pcss create mode 100644 components/Tooltip/index.tsx delete mode 100644 hooks/.gitkeep create mode 100644 hooks/useDetectOutsideClick.ts create mode 100644 hooks/useDetectTabOut.ts create mode 100644 hooks/useEventListener.ts delete mode 100644 styles/.gitkeep create mode 100644 styles/theme.pcss delete mode 100644 utils/.gitkeep create mode 100644 utils/classes.ts create mode 100644 utils/colorToString.ts create mode 100644 utils/mergeRefs.ts diff --git a/components/.gitkeep b/components/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/components/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/components/Alert/Alert.pcss b/components/Alert/Alert.pcss new file mode 100644 index 0000000..149412f --- /dev/null +++ b/components/Alert/Alert.pcss @@ -0,0 +1,213 @@ +.message { + align-items: stretch; + justify-content: center; + position: relative; + padding: 5px 7px; + line-height: 18px; + font-size: 13px; + background: #fff; + border: 1px solid; + border-radius: 3px; + + &:not(:first-child) { + margin-top: 10px; + } + + &:not(:last-child) { + margin-bottom: 10px; + } +} + +.centered { + padding: 5px 25px; +} + +.shaking { + animation-name: shake-animation; + animation-duration: 0.2s; +} + +.dashicon { + width: 16px !important; + height: 16px !important; + font-size: 16px !important; + line-height: 16px !important; + margin-top: 1px; +} + +.icon { + composes: dashicon; + margin-right: 8px; + flex-grow: 0; + flex-shrink: 0; +} + +.logo { + composes: icon; + + & img { + width: 18px; + height: 18px; + max-width: unset; + } +} + +.content { + display: block; +} + +.dismiss-btn { + position: absolute; + top: 5px; + right: 7px; + color: #333; + padding: 0 !important; + border: 0; + border-radius: 100px; + background: transparent; + box-sizing: border-box; + opacity: 0.5; + + &:hover { + opacity: 1; + } + + &:focus-visible { + box-shadow: 0 0 0 1px currentColor; + } + + & :global(.dashicon) { + transform: translateX(1px); + } +} + + +@keyframes shake-animation { + 0%, 20%, 40%, 60%, 80%, 100% { + transform: translateX(0); + } + 30%, 70% { + transform: translateX(-2px); + } + 10%, 50%, 90% { + transform: translateX(2px); + } +} + +/* + * COLOR VARIATIONS + *---------------------------- + */ + +/* SUCCESS */ +.success { + composes: message; + background: #ecf4eb; + box-shadow: 0 1px 2px rgba(37, 85, 31, 0.15); + border-color: rgba(61, 142, 52, 0.4); + + & .icon, & .dismiss-btn { + color: #2e6b27; + } + + & .content { + color: #122b10; + } +} + +/* INFO */ +.info { + composes: message; + background: #e8f5f7; + box-shadow: 0 1px 2px rgba(14, 91, 107, 0.15); + border-color: rgba(24, 152, 178, 0.4); + + & .icon, & .dismiss-btn { + color: #127286; + } + + & .content { + color: #072e35; + } +} + +/* WARNING */ +.warning { + composes: message; + background: #fff4e6; + box-shadow: 0 1px 2px rgba(153, 88, 0, 0.15); + border-color: rgba(255, 147, 0, 0.4); + + & .icon, & .dismiss-btn { + color: #bf6e00; + } + + & .content { + color: #4d2c00; + } +} + +/* PREMIUM */ +.premium { + composes: message; + background: rgba(221, 35, 75, .1); + border-color: var(--sli-pro); + + & .icon, & .dismiss-btn, & .content { + color: var(--sli-quasi-black); + } + + & a { + color: #000 !important; + font-weight: 600; + } +} + +/* PRO TIP */ +.pro-tip { + composes: message; + background: #eeeffa; + box-shadow: 0 1px 2px rgba(53, 57, 123, 0.15); + border-color: rgba(89, 95, 205, 0.4); + + & .icon, & .dismiss-btn { + color: #43479a; + } + + & .content { + color: #1b1d3e; + } +} + +/* ERROR */ +.error { + composes: message; + background: #fbe9ec; + box-shadow: 0 1px 2px rgba(130, 22, 40, 0.15); + border-color: rgba(216, 36, 66, 0.4); + + & .icon, & .dismiss-btn { + color: #a21b32; + } + + & .content { + color: #410b14; + } +} + + +/* GREY */ +.grey { + composes: message; + background: var(--sli-wp-grey); + box-shadow: 0 1px 2px rgba(120, 120, 120, 0.15); + border-color: var(--sli-line-color); + + & .icon, & .dismiss-btn { + color: #555; + } + + & .content { + color: #222; + } +} diff --git a/components/Alert/Alert.stories.tsx b/components/Alert/Alert.stories.tsx new file mode 100644 index 0000000..9577787 --- /dev/null +++ b/components/Alert/Alert.stories.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import { Alert } from './index'; + +export default { + title: 'Alert', + component: Alert, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: 'fullscreen', + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ; + +export const Warning = Template.bind({}); +Warning.args = { + type: "warning", + children:

lskdmskmdkms

+}; + diff --git a/components/Alert/index.tsx b/components/Alert/index.tsx new file mode 100644 index 0000000..80c7691 --- /dev/null +++ b/components/Alert/index.tsx @@ -0,0 +1,89 @@ +import React, { CSSProperties, ReactNode } from "react"; +import css from "./Alert.pcss"; +import { classList } from "../../utils/classes"; +import { Dashicon, DashiconTy } from "../Dashicon"; + +type AlertType = + | "success" + | "info" + | "pro-tip" + | "premium" + | "warning" + | "error" + | "grey"; + +export type Props = { + className?: string; + style?: CSSProperties; + children?: ReactNode; + type: AlertType; + showIcon?: boolean; + shake?: boolean; + centered?: boolean; + isDismissible?: boolean; + onDismiss?: () => void; +}; + +export function Alert({ + className, + style, + children, + type, + showIcon, + shake, + centered, + isDismissible, + onDismiss, +}: Props) { + const [dismissed, setDismissed] = React.useState(false); + + const handleClick = () => { + if (isDismissible) { + setDismissed(true); + onDismiss && onDismiss(); + } + }; + + const fullClassName = classList( + css[type], + shake && css.shaking, + centered && css.centered, + className + ); + + return dismissed ? null : ( +
+ {showIcon && ( + + )} + +
{children}
+ + {isDismissible && ( + + )} +
+ ); +} + +/** + * Retrieves the appropriate dashicon for a given message type. + * + * @param type + */ +function getIconFor(type: AlertType): DashiconTy { + switch (type) { + case "success": + return "yes-alt"; + case "pro-tip": + return "lightbulb"; + case "error": + case "warning": + return "warning"; + case "info": + default: + return "info"; + } +} diff --git a/components/Checkbox/Checkbox.pcss b/components/Checkbox/Checkbox.pcss new file mode 100644 index 0000000..cd6d52e --- /dev/null +++ b/components/Checkbox/Checkbox.pcss @@ -0,0 +1,13 @@ +.checkbox-field { + display: flex; + flex-direction: column; + justify-content: center; + min-height: 40px; + padding: 10px 0; + box-sizing: border-box; +} + +.aligner { + display: flex; + flex-direction: row; +} diff --git a/components/Checkbox/index.tsx b/components/Checkbox/index.tsx new file mode 100644 index 0000000..90458b6 --- /dev/null +++ b/components/Checkbox/index.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import {FlexRow, FlexColumn} from "../Flex" + +type Props = { + id?: string; + value?: boolean; + disabled?: boolean; + onChange?: (value: boolean) => void; +} + +export function Checkbox({id, value, onChange, disabled}: Props) { + return ( + + + onChange(e.target.checked)} + disabled={disabled} + /> + + + ); +} diff --git a/components/ColorPicker/ColorPicker.pcss b/components/ColorPicker/ColorPicker.pcss new file mode 100644 index 0000000..e875bd6 --- /dev/null +++ b/components/ColorPicker/ColorPicker.pcss @@ -0,0 +1,41 @@ +:root { + --sli-color-picker-padding: 5px; + --sli-color-picker-alpha-grid-size: 14px; + --sli-color-picker-inner-shadow: 0 0 0 5px #fff inset; +} + +.button { + position: relative; + padding: 7px 12px; + width: 100%; + height: 36px; + border: 1px solid var(--sli-line-color); + border-radius: 3px; + cursor: pointer; + + &, &:hover, &:focus-visible, &:active { + background-size: var(--sli-color-picker-alpha-grid-size); + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpSIVUYuoOGSoThZERRy1CkWoEGqFVh1MLv2CJg1Jiouj4Fpw8GOx6uDirKuDqyAIfoA4OTopukiJ/0sKLWI9OO7Hu3uPu3eAUC0yzWobBzTdNhOxqJhKr4qBVwjoRQ/6MSgzy5iTpDhajq97+Ph6F+FZrc/9ObrUjMUAn0g8ywzTJt4gnt60Dc77xCGWl1Xic+Ixky5I/Mh1xeM3zjmXBZ4ZMpOJeeIQsZhrYqWJWd7UiKeIw6qmU76Q8ljlvMVZK5ZZ/Z78hcGMvrLMdZrDiGERS5AgQkEZBRRhI0KrToqFBO1HW/iHXL9ELoVcBTByLKAEDbLrB/+D391a2ckJLykYBdpfHOdjBAjsArWK43wfO07tBPA/A1d6w1+qAjOfpFcaWvgI6N4GLq4bmrIHXO4AA0+GbMqu5KcpZLPA+xl9UxrouwU617ze6vs4fQCS1FX8Bjg4BEZzlL3e4t0dzb39e6be3w88D3KRJNOW/QAAAAZiS0dEACcAAAAB/aV4/QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+QCEhEhKGUfSx4AAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMElEQVQ4y2M8cuTIfwY8INnGlhGfPBMDhWDUgMFgAKM6A95oZph75PD/0UAc9gYAAER7B5JZPHhAAAAAAElFTkSuQmCC') var(--sli-color-picker-padding), var(--sli-color-picker-padding); + } + + &, &:focus-visible, &:active { + box-shadow: var(--sli-color-picker-inner-shadow); + } + + &:focus-visible { + outline: 0; + box-shadow: var(--sli-color-picker-inner-shadow), 0 0 0 2px var(--sli-focus-color) !important; + } +} + +.color-preview { + position: absolute; + top: calc(var(--sli-color-picker-padding) - 1px); + bottom: calc(var(--sli-color-picker-padding) - 1px); + left: calc(var(--sli-color-picker-padding) - 1px); + right: calc(var(--sli-color-picker-padding) - 1px); +} + +.popper { + z-index: 100; +} diff --git a/components/ColorPicker/index.tsx b/components/ColorPicker/index.tsx new file mode 100644 index 0000000..38f8204 --- /dev/null +++ b/components/ColorPicker/index.tsx @@ -0,0 +1,83 @@ +import React, {useEffect} from "react" +import css from "./ColorPicker.pcss" +import ChromePicker from "react-color/lib/components/chrome/Chrome" +import {Color, MultiColor} from "react-color-types" +import {useDetectTabOut} from "../../hooks/useDetectTabOut" +import {useDetectOutsideClick} from "../../hooks/useDetectOutsideClick" +import {useDocumentEventListener} from "../../hooks/useEventListener" +import {colorToString} from "../../utils/colorToString" +import {Manager, Popper, Reference} from "react-popper" +import {mergeRefs} from "../../utils/mergeRefs" + +type Props = { + id?: string; + value?: Color; + onChange?: (c: MultiColor) => void; + disableAlpha?: boolean; +} + +export function ColorPicker({id, value, disableAlpha, onChange}: Props) { + value = value ?? "#fff" + + const [color, setColor] = React.useState(value) + const [isOpen, setOpen] = React.useState(false) + const btn = React.useRef() + const picker = React.useRef() + + const close = React.useCallback(() => setOpen(false), []) + const toggle = React.useCallback(() => setOpen(v => !v), []) + + const handleChange = React.useCallback((color) => { + document.getSelection().removeAllRanges() + setColor(color.rgb) + onChange && onChange(color) + }, [onChange]) + + const onKeyDown = React.useCallback((e: KeyboardEvent) => { + if (e.key === "Escape" && isOpen) { + close() + e.preventDefault() + e.stopPropagation() + } + }, [isOpen]) + + useEffect(() => setColor(value), [value]) + useDetectOutsideClick(btn, close, [picker]) + useDetectTabOut([btn, picker], close) + useDocumentEventListener("keydown", onKeyDown, [isOpen]) + + const modifiers = { + preventOverflow: { + boundariesElement: document.body, + padding: 5, + }, + } + + return ( + + + {({ref}) => ( + + )} + + + { + ({ref, style}) => isOpen && ( +
+ +
+ ) + } +
+
+ ) +} diff --git a/components/Dashicon/index.tsx b/components/Dashicon/index.tsx new file mode 100644 index 0000000..3f89418 --- /dev/null +++ b/components/Dashicon/index.tsx @@ -0,0 +1,284 @@ +import React, {HTMLAttributes} from "react"; +import {classList} from "../../utils/classes"; + +export const Dashicon = ({icon, className, ...rest}: Props) => ( + +); + +export interface Props extends HTMLAttributes { + icon: DashiconTy; +} + +export type DashiconTy = + "menu" + | "menu-alt" + | "menu-alt2" + | "menu-alt3" + | "admin-site" + | "admin-site-alt" + | "admin-site-alt2" + | "admin-site-alt3" + | "dashboard" + | "admin-post" + | "admin-media" + | "admin-links" + | "admin-page" + | "admin-comments" + | "admin-appearance" + | "admin-plugins" + | "plugins-checked" + | "admin-users" + | "admin-tools" + | "admin-settings" + | "admin-network" + | "admin-home" + | "admin-generic" + | "admin-collapse" + | "filter" + | "admin-customizer" + | "admin-multisite" + | "welcome-write-blog" + | "welcome-add-page" + | "welcome-view-site" + | "welcome-widgets-menus" + | "welcome-comments" + | "welcome-learn-more" + | "format-aside" + | "format-image" + | "format-gallery" + | "format-video" + | "format-status" + | "format-quote" + | "format-chat" + | "format-audio" + | "camera" + | "camera-alt" + | "images-alt" + | "images-alt2" + | "video-alt" + | "video-alt2" + | "video-alt3" + | "media-archive" + | "media-audio" + | "media-code" + | "media-default" + | "media-document" + | "media-interactive" + | "media-spreadsheet" + | "media-text" + | "media-video" + | "playlist-audio" + | "playlist-video" + | "controls-play" + | "controls-pause" + | "controls-forward" + | "controls-skipforward" + | "controls-back" + | "controls-skipback" + | "controls-repeat" + | "controls-volumeon" + | "controls-volumeoff" + | "image-crop" + | "image-rotate" + | "image-rotate-left" + | "image-rotate-right" + | "image-flip-vertical" + | "image-flip-horizontal" + | "image-filter" + | "undo" + | "redo" + | "editor-bold" + | "editor-italic" + | "editor-ul" + | "editor-ol" + | "editor-ol-rtl" + | "editor-quote" + | "editor-alignleft" + | "editor-aligncenter" + | "editor-alignright" + | "editor-insertmore" + | "editor-spellcheck" + | "editor-expand" + | "editor-contract" + | "editor-kitchensink" + | "editor-underline" + | "editor-justify" + | "editor-textcolor" + | "editor-paste-word" + | "editor-paste-text" + | "editor-removeformatting" + | "editor-video" + | "editor-customchar" + | "editor-outdent" + | "editor-indent" + | "editor-help" + | "editor-strikethrough" + | "editor-unlink" + | "editor-rtl" + | "editor-ltr" + | "editor-break" + | "editor-code" + | "editor-paragraph" + | "editor-table" + | "align-left" + | "align-right" + | "align-center" + | "align-none" + | "lock" + | "unlock" + | "calendar" + | "calendar-alt" + | "visibility" + | "hidden" + | "post-status" + | "edit" + | "trash" + | "sticky" + | "external" + | "arrow-up" + | "arrow-down" + | "arrow-right" + | "arrow-left" + | "arrow-up-alt" + | "arrow-down-alt" + | "arrow-right-alt" + | "arrow-left-alt" + | "arrow-up-alt2" + | "arrow-down-alt2" + | "arrow-right-alt2" + | "arrow-left-alt2" + | "sort" + | "leftright" + | "randomize" + | "list-view" + | "excerpt-view" + | "grid-view" + | "move" + | "share" + | "share-alt" + | "share-alt2" + | "twitter" + | "rss" + | "email" + | "email-alt" + | "email-alt2" + | "facebook" + | "facebook-alt" + | "googleplus" + | "networking" + | "instagram" + | "hammer" + | "art" + | "migrate" + | "performance" + | "universal-access" + | "universal-access-alt" + | "tickets" + | "nametag" + | "clipboard" + | "heart" + | "megaphone" + | "schedule" + | "tide" + | "rest-api" + | "code-standards" + | "buddicons-activity" + | "buddicons-bbpress-logo" + | "buddicons-buddypress-logo" + | "buddicons-community" + | "buddicons-forums" + | "buddicons-friends" + | "buddicons-groups" + | "buddicons-pm" + | "buddicons-replies" + | "buddicons-topics" + | "buddicons-tracking" + | "wordpress" + | "wordpress-alt" + | "pressthis" + | "update" + | "update-alt" + | "screenoptions" + | "info" + | "cart" + | "feedback" + | "cloud" + | "translation" + | "tag" + | "category" + | "archive" + | "tagcloud" + | "text" + | "yes" + | "yes-alt" + | "no" + | "no-alt" + | "plus" + | "plus-alt" + | "plus-alt2" + | "minus" + | "dismiss" + | "marker" + | "star-filled" + | "star-half" + | "star-empty" + | "flag" + | "warning" + | "location" + | "location-alt" + | "vault" + | "shield" + | "shield-alt" + | "sos" + | "search" + | "slides" + | "text-page" + | "analytics" + | "chart-pie" + | "chart-bar" + | "chart-line" + | "chart-area" + | "groups" + | "businessman" + | "businesswoman" + | "businessperson" + | "id" + | "id-alt" + | "products" + | "awards" + | "forms" + | "testimonial" + | "portfolio" + | "book" + | "book-alt" + | "download" + | "upload" + | "backup" + | "clock" + | "lightbulb" + | "microphone" + | "desktop" + | "laptop" + | "tablet" + | "smartphone" + | "phone" + | "index-card" + | "carrot" + | "building" + | "store" + | "album" + | "palmtree" + | "tickets-alt" + | "money" + | "smiley" + | "thumbs-up" + | "thumbs-down" + | "layout" + | "paperclip" + | "database" + | "database-add" + | "database-remove" + | "database-view" + | "database-import" + | "database-export" +; diff --git a/components/FilteredListField/index.tsx b/components/FilteredListField/index.tsx new file mode 100644 index 0000000..3f6b4a1 --- /dev/null +++ b/components/FilteredListField/index.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import {ListField, Props as InnerProps} from "../ListField"; + +export interface Props extends InnerProps { + filter?: Function; + excludeMsg?: string; +} + +export function FilteredListField(props: Props) { + const [excluded, setExcluded] = React.useState(""); + + if (props.filter) { + setExcluded(""); + } + // TODO: fix come back n fix this + const onChange = (values: Array) => { + const excludedIdx = props.filter ? + values.findIndex((val) => props.filter(s => !values.includes(val))) + + // values.findIndex((val) => props.exclude.includes(val)) + : -1; + + if (excludedIdx > -1) { + setExcluded(values[excludedIdx]); + } else { + props.onChange(values); + } + }; + + let message = undefined; + if (excluded.length > 0) { + const token = "%s"; + const tokenIdx = props.excludeMsg.indexOf("%s"); + + const before = props.excludeMsg.substring(0, tokenIdx); + const after = props.excludeMsg.substring(tokenIdx + token.length); + + message = <>{before}{excluded}{after}; + } + + const newProps = { + ...props, + message, + onChange, + }; + + return ; +} diff --git a/components/Flex/index.tsx b/components/Flex/index.tsx new file mode 100644 index 0000000..b54de39 --- /dev/null +++ b/components/Flex/index.tsx @@ -0,0 +1,47 @@ +import React, {CSSProperties, forwardRef} from "react" + +type BaseProps = React.HTMLAttributes + +type FlexProps = BaseProps & { + dir?: "column" | "row", + wrap?: boolean, + justify?: CSSProperties["justifyContent"], + align?: CSSProperties["alignItems"], + justifySelf?: CSSProperties["justifySelf"], + alignSelf?: CSSProperties["alignSelf"], +} + +export const Flex = forwardRef( + function Flex( + { + dir = "column", + justify = "flex-start", + align = "center", + wrap, + justifySelf, + alignSelf, + style, + ...props + }, + ref, + ) { + const styles: CSSProperties = { + ...style, + display: "flex", + flexFlow: dir + " " + (wrap ? "wrap" : "nowrap"), + justifyContent: justify, + alignItems: align, + justifySelf, + alignSelf, + } + + return
+ }, +) + +type PropsWithoutDir = Omit + +// @ts-ignore +export const FlexColumn = (props: PropsWithoutDir) => +// @ts-ignore +export const FlexRow = (props: PropsWithoutDir) => diff --git a/components/HelpTooltip/HelpTooltip.pcss b/components/HelpTooltip/HelpTooltip.pcss new file mode 100644 index 0000000..72c9d38 --- /dev/null +++ b/components/HelpTooltip/HelpTooltip.pcss @@ -0,0 +1,60 @@ +.root { + display: inline-flex; + flex-direction: column; + justify-content: center; +} + +.tooltip { + composes: z-high from "../../../common/styles/layout.pcss"; +} + +.tooltip-container { + text-align: left; + padding-top: 7px; + padding-bottom: 7px; +} + +.tooltip-content { + & p { + margin: 0 0 5px; + + &:last-child { + margin-bottom: 0; + } + } + + & img { + max-width: 100%; + margin: 5px 0; + border-radius: 2px; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } +} + +.icon { + height: 18px; + line-height: 18px; + + & :global .dashicons { + font-size: 16px; + width: 16px; + height: 16px; + line-height: 16px; + vertical-align: bottom; + } +} + +.icon-visible { + opacity: 1; +} + +.icon-notvisible { + opcaity: 0.7; +} \ No newline at end of file diff --git a/components/HelpTooltip/index.tsx b/components/HelpTooltip/index.tsx new file mode 100644 index 0000000..3e82587 --- /dev/null +++ b/components/HelpTooltip/index.tsx @@ -0,0 +1,42 @@ +import React, {ReactNode} from "react"; +import css from "./HelpTooltip.pcss"; +import {Dashicon} from "../Dashicon"; +import Tooltip from "../Tooltip"; + +type Props = { + maxWidth?: number; + children: ReactNode; +} + +export default function HelpTooltip({maxWidth, children}: Props) { + maxWidth = maxWidth ?? 300; + + const [isTooltipVisible, setIsTooltipVisible] = React.useState(false); + + const handleMouseOver = () => setIsTooltipVisible(true); + const handleMouseOut = () => setIsTooltipVisible(false); + + const tooltipTheme = { + content: css.tooltipContent, + container: css.tooltipContainer, + }; + + return ( +
+ + { + ({ref}) => ( + + + + ) + } + +
{children}
+
+
+ ); +}; diff --git a/components/ListField/index.tsx b/components/ListField/index.tsx new file mode 100644 index 0000000..c54007d --- /dev/null +++ b/components/ListField/index.tsx @@ -0,0 +1,128 @@ +import React, {KeyboardEvent, ReactElement, useEffect} from "react"; +import CreatableSelect from "react-select"; +import {Alert} from "../Alert"; +import {SelectStyles} from "../Select"; + +const components = { + DropdownIndicator: null, +}; + +const createOption = (label: string) => ({ + label, + value: label, +}); + +export type Props = { + id?: string; + value: Array; + onChange?: (value: Array) => void; + sanitize?: (value: string) => string; + autoFocus?: boolean; + message?: string | ReactElement; +} + +export function ListField({id, value, onChange, sanitize, autoFocus, message}: Props) { + const [inputValue, setInputValue] = React.useState(""); + const [duplicate, setDuplicate] = React.useState(-1); + const [currMessage, setCurrMessage] = React.useState(); + + useEffect(() => { + setCurrMessage(message); + }, [message]); + + value = Array.isArray(value) ? value : []; + const values = value.map((v) => createOption(v)); + + const addValue = () => { + if (inputValue.length) { + setInputValue(""); + handleChange([...values, createOption(inputValue)]); + } + }; + + const handleChange = (value: any) => { + if (!onChange) return; + + let dupeIdx = -1; + + if (!value) { + value = []; + } else { + value = value + .map((opt) => (opt && sanitize) ? sanitize(opt.value) : opt.value) + .filter((val, idx, list) => { + const firstIndex = list.indexOf(val); + + if (firstIndex !== idx) { + dupeIdx = firstIndex; + + return false; + } + + return !!val; + }); + } + + setDuplicate(dupeIdx); + + if (dupeIdx === -1) { + onChange(value); + } + }; + + const handleInputChange = (inputValue: string) => { + setInputValue(inputValue); + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (!inputValue) return; + + switch (event.key) { + case ",": + case "Enter": + case "Tab": + addValue(); + event.preventDefault(); + break; + } + }; + + const styles = SelectStyles(); + + return ( + <> + + { + (duplicate < 0 || values.length === 0) ? null : ( + + {values[duplicate].label} is already in the list + + ) + } + { + (!currMessage) ? null : ( + + {currMessage} + + ) + } + + ); +} diff --git a/components/OrSeparator/OrSeperator.pcss b/components/OrSeparator/OrSeperator.pcss new file mode 100644 index 0000000..fb6e3f9 --- /dev/null +++ b/components/OrSeparator/OrSeperator.pcss @@ -0,0 +1,14 @@ +.container { + margin: 25px 0; +} + +.line { + flex: 1; + height: 1px; + background: var(--sli-line-color); +} + +.text { + margin: 0 12px; + font-size: 16px; +} diff --git a/components/OrSeparator/index.tsx b/components/OrSeparator/index.tsx new file mode 100644 index 0000000..171de15 --- /dev/null +++ b/components/OrSeparator/index.tsx @@ -0,0 +1,13 @@ +import React from "react" +import css from "./OrSeperator.pcss" +import {FlexRow} from "../Flex" + +export function OrSeparator() { + return ( + +
+ OR +
+ + ) +} diff --git a/components/RadioGroup/RadioGroup.pcss b/components/RadioGroup/RadioGroup.pcss new file mode 100644 index 0000000..1a53abc --- /dev/null +++ b/components/RadioGroup/RadioGroup.pcss @@ -0,0 +1,15 @@ +.radio-group { + display: flex; + flex-direction: column; +} + +.disabled { + composes: radio-group; + composes: disabled from "../../styles/theme.pcss"; +} + +.option { + display: flex; + flex-direction: row; + margin: 5px 0; +} diff --git a/components/RadioGroup/index.tsx b/components/RadioGroup/index.tsx new file mode 100644 index 0000000..fa629d2 --- /dev/null +++ b/components/RadioGroup/index.tsx @@ -0,0 +1,43 @@ +import React, {ChangeEvent} from "react"; +import css from "./RadioGroup.pcss"; + +export type RadioOption = { + value: string | number, + label: string +} + +type Props = { + name?: string; + className?: string; + value: string | number; + onChange?: (value) => void; + options: Array; + disabled?: boolean; +} + +export function RadioGroup({name, className, disabled, value, onChange, options}: Props) { + const handleChange = (e: ChangeEvent) => { + (!disabled && e.target.checked && onChange) && onChange(e.target.value); + }; + + className = (disabled ? css.disabled : css.radioGroup) + " " + (className ?? ""); + + return ( +
+ { + options.map((option, idx) => ( + + )) + } +
+ ); +} diff --git a/components/Select/Select.scss b/components/Select/Select.scss new file mode 100644 index 0000000..1163506 --- /dev/null +++ b/components/Select/Select.scss @@ -0,0 +1,13 @@ +$wp-blue: #007cba; + +// Theme colors +$primary-color: $wp-blue; +$washed-color: mix(desaturate($primary-color, 50%), #fff, 10%); +$shadow-color: rgba(20, 25, 60, 0.32); + +:export { + primaryColor: $primary-color; + shadowColor: $shadow-color; + washedColor: $washed-color; + } + \ No newline at end of file diff --git a/components/Select/index.tsx b/components/Select/index.tsx new file mode 100644 index 0000000..f732399 --- /dev/null +++ b/components/Select/index.tsx @@ -0,0 +1,133 @@ +import React, {MutableRefObject, ReactElement} from "react"; +import ReactSelect from "react-select"; +import CreatableSelect from "react-select/creatable"; +import AsyncSelect from "react-select/async"; +import css from "./Select.scss"; +import {classList} from "../../utils/classes"; + +export type SelectOption = { + value: any; + label: string | ReactElement; +} + +export declare type SelectChangeHandler = (option: SelectOption) => void; + +type Props = { + id?: string; + className?: string; + value?: string; + placeholder?: string; + onChange?: SelectChangeHandler; + options?: Array; + isSearchable?: boolean; + isMulti?: boolean; + isClearable?: boolean; + isCreatable?: boolean; + width?: string | number; + menuIsOpen?: boolean; + isValidNewOption?: (value: string) => boolean; + + [k: string]: any; +} + +export const SelectStyles = (props: Record = {}) => ({ + option: (prev, state) => ({ + ...prev, + cursor: "pointer", + lineHeight: "24px", + }), + menu: (prev, state) => ({ + ...prev, + margin: "6px 0", + boxShadow: "0 2px 8px " + css.shadowColor, + overflow: "hidden", + }), + menuList: (prev, state) => ({ + padding: "0px", + }), + control: (prev, state) => { + let style = { + ...prev, + cursor: "pointer", + lineHeight: "2", + minHeight: "40px", + }; + + if (state.isFocused) { + style.borderColor = css.primaryColor; + style.boxShadow = `0 0 0 1px ${css.primaryColor}`; + } + + return style; + }, + valueContainer: (prev, state) => ({ + ...prev, + paddingTop: 0, + paddingBottom: 0, + paddingRight: 0, + }), + container: (prev, state) => ({ + ...prev, + width: props.width || "100%", + }), + multiValue: (prev, state) => ({ + ...prev, + padding: "0 6px", + }), + input: (prev, state) => ({ + ...prev, + outline: "0 transparent !important", + border: "0 transparent !important", + boxShadow: "0 0 0 transparent !important", + }), + indicatorSeparator: (prev, state) => ({ + ...prev, + margin: "0", + backgroundColor: "transparent", + }), + menuPortal: base => ({ + ...base, + zIndex: 9999999 + }) +}); + +export const Select = React.forwardRef((props: Props, ref: MutableRefObject) => { + const options = props.options ?? []; + const value = options.find((opt) => opt.value === props.value); + + props = { + ...props, + id: undefined, + className: classList("react-select", props.className), + classNamePrefix: "react-select", + inputId: props.id, + menuPosition: "absolute", + }; + + const styles = SelectStyles(props); + + const theme = (theme) => ({ + ...theme, + borderRadius: 3, + colors: { + ...theme.colors, + primary: css.primaryColor, + primary25: css.washedColor, + }, + }); + + const Component = props.isCreatable ? CreatableSelect : props.async ? AsyncSelect : ReactSelect; + + return ( + + ); +}); diff --git a/components/Square/Square.pcss b/components/Square/Square.pcss new file mode 100644 index 0000000..4aa9f14 --- /dev/null +++ b/components/Square/Square.pcss @@ -0,0 +1,18 @@ +.filler { + position: relative; + padding-bottom: 100%; + box-sizing: border-box; +} + +.positioner { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + z-index: 0; + + & > *:first-child { + inset: 0; + flex: 1; + } +} diff --git a/components/Square/index.tsx b/components/Square/index.tsx new file mode 100644 index 0000000..288ccbd --- /dev/null +++ b/components/Square/index.tsx @@ -0,0 +1,15 @@ +import React, {HTMLAttributes} from "react"; +import css from "./Square.pcss"; +import {classList} from "../../utils/classes"; + +export type Props = HTMLAttributes & {}; + +export function Square({className, children, ...props}: Props) { + return ( +
+
+ {children} +
+
+ ); +} diff --git a/components/TextArea/index.tsx b/components/TextArea/index.tsx new file mode 100644 index 0000000..1b60773 --- /dev/null +++ b/components/TextArea/index.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +type Props = { + id?: string; + value: string; + onChange: (value: string) => void; +} + +export function TextArea({id, value, onChange}: Props) { + return ( +