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/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/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',
+ },
+};
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', () => (
+ }>
+ KubeSphere
+
+));
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/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';
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/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/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'),
+});
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"