From d807de0da918b4ccbfc96cd8d880f34810de8a65 Mon Sep 17 00:00:00 2001 From: chenz24 Date: Fri, 27 Aug 2021 14:24:33 +0800 Subject: [PATCH 1/4] feat: Initial components package and add config-provider,theme, button components --- packages/components/README.md | 0 packages/components/package.json | 33 +++ .../components/src/Button/Button.story.tsx | 36 +++ .../components/src/Button/Button.styles.ts | 260 +++++++++++++++++ packages/components/src/Button/Button.tsx | 87 ++++++ .../components/src/ConfigProvider/index.tsx | 19 ++ .../components/src/theme/AllThemesContext.ts | 16 + packages/components/src/theme/CssBaseline.tsx | 273 ++++++++++++++++++ packages/components/src/theme/ThemeContext.ts | 5 + .../components/src/theme/ThemeProvider.tsx | 37 +++ packages/components/src/theme/index.ts | 5 + .../components/src/theme/presets/colors.ts | 10 + packages/components/src/theme/presets/dark.ts | 65 +++++ .../components/src/theme/presets/default.ts | 65 +++++ .../components/src/theme/presets/shared.ts | 70 +++++ packages/components/src/theme/styled.d.ts | 6 + packages/components/src/theme/types.ts | 114 ++++++++ packages/components/src/theme/utils.ts | 98 +++++++ packages/components/src/utils/color.ts | 37 +++ packages/components/src/utils/forwardRef.tsx | 18 ++ packages/components/src/utils/toArray.ts | 29 ++ packages/components/src/utils/types.ts | 57 ++++ packages/components/tsconfig.json | 17 ++ yarn.lock | 45 ++- 24 files changed, 1399 insertions(+), 3 deletions(-) create mode 100644 packages/components/README.md create mode 100644 packages/components/package.json create mode 100644 packages/components/src/Button/Button.story.tsx create mode 100644 packages/components/src/Button/Button.styles.ts create mode 100644 packages/components/src/Button/Button.tsx create mode 100644 packages/components/src/ConfigProvider/index.tsx create mode 100644 packages/components/src/theme/AllThemesContext.ts create mode 100644 packages/components/src/theme/CssBaseline.tsx create mode 100644 packages/components/src/theme/ThemeContext.ts create mode 100644 packages/components/src/theme/ThemeProvider.tsx create mode 100644 packages/components/src/theme/index.ts create mode 100644 packages/components/src/theme/presets/colors.ts create mode 100644 packages/components/src/theme/presets/dark.ts create mode 100644 packages/components/src/theme/presets/default.ts create mode 100644 packages/components/src/theme/presets/shared.ts create mode 100644 packages/components/src/theme/styled.d.ts create mode 100644 packages/components/src/theme/types.ts create mode 100644 packages/components/src/theme/utils.ts create mode 100644 packages/components/src/utils/color.ts create mode 100644 packages/components/src/utils/forwardRef.tsx create mode 100644 packages/components/src/utils/toArray.ts create mode 100644 packages/components/src/utils/types.ts create mode 100644 packages/components/tsconfig.json diff --git a/packages/components/README.md b/packages/components/README.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/components/package.json b/packages/components/package.json new file mode 100644 index 00000000..97b36f14 --- /dev/null +++ b/packages/components/package.json @@ -0,0 +1,33 @@ +{ + "name": "@kubed/components", + "description": "React components library for KubeSphere console.", + "version": "0.0.1", + "main": "cjs/index.js", + "module": "esm/index.js", + "browser": "lib/index.umd.js", + "types": "lib/src/index.d.ts", + "license": "MIT", + "sideEffects": false, + "repository": { + "url": "https://github.com/kubesphere/kube-design.git", + "type": "git", + "directory": "packages/components" + }, + "peerDependencies": { + "@kubed/hooks": "0.0.1", + "@kubed/icons": "0.0.1", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "react-is": "^17.0.2" + }, + "dependencies": { + "@tippyjs/react": "^4.2.5", + "clsx": "^1.1.1", + "react-textarea-autosize": "^8.3.2", + "react-transition-group": "^4.4.2", + "react-feather": "^2.0.9" + }, + "devDependencies": { + "@kubed/tests": "0.0.1" + } +} diff --git a/packages/components/src/Button/Button.story.tsx b/packages/components/src/Button/Button.story.tsx new file mode 100644 index 00000000..df5ce5ee --- /dev/null +++ b/packages/components/src/Button/Button.story.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { Add } from '@kubed/icons'; +import { Group } from '../Group/Group'; +import { Button } from './Button'; + +storiesOf('@kubed/components/Button', module) + .addParameters({ component: Button }) + .add('Demos', () => ( + + + + + + + )); + +storiesOf('@kubed/components/Button', module).add('Color', () => ( + +)); + +storiesOf('@kubed/components/Button', module).add('Icon', () => ( + +)); diff --git a/packages/components/src/Button/Button.styles.ts b/packages/components/src/Button/Button.styles.ts new file mode 100644 index 00000000..996f7be9 --- /dev/null +++ b/packages/components/src/Button/Button.styles.ts @@ -0,0 +1,260 @@ +import styled, { css } from 'styled-components'; +import { KubedNumberSize, KubedSizes, KubedTheme } from '../theme'; +import { colorToRgbValues } from '../utils/color'; + +interface ButtonStylesProps { + loading?: boolean; + size?: KubedSizes; + radius?: KubedNumberSize; + theme?: KubedTheme; + block?: boolean; + disabled?: boolean; + shadow?: boolean; + variant?: 'filled' | 'outline' | 'text' | 'link'; + color?: string | 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error'; +} + +const getButtonCursor = (loading: boolean, disabled: boolean) => { + if (disabled) { + return css` + cursor: not-allowed; + pointer-events: auto; + `; + } + if (loading) { + return css` + cursor: default; + pointer-events: none; + `; + } + return css` + cursor: pointer; + pointer-events: auto; + `; +}; + +const sizes = { + xs: css` + font-size: 10px; + height: 28px; + padding: 0 14px; + `, + sm: css` + font-size: 12px; + height: 32px; + padding: 0 20px; + `, + md: css` + font-size: 14px; + height: 36px; + padding: 0 23px; + `, + lg: css` + font-size: 16px; + height: 48px; + padding: 0 26px; + `, + xl: css` + font-size: 18px; + height: 56px; + padding: 0 30px; + `, +}; + +const getButtonSize = (size, block) => css` + ${sizes[size]} + width: ${block ? '100%' : 'auto'} +`; + +const getButtonColor = (variant = 'filled', colorSchema, disabled, theme: KubedTheme) => { + const { palette } = theme; + if (disabled) { + return css` + background-color: ${palette.accents_1}; + border-color: ${palette.border}; + color: ${palette.accents_7}; + opacity: 0.7; + `; + } + + const styles = { + filled: css` + background-color: ${colorSchema.bg}; + border-color: ${colorSchema.border}; + color: ${colorSchema.color}; + `, + outline: css` + background-color: #fff; + border-color: ${colorSchema.border}; + color: ${colorSchema.fg || colorSchema.bg}; + `, + link: css` + font-weight: 400; + background-color: transparent; + border-color: transparent; + color: ${colorSchema.fg || colorSchema.bg}; + padding: 0; + `, + text: css` + background-color: transparent; + border-color: transparent; + color: ${colorSchema.fg || colorSchema.bg}; + `, + }; + + return styles[variant]; +}; + +const getButtonHoverColor = ( + variant = 'filled', + colorSchema, + disabled, + shadow, + theme: KubedTheme +) => { + if (disabled) return null; + if (shadow) { + return css` + box-shadow: none; + `; + } + const { palette } = theme; + if (variant === 'link') { + return css` + text-decoration: underline; + `; + } + if (variant === 'text') { + return css` + background-color: ${palette.accents_2}; + `; + } + + const styles = { + filled: css` + background-color: ${colorSchema.hoverBg}; + border-color: ${colorSchema.hoverBorder || colorSchema.hoverBg}; + `, + outline: css` + background-color: ${colorSchema.hoverBg}; + color: ${colorSchema.color}; + `, + text: css` + background-color: transparent; + border-color: transparent; + color: ${palette.accents_5}; + `, + }; + return styles[variant]; +}; + +const getShadow = (shadow, colorSchema, theme) => { + if (shadow) { + const { expressiveness } = theme; + const rgb = colorToRgbValues(colorSchema.bg).join(' '); + return expressiveness.buttonShadow(rgb); + } + return null; +}; + +const getButtonStyles = (theme: KubedTheme, color = 'default', variant, shadow, disabled) => { + const { palette } = theme; + const { background, border, accents_1, accents_2, accents_7, accents_8, accents_9, primary } = + palette; + const colors = { + default: { + bg: accents_1, + border, + color: accents_7, + fg: accents_7, + hoverBg: accents_2, + hoverBorder: border, + }, + primary: { + bg: palette.colors[primary][2], + border: palette.colors[primary][2], + color: '#fff', + hoverBg: palette.colors[primary][3], + }, + secondary: { + bg: accents_8, + border: accents_8, + color: background, + hoverBg: accents_9, + }, + }; + + let colorSchema; + if (colors[color]) { + colorSchema = colors[color]; + } else if (palette[color]) { + colorSchema = { + bg: palette[color], + border: palette[color], + color: '#fff', + hoverBg: palette[`${color}Dark`], + }; + } else if (palette.colors[color]) { + const currentColor = palette.colors[color]; + colorSchema = { + bg: currentColor[2], + border: currentColor[2], + color: '#fff', + hoverBg: currentColor[3], + }; + } else { + colorSchema = colors.default; + } + + return css` + box-shadow: ${getShadow(shadow, colorSchema, theme)}; + ${getButtonColor(variant, colorSchema, disabled, theme)}; + + &:hover { + ${getButtonHoverColor(variant, colorSchema, disabled, shadow, theme)}; + } + `; +}; + +export const ButtonContainer = styled('div')` + box-sizing: border-box; + user-select: none; + outline: none; + position: relative; + overflow: hidden; + font-weight: 600; + border: 1px solid; + transition: all 0.3s ease-in-out; + border-radius: ${({ radius, theme }) => theme.layout.radius[radius!]}; + display: ${(props) => (props.block ? 'block' : 'inline-block')}; + ${({ loading, disabled }) => getButtonCursor(loading, disabled)}; + ${({ size, block }) => getButtonSize(size, block)}; + ${({ theme, color, variant, shadow, disabled }) => + getButtonStyles(theme, color, variant, shadow, disabled)}; +`; + +export const ButtonInner = styled('div')` + display: flex; + justify-content: center; + align-items: center; + height: 100%; +`; + +export const ButtonLabel = styled('span')` + display: inherit; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +export const IconLeftSpan = styled('span')` + display: flex; + align-items: center; + margin-right: 10px; +`; + +export const IconRightSpan = styled('span')` + display: flex; + align-items: center; + margin-left: 10px; +`; diff --git a/packages/components/src/Button/Button.tsx b/packages/components/src/Button/Button.tsx new file mode 100644 index 00000000..3e4300cc --- /dev/null +++ b/packages/components/src/Button/Button.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { DefaultProps, KubedNumberSize, KubedSizes } from '../theme'; +import forwardRef from '../utils/forwardRef'; +import { + ButtonContainer, + ButtonInner, + ButtonLabel, + IconLeftSpan, + IconRightSpan, +} from './Button.styles'; + +interface ButtonOptions { + /** Predefined button size */ + size?: KubedSizes; + + /** Disable button or not */ + disabled?: boolean; + + /** Display shadow or not */ + shadow?: boolean; + + /** Display shadow or not */ + block?: boolean; + + /** Display loading indicator */ + loading?: boolean; + + /** Adds icon before button label */ + leftIcon?: React.ReactNode; + + /** Adds icon after button label */ + rightIcon?: React.ReactNode; + + /** Button border-radius from theme or number to set border-radius in px */ + radius?: KubedNumberSize; + + /** Button color from theme */ + color?: string | 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error'; + + /** Controls button appearance */ + variant?: 'filled' | 'outline' | 'text' | 'link'; +} + +export interface ButtonProps extends ButtonOptions, DefaultProps {} + +export const Button = forwardRef( + ( + { + className, + color = 'default', + disabled = false, + shadow = false, + size = 'sm', + radius = 'sm', + as = 'button', + leftIcon, + rightIcon, + variant = 'filled', + children, + ...others + }, + ref + ) => { + return ( + + + {leftIcon && {leftIcon}} + {children} + {rightIcon && {rightIcon}} + + + ); + } +); + +Button.displayName = '@kubed/components/Button'; diff --git a/packages/components/src/ConfigProvider/index.tsx b/packages/components/src/ConfigProvider/index.tsx new file mode 100644 index 00000000..cf3d0171 --- /dev/null +++ b/packages/components/src/ConfigProvider/index.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { KubedTheme } from '../theme'; +import ThemeProvider from '../theme/ThemeProvider'; + +export interface Props { + children: React.ReactNode; + themes?: Array; + themeType?: string | 'dark' | 'light'; +} + +export function KubedConfigProvider({ children, themes, themeType }: Props) { + return ( + + {children} + + ); +} + +KubedConfigProvider.displayName = '@kubed/ConfigProvider'; diff --git a/packages/components/src/theme/AllThemesContext.ts b/packages/components/src/theme/AllThemesContext.ts new file mode 100644 index 00000000..2f37196f --- /dev/null +++ b/packages/components/src/theme/AllThemesContext.ts @@ -0,0 +1,16 @@ +import { Context, createContext, useContext } from 'react'; +import themeUtils from './utils'; +import { KubedTheme } from './types'; + +export type AllThemesConfig = { + themes: Array; +}; + +const defaultAllThemesConfig = { + themes: themeUtils.getPresets(), +}; + +export const AllThemesContext: Context = + createContext(defaultAllThemesConfig); + +export const useAllThemes = (): AllThemesConfig => useContext(AllThemesContext); diff --git a/packages/components/src/theme/CssBaseline.tsx b/packages/components/src/theme/CssBaseline.tsx new file mode 100644 index 00000000..05a6d480 --- /dev/null +++ b/packages/components/src/theme/CssBaseline.tsx @@ -0,0 +1,273 @@ +import React from 'react'; +import { createGlobalStyle } from 'styled-components'; + +const CssBaseline = createGlobalStyle` + html, + body { + background-color: ${({ theme }) => theme.palette.background}; + color: ${(p) => p.theme.palette.foreground}; + } + html { + font-size: ${({ theme }) => theme.font.fontSizeBase}; + --kubed-icons-background: ${(p) => p.theme.palette.background}; + } + + body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-size: 1rem; + line-height: 1.5; + margin: 0; + padding: 0; + min-height: 100%; + position: relative; + overflow-x: hidden; + font-family: ${(p) => p.theme.font.sans}; + } + + *, + *:before, + *:after { + box-sizing: border-box; + //text-rendering: geometricPrecision; + -webkit-tap-highlight-color: transparent; + } + + p, + small { + font-weight: 400; + color: inherit; + letter-spacing: -0.005625rem; + font-family: ${(p) => p.theme.font.sans}; + } + + p { + margin: 1rem 0; + font-size: 1em; + line-height: 1.625em; + } + small { + margin: 0; + line-height: 1.5; + font-size: 0.875rem; + } + b { + font-weight: 600; + } + span { + font-size: inherit; + color: inherit; + font-weight: inherit; + } + img { + max-width: 100%; + } + + a { + cursor: pointer; + font-size: inherit; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-box-align: center; + align-items: center; + color: ${(p) => p.theme.palette.link}; + text-decoration: ${(p) => p.theme.expressiveness.linkStyle}; + } + a:hover { + text-decoration: ${(p) => p.theme.expressiveness.linkHoverStyle}; + } + ul, + ol { + padding: 0; + list-style-type: none; + margin: ${(p) => p.theme.layout.gapHalf} ${(p) => p.theme.layout.gapHalf} ${(props) => + props.theme.layout.gapHalf} + ${(p) => p.theme.layout.gap}; + color: ${(p) => p.theme.palette.foreground}; + } + ol { + list-style-type: decimal; + } + li { + margin-bottom: 0.625rem; + font-size: 1em; + line-height: 1.625em; + } + ul li:before { + content: '–'; + display: inline-block; + color: ${(p) => p.theme.palette.accents_4}; + position: absolute; + margin-left: -15px; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + color: inherit; + margin: 0 0 0.625rem 0; + } + h1 { + font-size: 3rem; + letter-spacing: -0.066875rem; + line-height: 1.5; + font-weight: 700; + } + h2 { + font-size: 2.25rem; + letter-spacing: -0.020625rem; + font-weight: 600; + } + h3 { + font-size: 1.5rem; + letter-spacing: -0.029375rem; + font-weight: 600; + } + h4 { + font-size: 1.25rem; + letter-spacing: -0.020625rem; + font-weight: 600; + } + h5 { + font-size: 1rem; + letter-spacing: -0.01125rem; + font-weight: 600; + } + h6 { + font-size: 0.875rem; + letter-spacing: -0.005625rem; + font-weight: 600; + } + + button, + input, + select, + textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: inherit; + margin: 0; + } + button:focus, + input:focus, + select:focus, + textarea:focus { + outline: none; + } + + code { + color: ${(p) => p.theme.palette.code}; + font-family: ${(p) => p.theme.font.mono}; + font-size: 0.9em; + white-space: pre-wrap; + } + + code:before, + code:after { + content: '\`'; + } + + pre { + border: 1px solid ${(p) => p.theme.palette.accents_2}; + border-radius: ${(p) => p.theme.layout.radius.sm}; + padding: calc(${(p) => p.theme.layout.gap} * 0.75) ${(p) => p.theme.layout.gap}; + margin: ${(p) => p.theme.layout.gap} 0; + font-family: ${(p) => p.theme.font.mono}; + white-space: pre; + overflow: auto; + line-height: 1.5; + text-align: left; + font-size: 0.875rem; + -webkit-overflow-scrolling: touch; + } + + pre code { + color: ${(p) => p.theme.palette.foreground}; + font-size: 0.8125rem; + line-height: 1.25rem; + white-space: pre; + } + + pre code:before, + pre code:after { + display: none; + } + pre :global(p) { + margin: 0; + } + pre::-webkit-scrollbar { + display: none; + width: 0; + height: 0; + background: transparent; + } + hr { + border-color: ${(p) => p.theme.palette.accents_2}; + } + details { + background-color: ${(p) => p.theme.palette.accents_1}; + border: none; + } + + details:focus, + details:hover, + details:active { + outline: none; + } + + summary { + cursor: pointer; + user-select: none; + list-style: none; + outline: none; + } + + summary::marker, + summary::before { + display: none; + } + + summary::-moz-list-bullet { + font-size: 0; + } + + summary:focus, + summary:hover, + summary:active { + outline: none; + list-style: none; + } + + blockquote { + padding: calc(0.667 * ${(p) => p.theme.layout.gap}) ${(p) => p.theme.layout.gap}; + color: ${(p) => p.theme.palette.accents_5}; + background-color: ${(p) => p.theme.palette.accents_1}; + border-radius: ${(p) => p.theme.layout.radius.sm}; + margin: 1.5rem 0; + border: 1px solid ${(p) => p.theme.palette.border}; + } + blockquote :global(*:first-child) { + margin-top: 0; + } + blockquote :global(*:last-child) { + margin-bottom: 0; + } + ::selection { + background-color: ${(p) => p.theme.palette.selection}; + color: ${(p) => p.theme.palette.foreground}; + } + + .kubed-icon__light { + color: rgba(255, 255, 255, 0.9); + fill: rgba(255, 255, 255, 0.4); + } +`; + +const MemoCssBaseline = React.memo(CssBaseline); + +export default MemoCssBaseline; diff --git a/packages/components/src/theme/ThemeContext.ts b/packages/components/src/theme/ThemeContext.ts new file mode 100644 index 00000000..d2f8f805 --- /dev/null +++ b/packages/components/src/theme/ThemeContext.ts @@ -0,0 +1,5 @@ +import { useContext } from 'react'; +import { ThemeContext } from 'styled-components'; +import { KubedTheme } from './types'; + +export const useTheme = (): KubedTheme => useContext(ThemeContext); diff --git a/packages/components/src/theme/ThemeProvider.tsx b/packages/components/src/theme/ThemeProvider.tsx new file mode 100644 index 00000000..ba9d04cd --- /dev/null +++ b/packages/components/src/theme/ThemeProvider.tsx @@ -0,0 +1,37 @@ +import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react'; +import { ThemeProvider as SCProvider } from 'styled-components'; +import themeUtils from './utils'; +import { KubedTheme } from './types'; +import { AllThemesConfig, AllThemesContext } from './AllThemesContext'; + +export interface Props { + themeType?: string; + themes?: Array; +} + +function ThemeProvider({ children, themeType, themes = [] }: PropsWithChildren) { + const [allThemes, setAllThemes] = useState({ themes: themeUtils.getPresets() }); + + const currentTheme = useMemo(() => { + const theme = allThemes.themes.find((item) => item.type === themeType); + if (theme) return theme; + return themeUtils.getPresetStaticTheme(); + }, [allThemes, themeType]); + + useEffect(() => { + if (!themes?.length) return; + setAllThemes((last) => { + const safeThemes = themes.filter((item) => themeUtils.isAvailableThemeType(item.type)); + const nextThemes = themeUtils.getPresets().concat(safeThemes); + return { ...last, themes: nextThemes }; + }); + }, [themes]); + + return ( + + {children} + + ); +} + +export default ThemeProvider; diff --git a/packages/components/src/theme/index.ts b/packages/components/src/theme/index.ts new file mode 100644 index 00000000..763572cb --- /dev/null +++ b/packages/components/src/theme/index.ts @@ -0,0 +1,5 @@ +export * from './types'; + +export { default as CssBaseline } from './CssBaseline'; +export { useTheme } from './ThemeContext'; +export { default as themeUtils } from './utils'; diff --git a/packages/components/src/theme/presets/colors.ts b/packages/components/src/theme/presets/colors.ts new file mode 100644 index 00000000..230d15f7 --- /dev/null +++ b/packages/components/src/theme/presets/colors.ts @@ -0,0 +1,10 @@ +const colors = { + white: ['#f9fbfd', '#eff4f9', '#e3e9ef', '#ccd3db', '#abb4be'], + dark: ['#6b7b95', '#4a5974', '#36435c', '#242e42', '#181d28'], + blue: ['#c7deef', '#7eb8dc', '#329dce', '#3385b0', '#326e93'], + green: ['#c4e6d4', '#a2d8bb', '#55bc8a', '#479e88', '#3b747a'], + yellow: ['#ffe1be', '#ffc781', '#f5a623', '#e0992c', '#8d663e'], + red: ['#fae7e5', '#ea8573', '#ca2621', '#ab2F29', '#8c3231'], +}; + +export default colors; diff --git a/packages/components/src/theme/presets/dark.ts b/packages/components/src/theme/presets/dark.ts new file mode 100644 index 00000000..00cd60d0 --- /dev/null +++ b/packages/components/src/theme/presets/dark.ts @@ -0,0 +1,65 @@ +import { css } from 'styled-components'; +import { KubedTheme, KubedThemePalette, KubedThemeExpressiveness } from '../types'; +import { defaultFont, defaultBreakpoints, defaultLayout } from './shared'; +import colors from './colors'; + +export const palette: KubedThemePalette = { + colors, + accents_0: '#181d28', + accents_1: '#242e42', + accents_2: '#36435c', + accents_3: '#5f708a', + accents_4: '#79879c', + accents_5: '#abb4be', + accents_6: '#ccd3db', + accents_7: '#e3e9ef', + accents_8: '#eff4f9', + accents_9: '#f9fbfd', + background: '#000', + foreground: '#fff', + selection: '#f81ce5', + primary: 'green', + code: '#79ffe1', + border: '#333', + error: '#e00', + errorLight: '#ff1a1a', + errorLighter: '#f7d4d6', + errorDark: '#c50000', + success: '#0070f3', + successLight: '#3291ff', + successLighter: '#d3e5ff', + successDark: '#0761d1', + warning: '#f5a623', + warningLight: '#f7b955', + warningLighter: '#ffefcf', + warningDark: '#ab570a', + link: '#3291ff', +}; + +export const expressiveness: KubedThemeExpressiveness = { + linkStyle: 'none', + linkHoverStyle: 'none', + dropdownBoxShadow: '0 0 0 1px #333', + shadowSmall: '0 0 0 1px #333', + shadowMedium: '0 0 0 1px #333', + shadowLarge: '0 0 0 1px #333', + buttonShadow: (rgb) => css`0 8px 16px 0 rgb(${rgb} / 28%)`, + portalOpacity: 0.75, +}; + +export const font = defaultFont; + +export const breakpoints = defaultBreakpoints; + +export const layout = defaultLayout; + +export const themes: KubedTheme = { + type: 'dark', + font, + layout, + palette, + breakpoints, + expressiveness, +}; + +export default themes; diff --git a/packages/components/src/theme/presets/default.ts b/packages/components/src/theme/presets/default.ts new file mode 100644 index 00000000..e9cc225e --- /dev/null +++ b/packages/components/src/theme/presets/default.ts @@ -0,0 +1,65 @@ +import { css } from 'styled-components'; +import { KubedTheme, KubedThemePalette, KubedThemeExpressiveness } from '../types'; +import { defaultFont, defaultBreakpoints, defaultLayout } from './shared'; +import colors from './colors'; + +export const palette: KubedThemePalette = { + colors, + accents_0: '#f9fbfd', + accents_1: '#eff4f9', + accents_2: '#e3e9ef', + accents_3: '#ccd3db', + accents_4: '#abb4be', + accents_5: '#79879c', + accents_6: '#5f708a', + accents_7: '#36435c', + accents_8: '#242e42', + accents_9: '#181d28', + background: '#fff', + foreground: '#000', + selection: '#79ffe1', + primary: 'green', + code: '#f81ce5', + border: '#ccd3db', + error: colors.red[2], + errorLight: colors.red[1], + errorLighter: colors.red[0], + errorDark: colors.red[3], + success: colors.green[2], + successLight: colors.green[1], + successLighter: colors.green[0], + successDark: colors.green[3], + warning: colors.yellow[2], + warningLight: colors.yellow[1], + warningLighter: colors.yellow[0], + warningDark: colors.yellow[3], + link: '#0070f3', +}; + +export const expressiveness: KubedThemeExpressiveness = { + linkStyle: 'none', + linkHoverStyle: 'none', + dropdownBoxShadow: '0 4px 4px 0 rgba(0, 0, 0, 0.02)', + shadowSmall: '0 5px 10px rgba(0, 0, 0, 0.12)', + shadowMedium: '0 8px 30px rgba(0, 0, 0, 0.12)', + shadowLarge: '0 30px 60px rgba(0, 0, 0, 0.12)', + buttonShadow: (rgb) => css`0 8px 16px 0 rgb(${rgb} / 28%)`, + portalOpacity: 0.25, +}; + +export const font = defaultFont; + +export const breakpoints = defaultBreakpoints; + +export const layout = defaultLayout; + +export const themes: KubedTheme = { + type: 'light', + font, + layout, + palette, + breakpoints, + expressiveness, +}; + +export default themes; diff --git a/packages/components/src/theme/presets/shared.ts b/packages/components/src/theme/presets/shared.ts new file mode 100644 index 00000000..fa279417 --- /dev/null +++ b/packages/components/src/theme/presets/shared.ts @@ -0,0 +1,70 @@ +import { KubedThemeBreakpoints, KubedThemeFont, KubedThemeLayout } from '../types'; + +export const defaultFont: KubedThemeFont = { + sans: 'PingFang SC,Lantinghei SC,Helvetica Neue,Helvetica,Arial,Microsoft YaHei,微软雅黑,STHeitiSC-Light,simsun,宋体,WenQuanYi Zen Hei,WenQuanYi Micro Hei,sans-serif', + mono: 'Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace', + fontSizeBase: '12px', +}; + +export const defaultBreakpoints: KubedThemeBreakpoints = { + xs: { + min: '0', + max: '650px', + }, + sm: { + min: '650px', + max: '900px', + }, + md: { + min: '900px', + max: '1280px', + }, + lg: { + min: '1280px', + max: '1920px', + }, + xl: { + min: '1920px', + max: '10000px', + }, +}; + +// const fontSizes = { +// xs: '10px', +// sm: '12px', +// md: '14px', +// lg: '16px', +// xl: '18px', +// }; + +const spacing = { + xs: '10px', + sm: '12px', + md: '16px', + lg: '24px', + xl: '32px', +}; + +const radius = { + xs: '2px', + sm: '4px', + md: '8px', + lg: '16px', + xl: '32px', +}; + +export const defaultLayout: KubedThemeLayout = { + spacing, + gap: '20px', + gapNegative: '-20px', + gapHalf: '10px', + gapHalfNegative: '-10px', + gapQuarter: '5px', + gapQuarterNegative: '-5px', + pageMargin: '20px', + pageWidth: '1200px', + pageWidthWithMargin: '1240px', + breakpointMobile: defaultBreakpoints.xs.max, + breakpointTablet: defaultBreakpoints.sm.max, + radius, +}; diff --git a/packages/components/src/theme/styled.d.ts b/packages/components/src/theme/styled.d.ts new file mode 100644 index 00000000..2321f9f4 --- /dev/null +++ b/packages/components/src/theme/styled.d.ts @@ -0,0 +1,6 @@ +import 'styled-components'; +import { KubedTheme } from './types'; + +declare module 'styled-components' { + export interface DefaultTheme extends KubedTheme {} +} diff --git a/packages/components/src/theme/types.ts b/packages/components/src/theme/types.ts new file mode 100644 index 00000000..03e13494 --- /dev/null +++ b/packages/components/src/theme/types.ts @@ -0,0 +1,114 @@ +import { ThemedCssFunction } from 'styled-components'; +import type { CSSProperties } from 'react'; +import { DeepPartial } from '../utils/types'; + +export interface KubedThemePalette { + colors: Record>; + accents_0: string; + accents_1: string; + accents_2: string; + accents_3: string; + accents_4: string; + accents_5: string; + accents_6: string; + accents_7: string; + accents_8: string; + accents_9: string; + background: string; + foreground: string; + selection: string; + primary: string; + code: string; + border: string; + success: string; + successLighter: string; + successLight: string; + successDark: string; + error: string; + errorLighter: string; + errorLight: string; + errorDark: string; + warning: string; + warningLighter: string; + warningLight: string; + warningDark: string; + link: string; +} + +export interface KubedThemeExpressiveness { + linkStyle: string; + linkHoverStyle: string; + dropdownBoxShadow: string; + shadowSmall: string; + shadowMedium: string; + shadowLarge: string; + buttonShadow: ThemedCssFunction; + portalOpacity: number; +} + +export interface KubedThemeLayout { + spacing: Record; + gap: string; + gapNegative: string; + gapHalf: string; + gapHalfNegative: string; + gapQuarter: string; + gapQuarterNegative: string; + pageMargin: string; + pageWidth: string; + pageWidthWithMargin: string; + breakpointMobile: string; + breakpointTablet: string; + radius: Record; +} + +export interface KubedThemeFont { + sans: string; + mono: string; + fontSizeBase: string; +} + +export interface BreakpointsItem { + min: string; + max: string; +} + +export interface KubedThemeBreakpoints { + xs: BreakpointsItem; + sm: BreakpointsItem; + md: BreakpointsItem; + lg: BreakpointsItem; + xl: BreakpointsItem; +} + +export type KubedSizes = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +export const sizes = { + xs: 30, + sm: 36, + md: 42, + lg: 50, + xl: 60, +}; + +export type KubedNumberSize = KubedSizes | number; + +export interface KubedTheme { + type: string; + font: KubedThemeFont; + layout: KubedThemeLayout; + palette: KubedThemePalette; + breakpoints: KubedThemeBreakpoints; + expressiveness: KubedThemeExpressiveness; +} + +export type KubedThemeOverride = DeepPartial; + +export interface DefaultProps { + className?: string; + style?: CSSProperties; + themeType?: string; + readonly themeOverride?: KubedThemeOverride; + classNames?: Partial>; + styles?: Partial>; +} diff --git a/packages/components/src/theme/utils.ts b/packages/components/src/theme/utils.ts new file mode 100644 index 00000000..1527ac38 --- /dev/null +++ b/packages/components/src/theme/utils.ts @@ -0,0 +1,98 @@ +import { KubedTheme } from './types'; +import { DeepPartial } from '../utils/types'; +import lightTheme from './presets/default'; +import darkTheme from './presets/dark'; + +export const getSizeValue = ( + size: string | number, + sizes: Record, + defaultSize: string = 'md' +) => { + if (typeof size === 'number') { + return `${size}px`; + } + + return sizes[size] || size || sizes[defaultSize]; +}; + +export type UserTheme = DeepPartial & { type: string }; + +export const isObject = (target: unknown) => target && typeof target === 'object'; + +export const deepDuplicable = >(source: T, target: T): T => { + if (!isObject(target) || !isObject(source)) return source as T; + + const sourceKeys = Object.keys(source) as Array; + const result = {} as any; + // eslint-disable-next-line no-restricted-syntax + for (const key of sourceKeys) { + const sourceValue = source[key]; + const targetValue = target[key]; + + if (Array.isArray(sourceValue) && Array.isArray(targetValue)) { + result[key] = targetValue.concat(sourceValue); + } else if (isObject(sourceValue) && isObject(targetValue)) { + result[key] = deepDuplicable(sourceValue as Record, { + ...(targetValue as Record), + }); + } else if (targetValue) { + result[key] = targetValue; + } else { + result[key] = sourceValue; + } + } + return result; +}; + +const getPresets = (): Array => { + return [lightTheme, darkTheme]; +}; + +const getPresetStaticTheme = (): KubedTheme => { + return lightTheme; +}; + +const isAvailableThemeType = (type?: string): boolean => { + if (!type) return false; + const presetThemes = getPresets(); + const hasType = presetThemes.find((theme) => theme.type === type); + return !hasType; +}; + +const isPresetTheme = (themeOrType?: UserTheme | KubedTheme | string): boolean => { + if (!themeOrType) return false; + const isType = typeof themeOrType === 'string'; + const type = isType + ? (themeOrType as string) + : (themeOrType as Exclude).type; + return !isAvailableThemeType(type); +}; + +const hasUserCustomTheme = (themes: Array = []): boolean => { + return !!themes.find((item) => isAvailableThemeType(item.type)); +}; + +const create = (base: KubedTheme, custom: UserTheme): KubedTheme => { + if (!isAvailableThemeType(custom.type)) { + throw new Error('Duplicate or unavailable theme type'); + } + + return deepDuplicable(base, custom) as KubedTheme; +}; + +const createFromDark = (custom: UserTheme) => create(darkTheme, custom); +const createFromLight = (custom: UserTheme) => create(lightTheme, custom); + +const Utils = { + isPresetTheme, + isAvailableThemeType, + hasUserCustomTheme, + getPresets, + getPresetStaticTheme, + create, + createFromDark, + createFromLight, + getSizeValue, +}; + +export default Utils; diff --git a/packages/components/src/utils/color.ts b/packages/components/src/utils/color.ts new file mode 100644 index 00000000..36629845 --- /dev/null +++ b/packages/components/src/utils/color.ts @@ -0,0 +1,37 @@ +/* eslint-disable no-bitwise */ + +const hexToRgb = (color: string): [number, number, number] => { + const fullReg = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + const full = color.replace(fullReg, (_, r, g, b) => `${r}${r}${g}${g}${b}${b}`); + const values = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(full); + if (!values) { + throw new Error(`Kubed: Unsupported ${color} color.`); + } + return [ + Number.parseInt(values[1], 16), + Number.parseInt(values[2], 16), + Number.parseInt(values[3], 16), + ]; +}; + +export const colorToRgbValues = (color: string) => { + if (color.charAt(0) === '#') return hexToRgb(color); + + const safeColor = color.replace(/ /g, ''); + const colorType = color.substr(0, 4); + + const regArray = safeColor.match(/\((.+)\)/); + if (!colorType.startsWith('rgb') || !regArray) { + console.log(color); + throw new Error(`Kubed: Only support ["RGB", "RGBA", "HEX"] color.`); + } + + return regArray[1].split(',').map((str) => Number.parseFloat(str)); +}; + +export const addColorAlpha = (color: string, alpha: number) => { + if (!/^#|rgb|RGB/.test(color)) return color; + const [r, g, b] = colorToRgbValues(color); + const safeAlpha = alpha > 1 ? 1 : alpha < 0 ? 0 : alpha; + return `rgba(${r}, ${g}, ${b}, ${safeAlpha})`; +}; diff --git a/packages/components/src/utils/forwardRef.tsx b/packages/components/src/utils/forwardRef.tsx new file mode 100644 index 00000000..29145623 --- /dev/null +++ b/packages/components/src/utils/forwardRef.tsx @@ -0,0 +1,18 @@ +/** + * All credit goes to Chance (Reach UI), Haz (Reakit) and (fluentui) + * for creating the base type definitions upon which we improved on + */ + +import React from 'react'; +import { As, ComponentWithAs, PropsOf, RightJoinProps } from './types'; + +export default function forwardRef( + component: React.ForwardRefRenderFunction< + any, + RightJoinProps, Props> & { + as?: As; + } + > +) { + return React.forwardRef(component) as unknown as ComponentWithAs; +} diff --git a/packages/components/src/utils/toArray.ts b/packages/components/src/utils/toArray.ts new file mode 100644 index 00000000..1e86d6ab --- /dev/null +++ b/packages/components/src/utils/toArray.ts @@ -0,0 +1,29 @@ +import React from 'react'; +import { isFragment } from 'react-is'; + +export interface Option { + keepEmpty?: boolean; +} + +export default function toArray( + children: React.ReactNode, + option: Option = {} +): React.ReactElement[] { + let ret: React.ReactElement[] = []; + + React.Children.forEach(children, (child: any) => { + if ((child === undefined || child === null) && !option.keepEmpty) { + return; + } + + if (Array.isArray(child)) { + ret = ret.concat(toArray(child)); + } else if (isFragment(child) && child.props) { + ret = ret.concat(toArray(child.props.children, option)); + } else { + ret.push(child); + } + }); + + return ret; +} diff --git a/packages/components/src/utils/types.ts b/packages/components/src/utils/types.ts new file mode 100644 index 00000000..8b64df46 --- /dev/null +++ b/packages/components/src/utils/types.ts @@ -0,0 +1,57 @@ +import React from 'react'; + +export type DeepPartial = { + [P in keyof T]?: T[P] extends Record ? DeepPartial : T[P]; +}; + +export type ComponentPassThrough = Props & + React.ComponentPropsWithoutRef & { + /** Element or component that will be used as root element */ + component?: T; + }; + +export type As = React.ElementType; + +/** + * Extract the props of a React element or component + */ +export type PropsOf = React.ComponentPropsWithoutRef & { + as?: As; +}; + +export type OmitCommonProps = Omit< + Target, + 'transition' | 'as' | 'color' | OmitAdditionalProps +>; + +export type RightJoinProps< + SourceProps extends object = {}, + OverrideProps extends object = {} +> = OmitCommonProps & OverrideProps; + +export type MergeWithAs< + ComponentProps extends object, + AsProps extends object, + AdditionalProps extends object = {}, + AsComponent extends As = As +> = RightJoinProps & + RightJoinProps & { + as?: AsComponent; + }; + +export type ComponentWithAs = { + ( + props: MergeWithAs< + React.ComponentProps, + React.ComponentProps, + Props, + AsComponent + > + ): JSX.Element; + + displayName?: string; + propTypes?: React.WeakValidationMap; + contextTypes?: React.ValidationMap; + defaultProps?: Partial; + id?: string; +}; diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json new file mode 100644 index 00000000..01cd6ac9 --- /dev/null +++ b/packages/components/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["./src", "./demos"], + "compilerOptions": { + "rootDir": ".", + "baseUrl": ".", + "outDir": "lib", + "declaration": true, + "declarationMap": true, + "declarationDir": "lib", + "composite": true, + "paths": { + "@kubed/*": ["packages/*/src"], + } + }, + "references": [{ "path": "../hooks" }] +} diff --git a/yarn.lock b/yarn.lock index 4b437cc8..28aa0221 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1028,7 +1028,7 @@ core-js-pure "^3.16.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.15.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b" integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA== @@ -1530,7 +1530,7 @@ schema-utils "^2.6.5" source-map "^0.7.3" -"@popperjs/core@^2.5.4", "@popperjs/core@^2.6.0": +"@popperjs/core@^2.5.4", "@popperjs/core@^2.6.0", "@popperjs/core@^2.8.3": version "2.9.3" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.3.tgz#8b68da1ebd7fc603999cf6ebee34a4899a14b88e" integrity sha512-xDu17cEfh7Kid/d95kB6tZsLOmSWKCZKtprnhVepjsSaCij+lM3mItSJDuuHDMbCWTh8Ejmebwb+KONcCJ0eXQ== @@ -2181,6 +2181,13 @@ "@types/react-test-renderer" ">=16.9.0" react-error-boundary "^3.1.0" +"@tippyjs/react@^4.2.5": + version "4.2.5" + resolved "https://registry.yarnpkg.com/@tippyjs/react/-/react-4.2.5.tgz#9b5837db93a1cac953962404df906aef1a18e80d" + integrity sha512-YBLgy+1zznBNbx4JOoOdFXWMLXjBh9hLPwRtq3s8RRdrez2l3tPBRt2m2909wZd9S1KUeKjOOYYsnitccI9I3A== + dependencies: + tippy.js "^6.3.1" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -4933,6 +4940,14 @@ dom-converter@^0.2.0: dependencies: utila "~0.4" +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -9892,6 +9907,13 @@ react-fast-compare@^3.0.1, react-fast-compare@^3.2.0: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== +react-feather@^2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-2.0.9.tgz#6e42072130d2fa9a09d4476b0e61b0ed17814480" + integrity sha512-yMfCGRkZdXwIs23Zw/zIWCJO3m3tlaUvtHiXlW+3FH7cIT6fiK1iJ7RJWugXq7Fso8ZaQyUm92/GOOHXvkiVUw== + dependencies: + prop-types "^15.7.2" + react-helmet-async@^1.0.7: version "1.1.2" resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.1.2.tgz#653b7e6bbfdd239c5dcd6b8df2811c7a363b8334" @@ -10008,7 +10030,7 @@ react-test-renderer@^17.0.0: react-shallow-renderer "^16.13.1" scheduler "^0.20.2" -react-textarea-autosize@^8.3.0: +react-textarea-autosize@^8.3.0, react-textarea-autosize@^8.3.2: version "8.3.3" resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.3.tgz#f70913945369da453fd554c168f6baacd1fa04d8" integrity sha512-2XlHXK2TDxS6vbQaoPbMOfQ8GK7+irc2fVK6QFIcC8GOnH3zI/v481n+j1L0WaPVvKxwesnY93fEfH++sus2rQ== @@ -10017,6 +10039,16 @@ react-textarea-autosize@^8.3.0: use-composed-ref "^1.0.0" use-latest "^1.0.0" +react-transition-group@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" + integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" @@ -11409,6 +11441,13 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tippy.js@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.1.tgz#3788a007be7015eee0fd589a66b98fb3f8f10181" + integrity sha512-JnFncCq+rF1dTURupoJ4yPie5Cof978inW6/4S6kmWV7LL9YOSEVMifED3KdrVPEG+Z/TFH2CDNJcQEfaeuQww== + dependencies: + "@popperjs/core" "^2.8.3" + tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" From 5e0d48b3afa23b8f3a6f361410d35fd71bdea4cb Mon Sep 17 00:00:00 2001 From: chenz24 Date: Fri, 27 Aug 2021 14:27:01 +0800 Subject: [PATCH 2/4] feat: Add Group components --- packages/components/src/Group/Group.tsx | 97 +++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 packages/components/src/Group/Group.tsx diff --git a/packages/components/src/Group/Group.tsx b/packages/components/src/Group/Group.tsx new file mode 100644 index 00000000..bded0203 --- /dev/null +++ b/packages/components/src/Group/Group.tsx @@ -0,0 +1,97 @@ +import React, { PropsWithChildren, CSSProperties, Children, cloneElement } from 'react'; +import styled, { css } from 'styled-components'; +import cx from 'clsx'; +import { KubedNumberSize, themeUtils } from '../theme'; + +const { getSizeValue } = themeUtils; + +export type GroupPosition = 'right' | 'center' | 'left' | 'apart'; + +export type GroupDirection = 'row' | 'column'; + +const POSITIONS = { + left: 'flex-start', + center: 'center', + right: 'flex-end', + apart: 'space-between', +}; + +const getAlign = (direction, position, grow) => { + if (direction === 'row') { + return css` + align-items: center; + justify-content: ${POSITIONS[position]}; + `; + } + return css` + align-items: ${grow ? 'stretch' : POSITIONS[position]}; + `; +}; + +const getChildStyles = (spacing, grow) => { + return css` + flex-grow: ${grow ? 1 : 0}; + `; +}; + +const GroupContainer = styled('div')` + display: flex; + flex-flow: ${({ direction, noWrap }) => `${direction} ${noWrap ? 'nowrap' : 'wrap'}`}; + ${({ direction, position, grow }) => getAlign(direction, position, grow)}; + gap: ${({ spacing, theme }) => getSizeValue(spacing, theme.layout.spacing)}}; + + .group-child { + ${({ spacing, grow }) => getChildStyles(spacing, grow)}; + } +`; + +export interface GroupProps extends React.ComponentPropsWithoutRef<'div'> { + /** Defines justify-content property */ + position?: GroupPosition; + + /** Defined flex-wrap property */ + noWrap?: boolean; + + /** Defines flex-grow property for each element, true -> 1, false -> 0 */ + grow?: boolean; + + /** Space between elements */ + spacing?: KubedNumberSize; + + /** Defines flex-direction property, row for horizontal, column for vertical */ + direction?: GroupDirection; + + /** Defines align-items css property */ + align?: CSSProperties['alignItems']; +} + +export function Group({ + children, + position = 'left', + grow = false, + noWrap = false, + spacing = 'md', + direction = 'row', + ...rest +}: PropsWithChildren) { + const items = (Children.toArray(children) as React.ReactElement[]).map((child) => + cloneElement(child, { + className: cx('group-child', child.props?.className), + }) + ); + + return ( + + {items} + + ); +} + +Group.displayName = '@kubed/components/Group'; From fec1395ca2025075b86cb2cc019487dbcd14ec08 Mon Sep 17 00:00:00 2001 From: chenz24 Date: Fri, 27 Aug 2021 14:28:17 +0800 Subject: [PATCH 3/4] chore: Setup jest and enzyme --- configs/jest/enzyme.setup.js | 8 ++++++++ configs/jest/jsdom.mocks.js | 13 +++++++++++++ jest.config.js | 14 ++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 configs/jest/enzyme.setup.js create mode 100644 configs/jest/jsdom.mocks.js create mode 100644 jest.config.js diff --git a/configs/jest/enzyme.setup.js b/configs/jest/enzyme.setup.js new file mode 100644 index 00000000..31cd985d --- /dev/null +++ b/configs/jest/enzyme.setup.js @@ -0,0 +1,8 @@ +const Enzyme = require('enzyme'); +const Adapter = require('@wojtekmaj/enzyme-adapter-react-17'); +const React = require('react'); + +// Fix broken layout effects on testing environments +React.useLayoutEffect = React.useEffect; + +Enzyme.configure({ adapter: new Adapter() }); diff --git a/configs/jest/jsdom.mocks.js b/configs/jest/jsdom.mocks.js new file mode 100644 index 00000000..97cb9943 --- /dev/null +++ b/configs/jest/jsdom.mocks.js @@ -0,0 +1,13 @@ +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..6612a3ac --- /dev/null +++ b/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + transform: { + // https://github.com/aelbore/esbuild-jest/issues/21 + '^.+\\.tsx?$': '@sucrase/jest-plugin', + }, + testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'], + setupFilesAfterEnv: [ + './configs/jest/enzyme.setup.js', + './configs/jest/jsdom.mocks.js', + ], + moduleNameMapper: { + '@kubed/(.*)': '/packages/$1/src', + }, +}; From 5636f8186a0df3e655979dec62216c0359c66e83 Mon Sep 17 00:00:00 2001 From: chenz24 Date: Fri, 27 Aug 2021 14:32:46 +0800 Subject: [PATCH 4/4] chore: Setup storybook --- configs/storybook/main.js | 31 +++++++++++++++++++++++++++++ configs/storybook/preview-head.html | 12 +++++++++++ configs/storybook/preview.tsx | 13 ++++++++++++ packages/components/src/index.ts | 3 +++ scripts/storybook-start.ts | 8 ++++++++ 5 files changed, 67 insertions(+) create mode 100644 configs/storybook/main.js create mode 100644 configs/storybook/preview-head.html create mode 100644 configs/storybook/preview.tsx create mode 100644 packages/components/src/index.ts create mode 100644 scripts/storybook-start.ts diff --git a/configs/storybook/main.js b/configs/storybook/main.js new file mode 100644 index 00000000..1a790def --- /dev/null +++ b/configs/storybook/main.js @@ -0,0 +1,31 @@ +/* eslint-disable no-param-reassign */ +const path = require('path'); +const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin').default; + +module.exports = { + stories: [ + path.resolve(__dirname, '../../packages/**/*.story.@(ts|tsx)').replace(/\\/g, '/'), + // path.resolve(__dirname, './stories.tsx').replace(/\\/g, '/'), + ], + addons: ['storybook-addon-turbo-build', '@storybook/addon-docs'], + typescript: { + reactDocgen: false, + }, + webpackFinal: async (config) => { + config.resolve = { + ...config.resolve, + plugins: [ + ...(config.resolve.plugins || []), + new TsconfigPathsPlugin({ + extensions: ['.ts', '.tsx', '.js'], + configFile: path.join(__dirname, '../../tsconfig.json'), + }), + ], + }; + + // Turn off docgen plugin as it breaks bundle with displayName + config.plugins.pop(); + + return config; + }, +}; diff --git a/configs/storybook/preview-head.html b/configs/storybook/preview-head.html new file mode 100644 index 00000000..06d637de --- /dev/null +++ b/configs/storybook/preview-head.html @@ -0,0 +1,12 @@ + + + + + + diff --git a/configs/storybook/preview.tsx b/configs/storybook/preview.tsx new file mode 100644 index 00000000..fcb07d49 --- /dev/null +++ b/configs/storybook/preview.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { CssBaseline, KubedConfigProvider } from '../../packages/components/src/index'; + +export const parameters = {}; + +export const decorators = [ + (Story) => ( + + + + + ), +]; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts new file mode 100644 index 00000000..815f02f0 --- /dev/null +++ b/packages/components/src/index.ts @@ -0,0 +1,3 @@ +export { useTheme, themeUtils, CssBaseline } from './theme'; +export { KubedConfigProvider } from './ConfigProvider'; +export { Button } from './Button/Button'; diff --git a/scripts/storybook-start.ts b/scripts/storybook-start.ts new file mode 100644 index 00000000..d9a00287 --- /dev/null +++ b/scripts/storybook-start.ts @@ -0,0 +1,8 @@ +import path from 'path'; +import storybook from '@storybook/react/standalone'; + +storybook({ + port: 7000, + mode: 'dev', + configDir: path.join(__dirname, '../configs/storybook'), +});