├── .babelrc ├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .prettierrc ├── .storybook ├── addons.js ├── config.js ├── theme.js └── webpack.config.js ├── README.md ├── jest.config.js ├── package.json ├── src ├── common │ ├── TestUtil.tsx │ ├── _utils │ │ ├── __mocks__ │ │ │ └── fileMock.ts │ │ ├── __tests__ │ │ │ └── utils.test.ts │ │ └── index.ts │ ├── assets │ │ └── fonts │ │ │ ├── Inter-Black.woff │ │ │ ├── Inter-BlackItalic.woff │ │ │ ├── Inter-Bold.woff │ │ │ ├── Inter-BoldItalic.woff │ │ │ ├── Inter-ExtraBold.woff │ │ │ ├── Inter-ExtraBoldItalic.woff │ │ │ ├── Inter-ExtraLight-BETA.woff │ │ │ ├── Inter-ExtraLightItalic-BETA.woff │ │ │ ├── Inter-Italic.woff │ │ │ ├── Inter-Light-BETA.woff │ │ │ ├── Inter-LightItalic-BETA.woff │ │ │ ├── Inter-Medium.woff │ │ │ ├── Inter-MediumItalic.woff │ │ │ ├── Inter-Regular.woff │ │ │ ├── Inter-SemiBold.woff │ │ │ ├── Inter-SemiBoldItalic.woff │ │ │ ├── Inter-Thin-BETA.woff │ │ │ └── Inter-ThinItalic-BETA.woff │ ├── styles │ │ ├── GlobalStyles.tsx │ │ ├── ThemeProvider.stories.tsx │ │ ├── ThemeProvider.tsx │ │ ├── __test__ │ │ │ ├── ThemeProvider.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── ThemeProvider.test.tsx.snap │ │ ├── fonts.tsx │ │ ├── index.tsx │ │ ├── palette.tsx │ │ └── variables.tsx │ └── types.ts ├── components │ ├── Alerts │ │ ├── Alert.tsx │ │ ├── AlertContext.ts │ │ ├── Alerts.README.md │ │ ├── Alerts.stories.tsx │ │ ├── AlertsProvider.tsx │ │ ├── StyledAlert.tsx │ │ ├── StyledAlerts.tsx │ │ ├── TransitionWrapper.tsx │ │ ├── __test__ │ │ │ ├── Alerts.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── Alerts.test.tsx.snap │ │ ├── index.tsx │ │ ├── types.ts │ │ ├── useAlerts.ts │ │ └── withAlerts.tsx │ ├── Avatar │ │ ├── Avatar.stories.tsx │ │ ├── Avatar.tsx │ │ ├── README.md │ │ ├── __test__ │ │ │ ├── Avatar.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── Avatar.test.tsx.snap │ │ ├── index.ts │ │ └── types.ts │ ├── Breadcrumb │ │ ├── Breadcrumb.README.md │ │ ├── Breadcrumb.stories.tsx │ │ ├── Breadcrumb.tsx │ │ ├── BreadcrumbItem.tsx │ │ ├── __tests__ │ │ │ ├── Breadcrumb.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── Breadcrumb.test.tsx.snap │ │ ├── index.tsx │ │ └── types.ts │ ├── Button │ │ ├── Button.stories.tsx │ │ ├── ButtonGroup.tsx │ │ ├── ButtonWrapper.tsx │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── Button.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── Button.test.tsx.snap │ │ ├── components │ │ │ ├── ButtonBase.tsx │ │ │ ├── ButtonContent.tsx │ │ │ ├── ButtonInset.tsx │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── types.ts │ ├── Dropdown │ │ ├── Dropdown.stories.tsx │ │ ├── Dropdown.styles.tsx │ │ ├── Dropdown.tsx │ │ ├── README.md │ │ └── index.ts │ ├── Extractor │ │ ├── DataStructure.ts │ │ ├── DownSelect.tsx │ │ ├── Dropdown.tsx │ │ ├── Expression.tsx │ │ ├── __tests__ │ │ │ ├── Extractor.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── Extractor.test.tsx.snap │ │ ├── helpers.ts │ │ ├── index.tsx │ │ ├── stories │ │ │ └── Extractor.stories.tsx │ │ └── types.ts │ ├── Icon │ │ ├── Icon.stories.tsx │ │ ├── Icon.tsx │ │ ├── Icons.tsx │ │ └── index.tsx │ ├── Input │ │ ├── Input.stories.tsx │ │ ├── Input.tsx │ │ ├── InputAddons.tsx │ │ ├── InputRef.tsx │ │ ├── README.md │ │ ├── RenderInput.tsx │ │ ├── __tests__ │ │ │ ├── Input.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── Input.test.tsx.snap │ │ ├── index.tsx │ │ ├── styledComponents.tsx │ │ ├── types.ts │ │ └── utils.ts │ ├── Label │ │ ├── InlineLabel.tsx │ │ ├── Label.stories.tsx │ │ ├── Label.tsx │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── Label.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── Label.test.tsx.snap │ │ ├── commonUtils.ts │ │ ├── index.ts │ │ └── types.ts │ ├── Link │ │ ├── Link.stories.tsx │ │ ├── Link.styles.tsx │ │ ├── Link.tsx │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── Link.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── Link.test.tsx.snap │ │ ├── index.tsx │ │ └── types.ts │ ├── Loaders │ │ ├── IconLoader.stories.tsx │ │ ├── IconLoader.tsx │ │ ├── README.md │ │ └── index.tsx │ ├── Modal │ │ ├── Modal.stories.tsx │ │ ├── ModalWrapper.tsx │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── Modal.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── Modal.test.tsx.snap │ │ ├── components │ │ │ ├── Footer.tsx │ │ │ ├── Header.tsx │ │ │ ├── Main.tsx │ │ │ ├── ScrollWrapper.tsx │ │ │ └── index.ts │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── types.ts │ │ ├── variants │ │ │ ├── Base.tsx │ │ │ ├── BottomPanel.tsx │ │ │ ├── LeftPanel.tsx │ │ │ ├── RightPanel.tsx │ │ │ ├── index.ts │ │ │ ├── styledComponents.ts │ │ │ └── types.ts │ │ └── wrappers │ │ │ ├── Body.tsx │ │ │ ├── Footer.tsx │ │ │ ├── Header.tsx │ │ │ ├── Panel.tsx │ │ │ └── index.ts │ ├── OmniSearch │ │ ├── OmniSearch.stories.tsx │ │ ├── OmniSearch.tsx │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── OmniSearch.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── OmniSearch.test.tsx.snap │ │ ├── index.ts │ │ └── types.ts │ ├── Option │ │ ├── Components │ │ │ ├── Option.styles.tsx │ │ │ ├── Option.tsx │ │ │ ├── OptionGroup.tsx │ │ │ └── OptionText.tsx │ │ ├── Option.stories.tsx │ │ ├── README.md │ │ ├── __tests__ │ │ │ ├── Option.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── Option.test.tsx.snap │ │ ├── index.tsx │ │ └── types.ts │ ├── Radio │ │ ├── Interface.ts │ │ ├── Radio.stories.tsx │ │ ├── Radio.tsx │ │ ├── RadioGroup.tsx │ │ ├── RadioWrapper.tsx │ │ ├── __tests__ │ │ │ ├── Radio.test.jsx │ │ │ └── __snapshots__ │ │ │ │ └── Radio.test.jsx.snap │ │ ├── index.tsx │ │ └── styledRadio.tsx │ ├── Shimmer │ │ ├── README.md │ │ ├── Shimmer.stories.tsx │ │ ├── Shimmer.tsx │ │ ├── __tests__ │ │ │ ├── Shimmer.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── Shimmer.test.tsx.snap │ │ └── index.tsx │ ├── Switch │ │ ├── Switch.stories.tsx │ │ ├── Switch.tsx │ │ ├── index.tsx │ │ └── styledComponents.tsx │ ├── Tabs │ │ ├── README.md │ │ ├── TabItem.tsx │ │ ├── Tabs.stories.tsx │ │ ├── Tabs.tsx │ │ ├── TabsList.tsx │ │ ├── index.ts │ │ ├── styles.tsx │ │ ├── types.ts │ │ └── utils.ts │ └── index.tsx └── index.ts ├── tsconfig.json ├── tsconfig.test.json ├── tslint.json ├── typings └── index.d.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-typescript", 4 | "@babel/preset-react", 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "modules": "commonjs", 9 | "targets": { 10 | "browsers": "last 2 versions" 11 | } 12 | } 13 | ] 14 | ], 15 | "plugins": [ 16 | [ 17 | "babel-plugin-styled-components", 18 | { 19 | "pure": true 20 | } 21 | ], 22 | "@babel/plugin-proposal-class-properties" 23 | ], 24 | "env": { 25 | "es": { 26 | "presets": [ 27 | "@babel/preset-typescript", 28 | "@babel/preset-react", 29 | [ 30 | "@babel/preset-env", 31 | { 32 | "modules": "false", 33 | "targets": { 34 | "browsers": "last 2 versions" 35 | } 36 | } 37 | ] 38 | ], 39 | "plugins": [ 40 | [ 41 | "babel-plugin-styled-components", 42 | { 43 | "pure": true 44 | } 45 | ], 46 | "@babel/plugin-proposal-class-properties", 47 | "@babel/plugin-transform-runtime" 48 | ] 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: knit-ui 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Tests 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Install dependencies 27 | run: yarn 28 | 29 | - name: Build components 30 | run: yarn build 31 | 32 | - name: Run tests 33 | run: yarn test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | .ipr 4 | .out 5 | .iws 6 | *~ 7 | ~* 8 | *.diff 9 | *.patch 10 | *.bak 11 | .DS_Store 12 | Thumbs.db 13 | .project 14 | .*proj 15 | .svn/ 16 | *.swp 17 | *.swo 18 | *.log 19 | *.log.* 20 | *.json.gzip 21 | node_modules/ 22 | .buildpath 23 | .settings 24 | npm-debug.log 25 | nohup.out 26 | dist 27 | lib 28 | es 29 | config/base.yaml 30 | /.vscode/ 31 | /coverage 32 | /build 33 | /.history -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "bracketSpacing": true, 4 | "jsxBracketSameLine": true, 5 | "semi": false 6 | } 7 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-actions/register" 2 | import "storybook-readme/register" 3 | import '@storybook/addon-knobs/register'; 4 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { addParameters, configure, addDecorator } from "@storybook/react" 2 | import { withInfo } from "@storybook/addon-info" 3 | import { addReadme, configureReadme } from "storybook-readme" 4 | import React from "react" 5 | import theme from "./theme" 6 | import { GlobalStyles, ThemeProvider } from "../src/common/styles" 7 | 8 | // automatically import all files ending in *.stories.tsx 9 | const req = require.context("../src/", true, /.stories.tsx$/) 10 | 11 | const ThemeProviderDecorator = storyFn => ( 12 | {storyFn()} 13 | ) 14 | 15 | const GlobalStylesDecorator = storyFn => ( 16 | <> 17 | 18 | {storyFn()} 19 | 20 | ) 21 | 22 | addParameters({ 23 | options: { 24 | name: "KnitUI", 25 | theme, 26 | }, 27 | }) 28 | 29 | function loadStories() { 30 | req.keys().forEach(req) 31 | } 32 | 33 | addDecorator(withInfo) 34 | addDecorator(ThemeProviderDecorator) 35 | addDecorator(GlobalStylesDecorator) 36 | addDecorator(addReadme) 37 | configureReadme({ 38 | StoryPreview: ({ children }) => ( 39 |
{children}
40 | ), 41 | }), 42 | configure(loadStories, module) 43 | -------------------------------------------------------------------------------- /.storybook/theme.js: -------------------------------------------------------------------------------- 1 | import { create } from "@storybook/theming"; 2 | 3 | export default create({ 4 | // Is this a 'light' or 'dark' theme? 5 | base: "dark", 6 | 7 | // Color palette 8 | colorPrimary: "red", // primary color 9 | colorSecondary: "#0088FF", // secondary color 10 | 11 | // UI 12 | appBg: "#262626", 13 | appContentBg: "white", 14 | appBorderColor: "rgba(0,0,0,.1)", 15 | appBorderRadius: 4, 16 | 17 | // Fonts 18 | fontBase: '"Inter", "Helvetica", Arial, sans-serif', 19 | fontCode: "Monaco, monospace", 20 | 21 | // Text colors 22 | textColor: "#b1b1b1", 23 | textInverseColor: "#333333" 24 | }); 25 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ config, mode }) => { 2 | config.module.rules.push({ 3 | test: /\.(ts|tsx)$/, 4 | loader: require.resolve("babel-loader"), 5 | options: { 6 | presets: [["react-app", { flow: false, typescript: true }]], 7 | }, 8 | }) 9 | config.module.rules.push({ 10 | test: /\.(ts|tsx)$/, 11 | loader: require.resolve("react-docgen-typescript-loader"), 12 | }) 13 | config.resolve.extensions.push(".ts", ".tsx") 14 | return config 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # knit.design 2 | 3 | Storybook : [Link](https://knitui.design) 4 | 5 | Roadmap: https://www.figma.com/file/o3UDrumhtDT0huZEsPnGPw/KnitUI-planning?node-id=0%3A1 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "^.+\\.jsx?$": "babel-jest", 4 | "^.+\\.tsx?$": "ts-jest", 5 | }, 6 | moduleNameMapper: { 7 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": 8 | "/src/common/_utils/__mocks__/fileMock.ts", 9 | "\\.(css|less)$": "identity-obj-proxy", 10 | }, 11 | preset: "ts-jest", 12 | globals: { 13 | "ts-jest": { 14 | tsConfig: "./tsconfig.test.json", 15 | }, 16 | }, 17 | setupFilesAfterEnv: [ 18 | "@testing-library/jest-dom/extend-expect", 19 | "jest-styled-components", 20 | ], 21 | modulePathIgnorePatterns: [ 22 | "/build", 23 | "/es", 24 | "/lib", 25 | ], 26 | testPathIgnorePatterns: [ 27 | "/build/", 28 | "/es", 29 | "/lib", 30 | "/node_modules/", 31 | ], 32 | } 33 | -------------------------------------------------------------------------------- /src/common/TestUtil.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/react" 2 | import React, { ReactElement } from "react" 3 | import { ThemeProvider } from "./styles" 4 | 5 | const ProviderWrapper = ({ 6 | children, 7 | }: { 8 | children: ReactElement 9 | }): ReactElement => { 10 | // Nest all providers here 11 | return {children} 12 | } 13 | 14 | const customRender = (ui: ReactElement, options?: object) => 15 | render(ui, { wrapper: ProviderWrapper, ...options }) 16 | 17 | // re-export everything 18 | export * from "@testing-library/react" 19 | // override render method 20 | export { customRender as render } 21 | -------------------------------------------------------------------------------- /src/common/_utils/__mocks__/fileMock.ts: -------------------------------------------------------------------------------- 1 | // To enabel jest to run tests for components that import static 2 | // assets like images and fonts. 3 | // https://jestjs.io/docs/en/webpack#handling-static-assets 4 | 5 | module.exports = "test-file-stub" 6 | -------------------------------------------------------------------------------- /src/common/_utils/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { getFontColor, parseColorPreset, parseCustomColor } from "../index" 2 | import { palette, generateTheme } from "../../../" 3 | const { knitui: theme } = generateTheme(palette) 4 | const chromaPalette = theme.chromaPalette 5 | const { Neutral0, Neutral90 } = chromaPalette 6 | 7 | describe("_utils", () => { 8 | describe("getFontColor", () => { 9 | it("font is white for black background", () => { 10 | const fontColor = getFontColor(theme, chromaPalette.Neutral90) 11 | expect(fontColor.toString()).toBe(Neutral0.toString()) 12 | }) 13 | 14 | it("font is black for white background", () => { 15 | const fontColor = getFontColor(theme, chromaPalette.Neutral0) 16 | expect(fontColor.toString()).toBe(Neutral90.toString()) 17 | }) 18 | 19 | it("font is white for neutral background", () => { 20 | const fontColor = getFontColor(theme, chromaPalette.Blue100) 21 | expect(fontColor.toString()).toBe(Neutral0.toString()) 22 | }) 23 | 24 | it("font is white for success background", () => { 25 | const fontColor = getFontColor(theme, chromaPalette.Neutral90) 26 | expect(fontColor.toString()).toBe(Neutral0.toString()) 27 | }) 28 | 29 | it("font is white for error background", () => { 30 | const fontColor = getFontColor(theme, chromaPalette.Green80) 31 | expect(fontColor.toString()).toBe(Neutral0.toString()) 32 | }) 33 | 34 | it("font is white for unsaved background", () => { 35 | const fontColor = getFontColor(theme, chromaPalette.Magenta80) 36 | expect(fontColor.toString()).toBe(Neutral0.toString()) 37 | }) 38 | 39 | it("font is black for warning background", () => { 40 | const fontColor = getFontColor(theme, chromaPalette.Yellow80) 41 | expect(fontColor.toString()).toBe(Neutral90.toString()) 42 | }) 43 | }) 44 | 45 | describe("parseCustomColor", () => { 46 | it("should return an object with parsed colors", () => { 47 | const backgroundColor = "#000000" 48 | const fontColor = "#ffffff" 49 | const parsed = parseCustomColor(theme, backgroundColor) 50 | expect(parsed.background.toString()).toBe(backgroundColor) 51 | expect(parsed.font.toString()).toBe(fontColor) 52 | }) 53 | }) 54 | 55 | describe("parseColorPreset", () => { 56 | it("should return an object with parsed colors", () => { 57 | const parsed = parseColorPreset(theme, "neutral") 58 | expect(parsed.background.toString()).toBe("#002966") 59 | expect(parsed.font.toString()).toBe("#ffffff") 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-Black.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-BlackItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-BlackItalic.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-Bold.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-BoldItalic.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-ExtraBold.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-ExtraBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-ExtraBoldItalic.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-ExtraLight-BETA.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-ExtraLight-BETA.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-ExtraLightItalic-BETA.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-ExtraLightItalic-BETA.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-Italic.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-Light-BETA.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-Light-BETA.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-LightItalic-BETA.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-LightItalic-BETA.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-Medium.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-MediumItalic.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-Regular.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-SemiBold.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-SemiBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-SemiBoldItalic.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-Thin-BETA.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-Thin-BETA.woff -------------------------------------------------------------------------------- /src/common/assets/fonts/Inter-ThinItalic-BETA.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/common/assets/fonts/Inter-ThinItalic-BETA.woff -------------------------------------------------------------------------------- /src/common/styles/GlobalStyles.tsx: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components" 2 | import { fontsDefinition } from "./fonts" 3 | 4 | const GlobalStyles = createGlobalStyle` 5 | ${fontsDefinition} 6 | 7 | body, 8 | div, 9 | dl, 10 | dt, 11 | dd, 12 | ul, 13 | ol, 14 | li, 15 | h1, 16 | h2, 17 | h3, 18 | h4, 19 | h5, 20 | h6, 21 | pre, 22 | code, 23 | form, 24 | fieldset, 25 | legend, 26 | input, 27 | textarea, 28 | p, 29 | blockquote, 30 | th, 31 | td, 32 | hr, 33 | button, 34 | article, 35 | aside, 36 | details, 37 | figcaption, 38 | figure, 39 | footer, 40 | header, 41 | hgroup, 42 | menu, 43 | nav, 44 | section { 45 | margin: 0; 46 | padding: 0; 47 | } 48 | 49 | body { 50 | overscroll-behavior-x: none; /* Prevent the page redirection on swipe */ 51 | } 52 | 53 | ul, 54 | ol { 55 | list-style: none; 56 | } 57 | 58 | html { 59 | font-size: 10px; 60 | } 61 | 62 | body { 63 | font-family: InterRegular, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 64 | } 65 | 66 | *, *::before, *::after { 67 | box-sizing: border-box; 68 | } 69 | :focus { 70 | outline-color: #0066FF; 71 | } 72 | ` 73 | 74 | export default GlobalStyles 75 | -------------------------------------------------------------------------------- /src/common/styles/ThemeProvider.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import ThemeProvider from "./ThemeProvider" 4 | import * as palette from "./palette" 5 | import { Button } from "../../" 6 | import generateTheme from "./variables" 7 | 8 | const stories = storiesOf("ThemeProvider", module) 9 | 10 | stories.add("Blue100 overright to #1182D4", () => { 11 | const newPalette = { 12 | ...palette, 13 | Blue100: { 14 | hsl: [205, 85, 45], 15 | hex: "#1182D4", 16 | }, 17 | } 18 | const theme = generateTheme(newPalette) 19 | return ( 20 | 21 | 92 | 93 | `; 94 | -------------------------------------------------------------------------------- /src/common/styles/fonts.tsx: -------------------------------------------------------------------------------- 1 | const InterThin = require("../assets/fonts/Inter-Thin-BETA.woff") 2 | const InterLight = require("../assets/fonts/Inter-Light-BETA.woff") 3 | const InterRegular = require("../assets/fonts/Inter-Regular.woff") 4 | const InterMedium = require("../assets/fonts/Inter-Medium.woff") 5 | const InterSemiBold = require("../assets/fonts/Inter-SemiBold.woff") 6 | const InterBold = require("../assets/fonts/Inter-Bold.woff") 7 | const InterBlack = require("../assets/fonts/Inter-Black.woff") 8 | 9 | export const fontsDefinition = ` 10 | @font-face { 11 | font-family: "InterThin"; 12 | src: url(${InterThin}) format('woff'); /*URL to font*/ 13 | } 14 | 15 | @font-face { 16 | font-family: "InterLight"; 17 | src: url(${InterLight}) format('woff'); 18 | } 19 | 20 | @font-face { 21 | font-family: "InterRegular"; 22 | src: url(${InterRegular}) format('woff'); 23 | } 24 | 25 | @font-face { 26 | font-family: "InterMedium"; 27 | src: url(${InterMedium}) format('woff'); 28 | } 29 | 30 | @font-face { 31 | font-family: "InterSemiBold"; 32 | src: url(${InterSemiBold}) format('woff'); 33 | } 34 | 35 | @font-face { 36 | font-family: "InterBold"; 37 | src: url(${InterBold}) format('woff'); 38 | } 39 | 40 | @font-face { 41 | font-family: "InterBlack"; 42 | src: url(${InterBlack}) format('woff'); 43 | } 44 | ` 45 | 46 | export const fontsSource = { 47 | InterThin, 48 | InterLight, 49 | InterRegular, 50 | InterMedium, 51 | InterBold, 52 | InterBlack, 53 | InterSemiBold, 54 | } 55 | -------------------------------------------------------------------------------- /src/common/styles/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as GlobalStyles } from "./GlobalStyles" 2 | export { default as ThemeProvider } from "./ThemeProvider" 3 | import * as palette from "./palette" 4 | export { palette } 5 | export { default as generateTheme } from "./variables" 6 | -------------------------------------------------------------------------------- /src/common/styles/palette.tsx: -------------------------------------------------------------------------------- 1 | /* Primary Palette 2 | --------------- 3 | The primary palette comprises bright and bold range of hues derived from Lapis lazuli. These should be used selectively to highlight the important actions and guide the eye. 4 | */ 5 | 6 | export const Blue100 = { 7 | hsl: [216, 100, 20], 8 | hex: "#002966", 9 | } 10 | 11 | export const Azure80 = { 12 | hsl: [205, 85, 65], 13 | hex: "#5AB2F2", 14 | } 15 | 16 | export const LightBlue40 = { 17 | hsl: [203, 100, 95], 18 | hex: "#E5F5FF", 19 | } 20 | 21 | export const Neutral0 = { 22 | hsl: [0, 0, 100], 23 | hex: "#FFFFFF", 24 | } 25 | 26 | /* Secondary Palette 27 | -------------------- 28 | The secondary pallete comprises composed hues that compliment the primaries well. These provide a warmer tone to the product, and are deeply associated with the state and type of action, for instance, Crimson - danger/failure, Dark Lime - success, Pure Yellow - warning alerts, Moderate magenta - unsaved state. 29 | */ 30 | 31 | export const Crimson80 = { 32 | hsl: [0, 100, 30], 33 | hex: "#990000", 34 | } 35 | 36 | export const Green80 = { 37 | hsl: [118, 100, 20], 38 | hex: "#036600", 39 | } 40 | 41 | export const Yellow80 = { 42 | hsl: [47, 100, 50], 43 | hex: "#FFC700", 44 | } 45 | 46 | export const Magenta80 = { 47 | hsl: [288, 20, 35], 48 | hex: "#64476B", 49 | } 50 | 51 | /* Tertiary Palette 52 | --------------- 53 | The tertiary palette comprises the washed-out hues that are used for highlights. It provides a softer experience and help in easily differentiating between bits. 54 | */ 55 | 56 | export const Beige10 = { 57 | hsl: [42, 38, 95], 58 | hex: "#F7F4ED", 59 | } 60 | 61 | export const Rose10 = { 62 | hsl: [0, 100, 95], 63 | hex: "#FFE3E3", 64 | } 65 | 66 | export const Fern10 = { 67 | hsl: [148, 33, 90], 68 | hex: "#DDEEE5", 69 | } 70 | 71 | export const Sun10 = { 72 | hsl: [40, 92, 90], 73 | hex: "#FDEDCE", 74 | } 75 | 76 | export const Sapphire10 = { 77 | hsl: [217, 80, 90], 78 | hex: "#D1E1FA", 79 | } 80 | 81 | export const Violet10 = { 82 | hsl: [274, 49, 90], 83 | hex: "#E7D9F2", 84 | } 85 | 86 | export const Coral10 = { 87 | hsl: [22, 80, 90], 88 | hex: "#FAE0D1", 89 | } 90 | 91 | export const Wood10 = { 92 | hsl: [33, 35, 85], 93 | hex: "#E6DACB", 94 | } 95 | 96 | /* Neutral Palette 97 | --------------- 98 | The neutrals bring balance to the color palette, and used for actions that either don’t need direct attention or reduce focus from those already taking more than needed. Darker neutrals are used for shadows, borders, etc. 99 | */ 100 | 101 | export const Neutral05 = { 102 | hsl: [0, 0, 97], 103 | hex: "#F7F7F7", 104 | } 105 | 106 | export const Neutral10 = { 107 | hsl: [0, 0, 95], 108 | hex: "#F2F2F2", 109 | } 110 | 111 | export const Neutral20 = { 112 | hsl: [0, 0, 90], 113 | hex: "#E5E5E5", 114 | } 115 | 116 | export const Neutral30 = { 117 | hsl: [0, 0, 85], 118 | hex: "#D9D9D9", 119 | } 120 | 121 | export const Neutral40 = { 122 | hsl: [0, 0, 80], 123 | hex: "#CCCCCC", 124 | } 125 | 126 | export const Neutral45 = { 127 | hsl: [0, 0, 65], 128 | hex: "#A6A6A6", 129 | } 130 | 131 | export const Neutral50 = { 132 | hsl: [0, 0, 50], 133 | hex: "#808080", 134 | } 135 | 136 | export const Neutral60 = { 137 | hsl: [0, 0, 40], 138 | hex: "#666666", 139 | } 140 | 141 | export const Neutral70 = { 142 | hsl: [0, 0, 30], 143 | hex: "#4C4C4C", 144 | } 145 | 146 | export const Neutral80 = { 147 | hsl: [0, 0, 20], 148 | hex: "#333333", 149 | } 150 | 151 | export const Neutral90 = { 152 | hsl: [0, 0, 10], 153 | hex: "#1A1A1A", 154 | } 155 | 156 | export const Azure100 = { 157 | hsl: [205, 85, 40], 158 | hex: "#0F74BD", 159 | } 160 | -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from "react" 2 | 3 | export type ColorPreset = 4 | | "neutral" 5 | | "danger" 6 | | "success" 7 | | "warning" 8 | | "unsaved" 9 | | "regular" 10 | 11 | export type CustomColor = string | { color: string; secondaryColor: string } 12 | 13 | export interface ParsedColorTheme { 14 | background: any 15 | font: any 16 | } 17 | 18 | export type fontSizeType = 10 | 12 | 14 | 16 | 18 | 20 19 | 20 | export interface BaseComponentProps { 21 | /** CSS classname to be added */ 22 | className?: string 23 | /** CSS styles to be adeed */ 24 | style?: CSSProperties 25 | } 26 | 27 | /** 28 | * styled-components will by default apply valid HTML props onto the DOM. 29 | * Refer: https://www.styled-components.com/docs/basics#passed-props 30 | * However in some cases we do use props only for some computation and we 31 | * would not want them to be applied on the DOM. To address this, we use 32 | * add all such props within another property called `customProps`. 33 | */ 34 | export interface IStyled { 35 | customProps: PropType 36 | theme?: any 37 | [htmlProp: string]: any 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Alerts/AlertContext.ts: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { AlertContextInterface } from "./types" 3 | 4 | export default React.createContext(null) 5 | -------------------------------------------------------------------------------- /src/components/Alerts/AlertsProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import { v4 as uuid } from "uuid" 4 | import AlertContext from "./AlertContext" 5 | import TransitionWrapper from "./TransitionWrapper" 6 | import { 7 | TopLeftBox, 8 | TopRightBox, 9 | BottomLeftBox, 10 | BottomRightBox, 11 | } from "./StyledAlerts" 12 | import { AlertProps, placementType, AlertsProviderState } from "./types" 13 | 14 | const PlacementWrapperDiv = { 15 | topLeft: TopLeftBox, 16 | topRight: TopRightBox, 17 | bottomLeft: BottomLeftBox, 18 | bottomRight: BottomRightBox, 19 | } 20 | 21 | class AlertsProvider extends React.Component<{}, AlertsProviderState> { 22 | queue: AlertProps[] = [] 23 | interval: number | undefined 24 | alertMountNode: HTMLDivElement 25 | constructor(props) { 26 | super(props) 27 | 28 | this.state = { 29 | alerts: [], 30 | contextAPI: { 31 | addAlert: this.handleAdd, 32 | removeAlert: this.handleRemove, 33 | }, 34 | } 35 | 36 | this.alertMountNode = document.createElement("div") 37 | this.alertMountNode.classList.add("knitui-alerts-root") 38 | } 39 | 40 | componentDidMount() { 41 | document.body.appendChild(this.alertMountNode) 42 | } 43 | 44 | handleAdd = (alertProps: AlertProps): string => { 45 | alertProps.placement = 46 | alertProps.placement || ("bottomLeft" as placementType) 47 | 48 | // If the key is not provided, generate a new random key. 49 | alertProps.alertKey = alertProps.alertKey || uuid() 50 | 51 | // Closing Alert will call remove method of this class to update state 52 | const curOnClose = alertProps.onClose 53 | alertProps.onClose = key => { 54 | if (curOnClose) { 55 | curOnClose(key) 56 | } 57 | this.handleRemove(alertProps.alertKey!) 58 | } 59 | this.queue.push(alertProps) 60 | this.displayAlert() 61 | return alertProps.alertKey 62 | } 63 | 64 | handleRemove = (key: string): boolean => { 65 | const index = this.state.alerts.findIndex(i => i.alertKey === key) 66 | if (index > -1) { 67 | this.setState({ 68 | alerts: [ 69 | ...this.state.alerts.slice(0, index), 70 | ...this.state.alerts.slice(index + 1), 71 | ], 72 | }) 73 | return true 74 | } 75 | return false 76 | } 77 | 78 | displayAlert() { 79 | this.interval = this.interval 80 | ? this.interval 81 | : setInterval(() => { 82 | if (this.queue.length > 0) { 83 | this.setState(prevState => ({ 84 | alerts: [...prevState.alerts, this.queue.shift()!], 85 | })) 86 | } else { 87 | clearInterval(this.interval) 88 | this.interval = undefined 89 | } 90 | }, 350) 91 | } 92 | 93 | componentWillUnmount() { 94 | clearInterval(this.interval) 95 | document.body.removeChild(this.alertMountNode) 96 | } 97 | 98 | render() { 99 | // alertsByPlace placement keys needs to be predefined, can not use each 100 | // alerts placement prop directly, because for transition of last element, 101 | // Wrapper Component is needed. 102 | const alertsByPlace = { 103 | topLeft: [], 104 | topRight: [], 105 | bottomLeft: [], 106 | bottomRight: [], 107 | } 108 | 109 | this.state.alerts.forEach((alert: AlertProps) => { 110 | const placement = alert.placement as string 111 | alertsByPlace[placement] = [alert, ...alertsByPlace[placement]] 112 | }) 113 | 114 | return ( 115 | 116 | {this.props.children} 117 | {ReactDOM.createPortal( 118 |
119 | {Object.entries(alertsByPlace).map(([placement, alertArr]) => { 120 | const Wrapper = PlacementWrapperDiv[placement] 121 | return ( 122 | 126 | 127 | 128 | ) 129 | })} 130 |
, 131 | this.alertMountNode 132 | )} 133 |
134 | ) 135 | } 136 | } 137 | 138 | export default AlertsProvider 139 | -------------------------------------------------------------------------------- /src/components/Alerts/StyledAlerts.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import { animated } from "react-spring" 3 | 4 | const positionBoxStyle = ` 5 | position: fixed; 6 | overflow: hidden; 7 | display: flex; 8 | pointer-events: none; 9 | > * { 10 | pointer-events: auto; 11 | } 12 | ` 13 | 14 | export const TopLeftBox = styled.div` 15 | ${positionBoxStyle} 16 | left: 0px; 17 | top: 0px; 18 | padding-top: 10px; 19 | padding-left: 10px; 20 | flex-flow: column nowrap; 21 | align-items: flex-start; 22 | ` 23 | 24 | export const TopRightBox = styled.div` 25 | ${positionBoxStyle} 26 | right: 0px; 27 | top: 0px; 28 | padding-top: 10px; 29 | padding-right: 10px; 30 | flex-flow: column nowrap; 31 | align-items: flex-end; 32 | ` 33 | 34 | export const BottomLeftBox = styled.div` 35 | ${positionBoxStyle} 36 | left: 0px; 37 | bottom: 0px; 38 | padding-bottom: 10px; 39 | padding-left: 10px; 40 | flex-flow: column-reverse nowrap; 41 | align-items: flex-start; 42 | ` 43 | 44 | export const BottomRightBox = styled.div` 45 | ${positionBoxStyle} 46 | right: 0px; 47 | bottom: 0px; 48 | padding-bottom: 10px; 49 | padding-right: 10px; 50 | flex-flow: column-reverse nowrap; 51 | align-items: flex-end; 52 | ` 53 | 54 | export const TransitionDiv = styled(animated.div)` 55 | margin: 4px 0; 56 | ` 57 | 58 | export const AlertDiv = styled(animated.div)`` 59 | -------------------------------------------------------------------------------- /src/components/Alerts/TransitionWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { AlertProps } from "./types" 2 | import React, { useState, useEffect } from "react" 3 | import { useTransition, UseTransitionResult } from "react-spring" 4 | import { TransitionDiv, AlertDiv } from "./StyledAlerts" 5 | import Alert from "./Alert" 6 | import { SpringConfig, State } from "react-spring/renderprops" 7 | 8 | // This interface extend Transition's return component with prop have properties Life, 9 | // which is not defined in react-spring's types but implemented in the their code 10 | interface UserTransitionResultWithLife 11 | extends UseTransitionResult { 12 | props: { life: string } 13 | } 14 | //Transition/Animation Using react-spring useTransition 15 | const TransitionWrapper = (props: { alerts: Array }) => { 16 | const config = (_item: AlertProps, state: State): SpringConfig => { 17 | return { 18 | duration: 250, 19 | easing: t => { 20 | if (state === "leave") { 21 | return (t ** 2 + t ** 3) / 2 22 | } else { 23 | return t ** 3 //Bezier Curve (0,0,0,1) 24 | } 25 | }, 26 | } 27 | } 28 | const [alerts, setAlerts] = useState(props.alerts) 29 | const [alertRefMap] = useState(() => new WeakMap()) 30 | 31 | useEffect(() => { 32 | setAlerts(props.alerts) 33 | }, [props.alerts]) 34 | 35 | // The type is defined `object` because react-spring typescript does not define 36 | // config as function with more than one argument, but it's js implementation does. 37 | // real interface is `UseTransition` 38 | const transitionProperties: object = { 39 | from: { height: 0, opacity: 0 }, 40 | enter: (item: AlertProps) => async (next: Function) => 41 | await next({ height: alertRefMap.get(item), opacity: 1 }), 42 | leave: { opacity: 0, height: 0 }, 43 | config, 44 | } 45 | 46 | const transitionProps = useTransition( 47 | alerts, 48 | item => item.alertKey as string, 49 | transitionProperties 50 | ) 51 | 52 | return ( 53 | <> 54 | {transitionProps.map( 55 | ({ 56 | key, 57 | item, 58 | props: { life, ...style }, 59 | }: UserTransitionResultWithLife) => { 60 | return ( 61 | 62 | ref && alertRefMap.set(item, ref.offsetHeight)}> 64 | 65 | 66 | 67 | ) 68 | } 69 | )} 70 | 71 | ) 72 | } 73 | 74 | export default TransitionWrapper 75 | -------------------------------------------------------------------------------- /src/components/Alerts/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC, Component, ComponentClass } from "react" 2 | import Alert from "./Alert" 3 | import AlertsProvider from "./AlertsProvider" 4 | import useAlerts from "./useAlerts" 5 | import withAlerts from "./withAlerts" 6 | 7 | import { AlertProps, useAlertType } from "./types" 8 | 9 | interface IAlertWrapper { 10 | Alert: FC 11 | AlertsProvider: ComponentClass 12 | useAlerts: useAlertType 13 | withAlerts: (component: any) => any 14 | } 15 | 16 | const AlertWrapper: IAlertWrapper = { 17 | Alert, 18 | AlertsProvider, 19 | useAlerts, 20 | withAlerts 21 | } 22 | 23 | export default AlertWrapper 24 | -------------------------------------------------------------------------------- /src/components/Alerts/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode, Component } from "react" 2 | 3 | // Size types of the alert components 4 | export type sizeType = "x-small" | "small" | "medium" | "large" 5 | 6 | // Type of alert in particular which define it's other property like color, icon,... 7 | export type alertType = "neutral" | "warning" | "success" | "danger" | "unsaved" 8 | 9 | // define where will be the placement in the container 10 | export type placementType = 11 | | "topLeft" 12 | | "topRight" 13 | | "bottomLeft" 14 | | "bottomRight" 15 | 16 | // Type of buttons given to have action 17 | export type actionType = { 18 | // Text To be displayed on action button 19 | text: string 20 | // Callback function when action button is Clicked 21 | callback: Function 22 | } 23 | 24 | // Alert Props are attributes which are used to define alert 25 | export interface AlertProps { 26 | // type of the alert 27 | type?: alertType 28 | // Size of the alert, default is small 29 | size?: sizeType 30 | // Main content of the alert 31 | content: ReactNode 32 | // whether alert is to be auto dismissed 33 | autoDismiss?: boolean 34 | // time after which we need to dismiss, effective when auto dismiss is set 35 | dismissDuration?: number 36 | // heading of the alert 37 | heading?: string 38 | // custom icon, effective when mention in React Component or as Prop in object passed to API, 39 | // for use of default icon just pass it empty or undefined 40 | icon?: string 41 | // image url to be shown, overrides icon when both are specified 42 | image?: string 43 | // whether alert is multiline, defaults to false 44 | multiLine?: boolean 45 | // actions of alert, max 2, pass an array of objects with keys as text and callback 46 | actions?: Array 47 | // Function to call once the alert is closed 48 | onClose?: (event) => void 49 | // Function to call while alert is unmounted 50 | onExit?: (key: string) => void 51 | // position on window where it will be displayed 52 | placement?: placementType 53 | // key to reference Element from Alerts Container (help in remove function) 54 | alertKey?: string 55 | // custom className to be passed 56 | className?: string 57 | // prefix classname for custom style 58 | prefixClassName?: string 59 | // custom Color is we don't want to use type presetColor 60 | customColor?: string 61 | } 62 | 63 | export type addAlertType = (props: AlertProps) => string 64 | 65 | export type removeAlertType = (key: string) => boolean 66 | 67 | export interface AlertContextInterface { 68 | // This function will add alert to your provider and return a unique key to reference the alert 69 | addAlert: addAlertType 70 | // This function will remove alert when passed unique key related to each alert 71 | removeAlert: removeAlertType 72 | } 73 | 74 | export interface AlertsProviderState { 75 | alerts: AlertProps[] 76 | contextAPI: AlertContextInterface 77 | } 78 | 79 | export type AlertProviderType = Component<{}, AlertsProviderState> 80 | 81 | export type useAlertType = () => { 82 | addAlert: addAlertType 83 | removeAlert: removeAlertType 84 | } -------------------------------------------------------------------------------- /src/components/Alerts/useAlerts.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | import AlertContext from "./AlertContext" 3 | 4 | // Will be used in functional component (hooks) 5 | export default () => { 6 | const { addAlert, removeAlert } = useContext(AlertContext)! 7 | return { addAlert, removeAlert } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Alerts/withAlerts.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import AlertContext from "./AlertContext" 3 | import hoistNonReactStatics from "hoist-non-react-statics" 4 | import { AlertContextInterface } from "./types" 5 | 6 | function withAlerts(Component: any) { 7 | // Reference for ForwardRef - https://reactjs.org/docs/higher-order-components.html#refs-arent-passed-through 8 | const WrappedComponent = React.forwardRef((props, ref) => ( 9 | 10 | {(context: AlertContextInterface) => ( 11 | 17 | )} 18 | 19 | )) 20 | 21 | // To display name during debuging 22 | if (process.env.NODE_ENV !== "production") { 23 | WrappedComponent.displayName = `WithAlert(${getDisplayName(Component)})` 24 | } 25 | function getDisplayName(Component) { 26 | return Component.displayName || Component.name || "Component" 27 | } 28 | 29 | // Reference https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over 30 | hoistNonReactStatics(WrappedComponent, Component) 31 | 32 | return WrappedComponent 33 | } 34 | 35 | export default withAlerts 36 | -------------------------------------------------------------------------------- /src/components/Avatar/Avatar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import Avatar from "./" 4 | import { 5 | withKnobs, 6 | text, 7 | boolean, 8 | optionsKnob as options, 9 | } from "@storybook/addon-knobs" 10 | import { action } from "@storybook/addon-actions" 11 | const Readme = require("./README.md") 12 | 13 | const stories = storiesOf("Avatar", module) 14 | stories.addDecorator(withKnobs) 15 | 16 | const defaultProps = { 17 | size: text("Size", "24px"), 18 | disabled: boolean("Disabled", false), 19 | onClick: action("button-click"), 20 | } 21 | 22 | const additionalProps = { 23 | picture: text("Picture", "https://i.ibb.co/DwVLSqF/user-profile.jpg"), 24 | name: text("Name", "Jane Doe"), 25 | } 26 | 27 | stories 28 | .addParameters({ 29 | readme: { 30 | // Show readme before story 31 | codeTheme: "shades-of-purple", 32 | // Show readme at the addons panel 33 | sidebar: Readme, 34 | }, 35 | }) 36 | .add("Picture avatar", () => ( 37 | 42 | )) 43 | .add("Name avatar", () => ( 44 | 45 | )) 46 | .add("Unknown user avatar", () => ) 47 | .add("Different size picture avatar", () => ( 48 | 49 | )) 50 | .add("Different size name avatar", () => ( 51 | 52 | )) 53 | .add("Different size icon avatar", () => ( 54 | 55 | )) 56 | .add("Disabled picture avatar", () => ( 57 | 58 | )) 59 | .add("Disabled name avatar", () => ( 60 | 61 | )) 62 | .add("Disabled icon avatar", () => ) 63 | -------------------------------------------------------------------------------- /src/components/Avatar/README.md: -------------------------------------------------------------------------------- 1 | # Avatar 2 | 3 | Avatar are useful to show information of user like user-profile, user-name. (it act as button, so according to usecase, valid to pass onClick or any html button attributes.) 4 | 5 | ## Usage 6 | 7 | ```javascript 8 | import { Avatar } from "KnitUI" 9 | 10 | ) 17 | ``` 18 | 19 | ### Props 20 | 21 | | Prop name | Type | Optional | Default | Description | 22 | | --------- | ---------- | -------- | ----------- | ---------------------------------------------------------------------------------------------- | 23 | | picture | string | Yes | `undefined` | url of the profile picture or any valid component for react img src attribute | 24 | | name | string | Yes | `undefined` | user name which first letter will be shown, if no picture url is passed or falsy picture value | 25 | | size | string | Yes | `24px` | Physical area occupied on the screen, any css valid length parameter | 26 | | disabled | boolean | Yes | `false` | Whether the Avatar should be interactive or not | 27 | | onClick | Function | Yes | None | A click handler to be executed on click of the button. Will receive the `event` as an argument | 28 | | className | string | Yes | None | CSS class name to be added | 29 | | style | CSS Object | Yes | None | CSS style to be added | 30 | -------------------------------------------------------------------------------- /src/components/Avatar/__test__/Avatar.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Avatar from "../" 3 | import { ThemeProvider } from "../../../common/styles" 4 | import { cleanup, fireEvent, render } from "../../../common/TestUtil" 5 | 6 | afterEach(cleanup) 7 | 8 | const defaultProps = { 9 | size: "24px", 10 | disabled: false, 11 | onClick: () => {}, 12 | } 13 | 14 | const additionalProps = { 15 | picture: "https://i.ibb.co/DwVLSqF/user-profile.jpg", 16 | name: "Jane Doe", 17 | } 18 | 19 | describe("Avatar Snapshots", () => { 20 | it("Picture avatar", () => { 21 | const { asFragment } = render( 22 | , 23 | { wrapper: ThemeProvider } 24 | ) 25 | expect(asFragment()).toMatchSnapshot() 26 | }) 27 | it("Name avatar", () => { 28 | const { asFragment, getByText } = render( 29 | , 30 | { wrapper: ThemeProvider } 31 | ) 32 | expect(getByText(additionalProps.name[0])).toBeInTheDocument() 33 | expect(asFragment()).toMatchSnapshot() 34 | }) 35 | it("Unknown user avatar", () => { 36 | const { asFragment } = render(, { 37 | wrapper: ThemeProvider, 38 | }) 39 | expect(asFragment()).toMatchSnapshot() 40 | }) 41 | it("Different size picture avatar", () => { 42 | const { asFragment } = render( 43 | , 44 | { wrapper: ThemeProvider } 45 | ) 46 | expect(asFragment()).toMatchSnapshot() 47 | }) 48 | it("Different size name avatar", () => { 49 | const size = "100px" 50 | const { asFragment, getByTestId } = render( 51 | , 52 | { wrapper: ThemeProvider } 53 | ) 54 | const fontSize = `calc(1.4 * ${size} / 2.4)` 55 | const lineHeight = `calc(2.0 * ${size} / 2.4)` 56 | expect(getByTestId("avatar")).toHaveStyleRule("font-size", fontSize) 57 | expect(getByTestId("avatar")).toHaveStyleRule("line-height", lineHeight) 58 | expect(asFragment()).toMatchSnapshot() 59 | }) 60 | it("Different size icon avatar", () => { 61 | const { asFragment } = render(, { 62 | wrapper: ThemeProvider, 63 | }) 64 | expect(asFragment()).toMatchSnapshot() 65 | }) 66 | it("Disabled picture avatar", () => { 67 | const { asFragment } = render( 68 | , 69 | { wrapper: ThemeProvider } 70 | ) 71 | expect(asFragment()).toMatchSnapshot() 72 | }) 73 | it("Disabled name avatar", () => { 74 | const { asFragment } = render( 75 | , 81 | { wrapper: ThemeProvider } 82 | ) 83 | expect(asFragment()).toMatchSnapshot() 84 | }) 85 | it("Disabled icon avatar", () => { 86 | const { asFragment } = render( 87 | , 88 | { wrapper: ThemeProvider } 89 | ) 90 | expect(asFragment()).toMatchSnapshot() 91 | }) 92 | }) 93 | 94 | describe("Avatar onClick Functionality", () => { 95 | const spyOnClick = jest.spyOn(defaultProps, "onClick") 96 | 97 | afterEach(() => { 98 | spyOnClick.mockReset() 99 | }) 100 | it("passed onClick function get fired when clicked", () => { 101 | const { getByTestId } = render( 102 | , 103 | { wrapper: ThemeProvider } 104 | ) 105 | 106 | fireEvent.click(getByTestId("avatar")) 107 | expect(spyOnClick).toBeCalledTimes(1) 108 | }) 109 | 110 | it("passed onClick function does not get fired when clicked on Disabled Avatar", () => { 111 | const { getByTestId } = render( 112 | , 117 | { wrapper: ThemeProvider } 118 | ) 119 | 120 | fireEvent.click(getByTestId("avatar")) 121 | expect(spyOnClick).toBeCalledTimes(0) 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /src/components/Avatar/index.ts: -------------------------------------------------------------------------------- 1 | import Avatar from "./Avatar" 2 | export default Avatar 3 | -------------------------------------------------------------------------------- /src/components/Avatar/types.ts: -------------------------------------------------------------------------------- 1 | import { SyntheticEvent } from 'react' 2 | 3 | export interface IAvatarProps extends React.ButtonHTMLAttributes { 4 | size?: string 5 | onClick?: (event?: SyntheticEvent) => void 6 | picture?: string 7 | name?: string 8 | disabled?: boolean 9 | } 10 | 11 | export interface IAvatarBaseProps 12 | extends React.ButtonHTMLAttributes { 13 | isPicture: boolean 14 | isName: boolean 15 | isIcon: boolean 16 | size: string 17 | disabled: boolean 18 | } -------------------------------------------------------------------------------- /src/components/Breadcrumb/Breadcrumb.README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Breadcrumb 4 | 5 | ### Usage: 6 | 7 | ##### Default 8 | ```javascript 9 | 10 | Dashboard 11 | Reports 12 | 13 | 50467 14 | 15 | 16 | ``` 17 | 18 | ##### Truncated 19 | 20 | ```javascript 21 | // 2 from end will be shown 22 | Dashboard 23 | Reports 24 | 50467 25 | Dashboard 26 | Reports 27 | 50467 28 | 29 | ``` 30 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/Breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | ReactNode, 3 | ReactElement, 4 | CSSProperties, 5 | cloneElement, 6 | useState, 7 | } from "react" 8 | import styled from "styled-components" 9 | import BreadcrumbItem, { StyledText } from "./BreadcrumbItem" 10 | import { BreadcrumbProps, IBreadCrumb } from "./types" 11 | 12 | const StyledParent = styled.div` 13 | display: flex; 14 | max-width: ${props => props.maxWidth || "initial"}; 15 | flex-wrap: wrap; 16 | width: fit-content; 17 | align-items: center; 18 | ` 19 | 20 | const Breadcrumb: IBreadCrumb = props => { 21 | const { 22 | className, 23 | rootStyle, 24 | maxWidth, 25 | children, 26 | truncateTo = Infinity, 27 | separator, 28 | activeStyles, 29 | childStyle, 30 | } = props 31 | 32 | const [showAll, setShowAll] = useState(false) 33 | 34 | const renderBreadcrumbs = (): ReactNode => { 35 | const updatedChildren = 36 | React.Children.count(children) > 1 37 | ? preprocessCrumbs( 38 | children as ReactNode[], 39 | separator, 40 | truncateTo, 41 | setShowAll, 42 | showAll, 43 | childStyle 44 | ) 45 | : children 46 | const crumbs = React.Children.map( 47 | updatedChildren, 48 | (element: ReactNode, index) => { 49 | if (!element) { 50 | return element 51 | } 52 | return cloneElement(element as ReactElement, { 53 | key: index, 54 | childStyle, 55 | activeStyles, 56 | }) 57 | } 58 | ) 59 | return crumbs 60 | } 61 | 62 | return ( 63 | 67 | {renderBreadcrumbs()} 68 | 69 | ) 70 | } 71 | 72 | // Logic to insert separators among the list of children 73 | const preprocessCrumbs = ( 74 | crumbs: ReactNode[], 75 | separator: ReactNode | string, 76 | truncateTo: number, 77 | setShowAll: Function, 78 | showAll: boolean, 79 | childStyle?: CSSProperties 80 | ) => { 81 | // Container where we insert nodes and separators in appropriate places 82 | const updatedCrumbs: ReactNode[] = [] 83 | // A bool to check whether the ... is inserted or not 84 | let truncated = false 85 | // Handle the range of truncateTo 86 | if (truncateTo < 1) { 87 | truncateTo = 1 88 | console.warn( 89 | `Prop 'truncateTo' has been set to 1 since it's the lowest possible value` 90 | ) 91 | } 92 | crumbs.forEach((crumb: ReactNode, index: number) => { 93 | const shouldInsertCrumbs = 94 | index === 0 || index >= crumbs.length - truncateTo || showAll 95 | if (shouldInsertCrumbs) { 96 | if (index !== crumbs.length - 1) { 97 | updatedCrumbs.push( 98 | cloneElement(crumb as ReactElement, { 99 | childStyle, 100 | }) 101 | ) 102 | } else { 103 | updatedCrumbs.push( 104 | cloneElement(crumb as ReactElement, { 105 | activeElement: true, 106 | childStyle, 107 | }) 108 | ) 109 | } 110 | // Insert nodes till last element 111 | if (index < crumbs.length - 1) { 112 | updatedCrumbs.push( 113 | 114 | {separator} 115 | 116 | ) 117 | } 118 | } else if (!truncated) { 119 | updatedCrumbs.push( 120 | setShowAll(true)}> 121 | ... 122 | 123 | ) 124 | updatedCrumbs.push( 125 | 126 | {separator} 127 | 128 | ) 129 | truncated = true 130 | } 131 | }) 132 | return updatedCrumbs 133 | } 134 | 135 | Breadcrumb.defaultProps = { 136 | separator: "/", 137 | truncateTo: Infinity, 138 | } 139 | 140 | Breadcrumb.Item = BreadcrumbItem 141 | 142 | export default Breadcrumb 143 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/BreadcrumbItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { SFC } from "react" 2 | import styled, { css } from "styled-components" 3 | import { BreadcrumbItemProps } from "./types" 4 | 5 | const TYPOGRAPHY_SIZE = 14 6 | 7 | const sharedStyles = css` 8 | font-size: ${({ theme: { knitui } }) => 9 | `${knitui.typography[TYPOGRAPHY_SIZE].fontSize}rem`}; 10 | line-height: ${({ theme: { knitui } }) => 11 | `${knitui.typography[TYPOGRAPHY_SIZE].lineHeight}rem`}; 12 | border-radius: ${({ theme: { knitui } }) => knitui.inputBorderRadius}; 13 | padding: 0 3px 0 3px; 14 | display: flex; 15 | align-items: center; 16 | &:hover { 17 | background-color: ${({ separator, theme: { knitui } }) => 18 | separator ? "" : knitui.chromaPalette.Neutral10}; 19 | } 20 | cursor: ${props => (props.separator ? "default" : "pointer")}; 21 | a, 22 | * a { 23 | text-decoration: underline; 24 | color: ${({ theme: { knitui } }) => knitui.shades.blue40}; 25 | } 26 | ` 27 | 28 | export const StyledText = styled.span` 29 | ${sharedStyles} 30 | color: ${({ theme: { knitui } }) => knitui.chromaPalette.Neutral50}; 31 | ` 32 | 33 | export const StyledActive = styled.span` 34 | ${sharedStyles} 35 | color: ${({ theme: { knitui } }) => knitui.chromaPalette.Neutral90} 36 | ` 37 | 38 | const BreadcrumbItem: SFC = props => { 39 | const { 40 | separator, 41 | children, 42 | childStyle, 43 | style, 44 | activeStyles, 45 | className, 46 | onClick, 47 | ...restProps 48 | } = props 49 | let link 50 | if ("activeElement" in props) { 51 | link = ( 52 | 57 | {children} 58 | 59 | ) 60 | } else { 61 | link = ( 62 | 67 | {children} 68 | 69 | ) 70 | } 71 | 72 | return link 73 | } 74 | 75 | export default BreadcrumbItem 76 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.tsx: -------------------------------------------------------------------------------- 1 | import Breadcrumb from "./Breadcrumb" 2 | 3 | export default Breadcrumb 4 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode, CSSProperties } from "react" 2 | 3 | export interface BreadcrumbProps { 4 | /** separate to be using between crumbs */ 5 | separator?: string | ReactNode 6 | /** Styles applied to the root */ 7 | rootStyle?: CSSProperties 8 | /** Styles applied to each child */ 9 | childStyle?: CSSProperties 10 | /** Breadcrumb Items in children */ 11 | children?: ReactNode[] | ReactNode 12 | /** className for user to specify their class */ 13 | className?: string 14 | /** Max width of the breadcrumb to be wrapped in */ 15 | maxWidth?: string 16 | /** Level till which the crumbs are to be shown (from end). First is shown anyways */ 17 | truncateTo?: number 18 | /** Styles for the active item */ 19 | activeStyles?: CSSProperties 20 | } 21 | 22 | export interface BreadcrumbItemProps { 23 | /** Link to the route which breadcrumb item should redirect to */ 24 | href?: string 25 | /** Separator to be used */ 26 | separator?: string | ReactNode 27 | /** Custom styles of the breadcrumb item */ 28 | style?: CSSProperties 29 | /** className to be passed to the item */ 30 | className?: string 31 | /** Styling of the active element if any */ 32 | activeStyles?: CSSProperties 33 | /** Styles for all crumbs */ 34 | childStyle?: CSSProperties 35 | /** onClick event */ 36 | onClick?: (event) => void 37 | } 38 | 39 | export interface IBreadCrumb extends React.FC { 40 | Item: React.FC 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Button/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, ReactElement } from "react" 2 | import styled from "styled-components" 3 | import { ButtonBase } from "./components" 4 | import { ButtonGroupProps } from "./types" 5 | 6 | const getStyleForGhostButtons = (props: ButtonGroupWrapperProps) => { 7 | const style = props.isAllGhost 8 | ? ` 9 | & ${ButtonBase}:not(:last-of-type){ 10 | border-right: 0px; 11 | } 12 | & ${ButtonBase}:hover,& ${ButtonBase}:focus{ 13 | border-right: 1px solid; 14 | } 15 | & ${ButtonBase}:hover+${ButtonBase}:focus{ 16 | margin-left: -1px; 17 | } 18 | & ${ButtonBase}:focus { 19 | box-shadow: 0 0 2px; 20 | z-index: 1; 21 | } 22 | & ${ButtonBase}:hover+${ButtonBase}, & ${ButtonBase}:focus+${ButtonBase}{ 23 | border-left: 0px; 24 | } 25 | ` 26 | : ` 27 | & ${ButtonBase}:not(:last-of-type)::after { 28 | content: ""; 29 | display: block; 30 | width: 1px; 31 | } 32 | ` 33 | return style 34 | } 35 | 36 | const StyledVerticalBar = styled.hr` 37 | margin-left: -1px; 38 | border: none; 39 | border-left: 1px solid rgba(255, 255, 255, 0.2); 40 | z-index: 1; 41 | ` 42 | 43 | interface ButtonGroupWrapperProps extends ButtonGroupProps { 44 | isAllGhost: boolean 45 | } 46 | const ButtonGroupWrapper = styled.div` 47 | //To have width equal to all of it's child component. 48 | display: inline-flex; 49 | 50 | & ${ButtonBase} { 51 | border-radius: 0rem; 52 | } 53 | & ${ButtonBase}:first-of-type { 54 | border-top-left-radius: 0.4rem; 55 | border-bottom-left-radius: 0.4rem; 56 | } 57 | & ${ButtonBase}:last-of-type { 58 | border-top-right-radius: 0.4rem; 59 | border-bottom-right-radius: 0.4rem; 60 | } 61 | 62 | // Style will be added if all buttons have ghost property 63 | ${props => getStyleForGhostButtons(props)} 64 | ` 65 | 66 | const ButtonGroup: React.FC = props => { 67 | const { children, ...rest } = props 68 | 69 | const childrenCount = React.Children.count(children) 70 | let countGhost = 0 71 | React.Children.forEach(children, (child: ReactElement, i) => { 72 | if (child.props.ghost) countGhost += 1 73 | }) 74 | 75 | const isAllGhost = countGhost === childrenCount ? true : false 76 | 77 | return ( 78 | 79 | {React.Children.map(props.children, (child: ReactElement, i: Number) => 80 | i == childrenCount - 1 ? ( 81 | child 82 | ) : ( 83 | <> 84 | {child} 85 | {!isAllGhost ? : null} 86 | 87 | ) 88 | )} 89 | 90 | ) 91 | } 92 | 93 | export default ButtonGroup 94 | -------------------------------------------------------------------------------- /src/components/Button/ButtonWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, SyntheticEvent } from "react" 2 | import Icon from "../Icon" 3 | import { ButtonBase, ButtonInset, ButtonContent } from "./components" 4 | import { ThemeContext } from "styled-components" 5 | import { parseCustomColor, parseColorPreset } from "../../common/_utils" 6 | import { ButtonWrapperInterface, ButtonWrapperProps } from "./types" 7 | 8 | const DEFAULT_COLOR_THEME = "neutral" 9 | 10 | const insetPositions = { 11 | LEFT: "left", 12 | RIGHT: "right", 13 | } 14 | 15 | const ButtonWrapper: ButtonWrapperInterface = ({ 16 | label, 17 | ghost = false, 18 | size = "medium", 19 | href, 20 | onClick, 21 | icon, 22 | bare = false, 23 | insetLabel, 24 | insetPosition = "right", 25 | colorPreset = DEFAULT_COLOR_THEME, 26 | customColor, 27 | insetCustomColor, 28 | customProps /** (define just to exclude from rest) In case Button is wrapped in StyledComponent with customProps passed down to manipulate style, 29 | like when Button is used in other components. eg. Alerts, yet no need in Button Component itself*/, 30 | ...rest 31 | }) => { 32 | const themeContext = useContext(ThemeContext) 33 | const { knitui } = themeContext 34 | 35 | // Typography 36 | const typographySize = 37 | size === "small" ? knitui.typography[12] : knitui.typography[14] 38 | const iconSize = size === "small" ? "1.4rem" : "1.8rem" 39 | const baseFontSize = typographySize.fontSize 40 | const baseLineHeight = typographySize.lineHeight 41 | const insetFontSize = baseFontSize - knitui.baseIncrementUnit 42 | const lowerTypographyUnit = knitui.typography[`${insetFontSize * 10}`] 43 | const insetLineHeight = 44 | (lowerTypographyUnit && lowerTypographyUnit.lineHeight) || baseLineHeight 45 | // Colors 46 | 47 | // Cannot be set as default arg since theme is not available in that context 48 | const DEFAULT_INSET_THEME = { 49 | background: knitui.chromaPalette.Neutral0, 50 | font: knitui.chromaPalette.Neutral80, 51 | } 52 | 53 | const insetColorTheme = insetCustomColor 54 | ? parseCustomColor(knitui, insetCustomColor) 55 | : DEFAULT_INSET_THEME 56 | const parsedColorTheme = customColor 57 | ? parseCustomColor(knitui, customColor) 58 | : parseColorPreset(knitui, colorPreset) 59 | return ( 60 | 72 | (onClick && onClick(e)) || (href && window.location.assign(href)) 73 | } 74 | {...rest}> 75 | {insetLabel && insetPosition === insetPositions.LEFT && ( 76 | 85 | {insetLabel} 86 | 87 | )} 88 | 99 | {icon ? : null} 100 | {label} 101 | 102 | {insetLabel && insetPosition === insetPositions.RIGHT && ( 103 | 112 | {insetLabel} 113 | 114 | )} 115 | 116 | ) 117 | } 118 | 119 | import ButtonGroup from "./ButtonGroup" 120 | ButtonWrapper.Group = ButtonGroup 121 | export default ButtonWrapper 122 | -------------------------------------------------------------------------------- /src/components/Button/components/ButtonContent.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import { IStyled } from "../../../common/types" 3 | 4 | import { ButtonProps } from "../types" 5 | 6 | type IStyledBaseButton = IStyled 7 | 8 | const ButtonContent = styled.div` 9 | display: flex; 10 | align-items: center; 11 | width: 100%; 12 | justify-content: center; 13 | ` 14 | 15 | export default ButtonContent 16 | -------------------------------------------------------------------------------- /src/components/Button/components/ButtonInset.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import { IStyled } from "../../../common/types" 3 | 4 | interface ButtonInsetProps { 5 | backgroundColor: string 6 | fontColor: string 7 | fontSize: string 8 | lineHeight: string 9 | insetPosition: string 10 | size: string 11 | } 12 | 13 | const getBorderRadius = position => { 14 | if (position === "right") return "0rem 0.3rem 0.3rem 0" 15 | return "0.3rem 0 0 0.3rem" 16 | } 17 | 18 | const getVerticalMargin = size => { 19 | switch (size) { 20 | case "small": 21 | return "0.1rem" 22 | case "large": 23 | return "0.7rem" 24 | default: 25 | return "0.4rem" 26 | } 27 | } 28 | 29 | const ButtonInset = styled.div>` 30 | display: inline-flex; 31 | background-color: ${({ customProps }) => customProps.backgroundColor}; 32 | color: ${({ customProps }) => customProps.fontColor}; 33 | font-size: ${({ customProps }) => customProps.fontSize}; 34 | line-height: ${({ customProps }) => customProps.lineHeight}; 35 | border-radius: ${({ customProps }) => 36 | getBorderRadius(customProps.insetPosition)}; 37 | margin-left: ${({ customProps }) => 38 | customProps.insetPosition === "right" ? "0.4rem" : 0}; 39 | margin-right: ${({ customProps }) => 40 | customProps.insetPosition === "right" ? 0 : "0.4rem"}; 41 | span { 42 | margin: ${({ customProps }) => getVerticalMargin(customProps.size)} 0.7rem; 43 | line-height: 2rem; 44 | } 45 | ` 46 | 47 | export default ButtonInset 48 | -------------------------------------------------------------------------------- /src/components/Button/components/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as ButtonBase } from "./ButtonBase" 2 | export { default as ButtonInset } from "./ButtonInset" 3 | export { default as ButtonContent } from "./ButtonContent" 4 | -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * We can not directly `export { default as ComponentName } from "./ComponentPath"` 3 | * That will not support { ButtonGroup } to get exported. So instead we are 4 | * doing it in following way 5 | */ 6 | import Button from "./ButtonWrapper" 7 | export default Button 8 | -------------------------------------------------------------------------------- /src/components/Button/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ColorPreset, 3 | CustomColor, 4 | BaseComponentProps, 5 | } from "../../common/types" 6 | import { SyntheticEvent, ReactNode } from "react" 7 | 8 | export interface ParsedColorTheme { 9 | background: any 10 | font: any 11 | insetBackground?: any 12 | insetFont?: any 13 | } 14 | 15 | type TInsetPosition = "left" | "right" 16 | 17 | export interface ButtonBaseProps 18 | extends React.ButtonHTMLAttributes { 19 | /** The text label to be shown on the button */ 20 | label?: string 21 | /** 22 | * One of a set of predefined values that are representative of 23 | * the type of action 24 | */ 25 | ghost?: boolean 26 | /** Physical area occupied on the screen */ 27 | size?: "small" | "medium" | "large" | "fluid" 28 | /** Only text/icon stripping the background */ 29 | bare?: boolean 30 | /** An icon type to be rendered in the button */ 31 | icon?: string 32 | /** An event handler to be called on click of the button */ 33 | onClick?: (event: SyntheticEvent) => void 34 | } 35 | 36 | export interface ButtonWrapperProps extends ButtonBaseProps { 37 | colorPreset?: ColorPreset 38 | /** Override preset colors, should be valid CSS string */ 39 | customColor?: CustomColor 40 | /** Override defaults, should be valid CSS string */ 41 | insetCustomColor?: CustomColor 42 | /** An inset value, typically used for showing notifications */ 43 | insetLabel?: string 44 | /** A location to navigate to on click of the button */ 45 | href?: string 46 | /** Specify the inset label position */ 47 | insetPosition?: TInsetPosition 48 | /** CustomProps, just to avoid, when passed from it's parent, yet no use case here */ 49 | customProps?: any 50 | } 51 | 52 | export interface ButtonProps extends ButtonBaseProps { 53 | colorTheme: ParsedColorTheme 54 | fontSize: number 55 | lineHeight: number 56 | customProps?: any 57 | } 58 | 59 | export interface ButtonGroupProps extends BaseComponentProps { 60 | children: ReactNode 61 | [htmlProp: string]: any 62 | } 63 | 64 | export interface ButtonWrapperInterface extends React.FC { 65 | Group: React.FunctionComponent 66 | } 67 | -------------------------------------------------------------------------------- /src/components/Dropdown/Dropdown.stories.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/components/Dropdown/Dropdown.stories.tsx -------------------------------------------------------------------------------- /src/components/Dropdown/Dropdown.styles.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarisights/KnitUI/46bc49ce60e0b704f5f75b871c6b8a80c7aa5a28/src/components/Dropdown/Dropdown.styles.tsx -------------------------------------------------------------------------------- /src/components/Dropdown/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const Dropdown = () => { 4 | return
Dropppu
5 | } 6 | 7 | export default Dropdown 8 | -------------------------------------------------------------------------------- /src/components/Dropdown/README.md: -------------------------------------------------------------------------------- 1 | # Dropdown 2 | -------------------------------------------------------------------------------- /src/components/Dropdown/index.ts: -------------------------------------------------------------------------------- 1 | import Dropdown from "./Dropdown" 2 | 3 | export default Dropdown 4 | -------------------------------------------------------------------------------- /src/components/Extractor/DataStructure.ts: -------------------------------------------------------------------------------- 1 | import { NodeType, TreeNodeValueType } from "./types" 2 | 3 | export class TreeNode { 4 | value: TreeNodeValueType 5 | children: NodeType[] 6 | constructor(value) { 7 | this.value = value 8 | this.children = [] 9 | } 10 | 11 | addChild(node: NodeType) { 12 | this.children.push(node) 13 | } 14 | 15 | setValue(val) { 16 | this.value = val 17 | } 18 | 19 | clearChildren() { 20 | this.children = [] 21 | } 22 | } 23 | 24 | export class EditorState { 25 | root: NodeType 26 | 27 | constructor(node) { 28 | this.root = node 29 | } 30 | 31 | buildExpression = (node: NodeType = this.root): any => { 32 | let str = "" 33 | if (node.value.type !== "fn") return node.value.data 34 | node.children.forEach((child, idx) => { 35 | str += this.buildExpression(child) 36 | str += idx === node.children.length - 1 ? "" : ", " 37 | }) 38 | return `${node.value.data.label} (${str})` 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Extractor/Expression.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, FC, SyntheticEvent } from "react" 2 | import Drop from "./Dropdown" 3 | import { TreeNode } from "./DataStructure" 4 | import { ExpressionRootPropTypes, OptionType } from "./types" 5 | 6 | const Expression: FC = ( 7 | props: ExpressionRootPropTypes 8 | ) => { 9 | const { 10 | fname, 11 | node, 12 | EditorData, 13 | setExp, 14 | options, 15 | setValue, 16 | onChangeFn, 17 | expressionRootClass = "", 18 | expressionInputClass = "", 19 | validationFn, 20 | } = props 21 | const [rootFocus, setRootFocus] = useState(false) 22 | const expressionRoot = useRef(null) 23 | // find function metadata as per the given key. 24 | const fnData: OptionType | undefined = options.find( 25 | f => f.key === fname.toLowerCase() 26 | ) 27 | 28 | useEffect(() => { 29 | // create nodes for all children of the given function 30 | const { params } = fnData! 31 | params!.forEach(() => { 32 | const refNode = new TreeNode({ data: "", type: "string" }) 33 | node.addChild(refNode) 34 | }) 35 | }, []) 36 | 37 | const findNextNode = () => { 38 | const initNode = (expressionRoot.current as any).firstElementChild 39 | if (initNode.dataset.type === "expression-root") return initNode 40 | return initNode.firstElementChild 41 | } 42 | 43 | const findPrevNode = () => { 44 | let initNode = expressionRoot.current as any 45 | if (initNode.previousElementSibling) { 46 | initNode = initNode.previousElementSibling 47 | while ( 48 | initNode.lastElementChild && 49 | initNode.dataset.type === "expression-root" 50 | ) { 51 | initNode = initNode.lastElementChild 52 | } 53 | initNode = initNode.firstElementChild 54 | } else { 55 | if (initNode.parentElement.dataset.type === "expression-root") 56 | initNode = initNode.parentElement 57 | } 58 | return initNode 59 | } 60 | 61 | const handleKeyDown = (e: React.KeyboardEvent) => { 62 | e.stopPropagation() 63 | // remove node when backspace is pressed and expression is in focus 64 | switch (e.keyCode) { 65 | case 8: 66 | case 46: 67 | if (rootFocus) { 68 | setExp(false) 69 | node.clearChildren() 70 | setValue("") 71 | } 72 | break 73 | case 39: 74 | findNextNode().focus() 75 | break 76 | case 37: 77 | findPrevNode().focus() 78 | break 79 | default: 80 | return 81 | } 82 | } 83 | 84 | // Build the dom with dropdowns for the parameters of the function 85 | const PHDom = () => { 86 | const { params } = fnData! 87 | return params!.map((param, i) => { 88 | return ( 89 | 101 | ) 102 | }) 103 | } 104 | if (fname) { 105 | return ( 106 | setRootFocus(true)} 112 | onBlur={() => setRootFocus(false)} 113 | tabIndex={0} 114 | style={{ display: "flex" }}> 115 | {fnData!.label}({PHDom()}) 116 | 117 | ) 118 | } 119 | return null 120 | } 121 | 122 | export default Expression 123 | -------------------------------------------------------------------------------- /src/components/Extractor/helpers.ts: -------------------------------------------------------------------------------- 1 | export const functions = [ 2 | { 3 | value: "SPLIT (dim, delimiter, occurrence_number)", 4 | label: "SPLIT", 5 | type: "function", 6 | key: "split", 7 | keyLabel: "f(x)", 8 | params: ["dim", "delimiter", "occurrence_number"], 9 | helper: 10 | "Returns the nth substring divided by a specified delimiter. Index, n, starts from 0", 11 | }, 12 | { 13 | value: "CONCAT (dim1, dim2)", 14 | label: "CONCAT", 15 | key: "concat", 16 | params: ["dim1", "dim2"], 17 | type: "function", 18 | keyLabel: "f(x)", 19 | helper: "Returns the concatenation of two strings.", 20 | }, 21 | { 22 | value: "SUB (dim, starting_at, ending_at)", 23 | label: "SUB", 24 | key: "sub", 25 | params: ["dim", "starting_at", "ending_at"], 26 | type: "function", 27 | keyLabel: "f(x)", 28 | helper: 29 | "Returns a substring between specified character indices. Index starts from 0", 30 | }, 31 | { 32 | value: "EXTRACT (dim, prefix_string, suffix_string)", 33 | label: "EXTRACT", 34 | key: "extract", 35 | params: ["dim1", "prefix_string", "suffix_string"], 36 | type: "function", 37 | keyLabel: "f(x)", 38 | helper: 39 | "Returns a substring between the first prefix_string and first suffix_string", 40 | }, 41 | ] 42 | 43 | export const staticValues = [ 44 | { 45 | value: "ACCOUNT", 46 | label: "Account", 47 | type: "dimension", 48 | keyLabel: "dim", 49 | key: "account", 50 | helper: "Account dimension", 51 | }, 52 | { 53 | value: "AD", 54 | label: "Ad", 55 | type: "dimension", 56 | keyLabel: "dim", 57 | key: "ad", 58 | helper: "Account ad", 59 | }, 60 | ] 61 | -------------------------------------------------------------------------------- /src/components/Extractor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Dropdown from "./Dropdown" 3 | import { EditorState, TreeNode } from "./DataStructure" 4 | import styled from "styled-components" 5 | import { getThemeColor } from "../../common/_utils" 6 | import { ExtractorProps, EditorType, NodeType } from "./types" 7 | 8 | const stringRegex = /"([^\\"]|\\")*"/ 9 | 10 | const expressionRootClass = "knit-ui_root-class" 11 | const expressionInputClass = "knit-ui_input-class" 12 | 13 | const validationFn = (val: any): boolean => { 14 | // mock api request 15 | const res = !isNaN(val) || stringRegex.test(val) 16 | // console.log(res) 17 | return res 18 | } 19 | 20 | const ExtractorWrapper = styled.div` 21 | width: 100%; 22 | display: flex; 23 | overflow: auto; 24 | align-items: center; 25 | border: 1px solid ${props => getThemeColor(props, "Azure80")}; 26 | box-sizing: border-box; 27 | scrollbar-width: none; /* Firefox */ 28 | -ms-overflow-style: none; /* IE 10+ */ 29 | &::-webkit-scrollbar { 30 | display: none; 31 | } 32 | 33 | &:focus { 34 | box-shadow: 0 0 0.2rem #0066ff; 35 | } 36 | border-radius: 0.4rem; 37 | height: 3.2rem; 38 | font-size: 1.4rem; 39 | line-height: 2rem; 40 | padding-left: 1.4rem; 41 | input { 42 | border: 0; 43 | outline: 0; 44 | color: ${props => getThemeColor(props, "Neutral90")}; 45 | height: 2rem; 46 | border-radius: 0.4rem; 47 | margin: 0 0.6rem; 48 | background-color: inherit; 49 | padding: 0 0.4rem; 50 | font-size: 1.4rem; 51 | line-height: 2rem; 52 | } 53 | input[data-value-type="dimension"] { 54 | background-color: ${props => getThemeColor(props, "Sun10")}; 55 | } 56 | input[data-valid="false"] { 57 | border-bottom: 0.1rem dashed ${props => getThemeColor(props, "Crimson80")}; 58 | border-radius: 0.2rem; 59 | box-sizing: border-box; 60 | } 61 | input[data-valid="true"] { 62 | border-bottom: 0; 63 | } 64 | [data-type="expression-root"] { 65 | color: ${props => getThemeColor(props, "Green80")}; 66 | box-sizing: border-box; 67 | display: flex; 68 | padding: 0 0.4rem; 69 | &:focus { 70 | outline: 1px dashed ${props => getThemeColor(props, "Azure80")}; 71 | box-sizing: border-box; 72 | background: rgba(209, 225, 250, 0.5); 73 | } 74 | &:not(:last-child):after { 75 | content: ","; 76 | } 77 | } 78 | [data-type="expression-input-root"] { 79 | display: flex; 80 | &:not(:last-child):after { 81 | content: ","; 82 | } 83 | & > ul { 84 | margin-top: 3rem; 85 | border: 1px solid ${props => getThemeColor(props, "Neutral20")}; 86 | box-shadow: 0px 2px 7px rgba(153, 153, 153, 0.3); 87 | border-radius: 4px; 88 | padding: 1.4rem 0; 89 | width: 28rem; 90 | position: absolute; 91 | li { 92 | font-size: 1.4rem; 93 | line-height: 2rem; 94 | display: flex; 95 | justify-content: space-between; 96 | padding: 0.7rem 1.4rem; 97 | &:hover { 98 | background-color: ${props => getThemeColor(props, "Neutral10")}; 99 | } 100 | span { 101 | &:nth-child(2) { 102 | color: ${props => getThemeColor(props, "Neutral45")}; 103 | font-style: italic; 104 | } 105 | &:first-child { 106 | color: ${props => getThemeColor(props, "Green80")}; 107 | } 108 | } 109 | } 110 | } 111 | } 112 | ` 113 | 114 | const Extractor: React.FC = props => { 115 | const { onChangeFn, options } = props 116 | // initialize root node of the expression 117 | const rootNode: NodeType = new TreeNode(null) 118 | // initialize the editor state with root node 119 | const EditorData: EditorType = new EditorState(rootNode) 120 | 121 | return ( 122 | 123 | 133 | 134 | ) 135 | } 136 | 137 | export default Extractor 138 | -------------------------------------------------------------------------------- /src/components/Extractor/stories/Extractor.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import { withKnobs } from "@storybook/addon-knobs" 4 | import Extractor from "../" 5 | import { functions, staticValues } from "../helpers" 6 | 7 | // functions for the extractor 8 | const options = [...functions, ...staticValues] 9 | 10 | const stories = storiesOf("Extractor", module) 11 | stories.addDecorator(withKnobs) 12 | 13 | const onChangeFn = editorState => 14 | console.log(editorState.buildExpression(), editorState) 15 | 16 | stories 17 | .addParameters({ 18 | readme: { 19 | // Show readme before story 20 | codeTheme: "shades-of-purple", 21 | // Show readme at the addons panel 22 | }, 23 | }) 24 | .add("Basic", () => ) 25 | -------------------------------------------------------------------------------- /src/components/Extractor/types.ts: -------------------------------------------------------------------------------- 1 | import { EditorState, TreeNode } from "./DataStructure" 2 | 3 | export interface ExtractorProps { 4 | onChangeFn: (EditorType) => void 5 | options: OptionType[] 6 | } 7 | 8 | export interface EditorType extends EditorState {} 9 | 10 | export interface NodeType extends TreeNode {} 11 | 12 | export interface OptionType { 13 | key: string 14 | type: string 15 | params?: string[] 16 | keyLabel?: string 17 | label: string 18 | } 19 | 20 | export interface TreeNodeValueType { 21 | data: OptionType 22 | type: string 23 | } 24 | 25 | export interface DropdownProps { 26 | inputRef?: React.RefObject 27 | inputPlaceholder?: string 28 | placeholder: string 29 | onKeyDown?: () => void 30 | initialFocus?: boolean 31 | node: NodeType 32 | EditorData: EditorType 33 | handleValueChange?: () => void 34 | options: OptionType[] 35 | validationFn: (val: any) => boolean 36 | inputValue?: any 37 | onChangeFn: (state: EditorType) => void 38 | expressionRootClass: string | undefined 39 | expressionInputClass: string | undefined 40 | } 41 | 42 | export interface SelectorPropTypes { 43 | inputRef: React.RefObject 44 | inputPlaceholder: string 45 | onKeyDown: (e) => void 46 | handleValueChange: (e) => void 47 | options: OptionType[] 48 | validationFn: (value: any) => boolean 49 | inputValue: string 50 | expressionInputClass: string | undefined 51 | } 52 | 53 | export interface ExpressionRootPropTypes { 54 | // function name that matches the input 55 | fname: string 56 | // parent node on which we append child dropdown as per the fn param 57 | node: NodeType 58 | // Editor data which needs to be passed down to every level 59 | EditorData: EditorType 60 | setExp: (isExpression: boolean) => void 61 | options: OptionType[] 62 | setValue: (value: string) => void 63 | onChangeFn: (state: EditorType) => void 64 | expressionRootClass: string | undefined 65 | expressionInputClass: string | undefined 66 | validationFn: (val: string) => boolean 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import Icon from "./index" 4 | import * as Icons from "./Icons" 5 | import { withKnobs, select } from "@storybook/addon-knobs" 6 | 7 | const stories = storiesOf("Icons", module) 8 | stories.addDecorator(withKnobs) 9 | 10 | const sizes = ["16px", "18px", "24px"] 11 | const colors = ["#000000", "#990000", "#036600"] 12 | const allIcons = Object.entries(Icons) 13 | 14 | stories 15 | .add("Icon", () => { 16 | const size = select("Size", sizes, "18px") 17 | const color = select("Color", colors, "#000000") 18 | return 19 | }) 20 | .add("All icons", () => { 21 | const size = select("Size", sizes, "18px") 22 | const color = select("Color", colors, "#000000") 23 | const props = { width: size, height: size, fill: color } 24 | const allIconsDOM = allIcons.map(([iconKey, icon], index) => ( 25 |
30 | {icon(props)} 31 |
32 | )) 33 | return ( 34 |
40 | {allIconsDOM} 41 |
42 | ) 43 | }) 44 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import styled from "styled-components" 3 | import * as Icons from "./Icons" 4 | import { withProps } from "../../common/_utils" 5 | 6 | export interface IIconProps { 7 | type: string 8 | size?: { width: string; height: string } | string 9 | fill?: string 10 | customStyles?: string 11 | } 12 | 13 | interface IIconWrapper { 14 | width: string 15 | height: string 16 | customStyles?: string 17 | } 18 | 19 | const StyledIconWrapper: any = withProps()(styled.span)` 20 | cursor: inherit; 21 | display: inline-block; 22 | height: ${({ height }) => height}; 23 | width: ${({ width }) => width}; 24 | & svg { 25 | ${({ customStyles }) => customStyles}; 26 | } 27 | ` 28 | 29 | const Icon: any = (props: IIconProps) => { 30 | const { type, size, fill, customStyles = "" } = props 31 | let width = "18px", 32 | height = "18px" 33 | if (typeof size === "string") { 34 | width = size 35 | height = size 36 | } else if (typeof size === "object") { 37 | width = size.width 38 | height = size.height 39 | } 40 | 41 | const svgStyles = { width, height, fill } 42 | // If type is not provided, then "oCheckBoxOutlineBlank" is used as placeholder icon 43 | const defaultType = "oCheckBoxOutlineBlank" 44 | const icon = 45 | type && Icons[type] ? Icons[type](svgStyles) : Icons[defaultType](svgStyles) 46 | return ( 47 | 52 | {icon} 53 | 54 | ) 55 | } 56 | 57 | export default Icon 58 | -------------------------------------------------------------------------------- /src/components/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import Icon from "./Icon" 2 | export default Icon 3 | -------------------------------------------------------------------------------- /src/components/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import InputRef from "./InputRef" 4 | 5 | import { IInputProps } from "./types" 6 | 7 | const Input = React.forwardRef((props, ref) => { 8 | return 9 | }) 10 | 11 | export default Input 12 | -------------------------------------------------------------------------------- /src/components/Input/InputAddons.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react" 2 | import _ from "lodash" 3 | 4 | import { IInputProps } from "./types" 5 | import { getIconSize } from "./utils" 6 | 7 | import { 8 | AddonBeforeSpan, 9 | AddonAfterSpan, 10 | AddonContainer, 11 | StyledLabel, 12 | NotificationContainer, 13 | } from "./styledComponents" 14 | 15 | const renderInputAddons = ( 16 | children: React.ReactElement, 17 | props: IInputProps 18 | ) => { 19 | const { 20 | addonAfter, 21 | addonBefore, 22 | label, 23 | notification, 24 | inputSize, 25 | state, 26 | } = props 27 | const customProps = { 28 | inputSize, 29 | state, 30 | } 31 | 32 | let labelDOM: null | ReactNode = null 33 | let notificationDOM: null | ReactNode = null 34 | let addonDOM: ReactNode = children 35 | 36 | if (label) { 37 | labelDOM = label 38 | if (_.isString(label)) { 39 | labelDOM = {label} 40 | } 41 | } 42 | 43 | if (notification) { 44 | notificationDOM = notification 45 | if (_.isString(notification)) { 46 | notificationDOM = ( 47 | 48 | {notification} 49 | 50 | ) 51 | } 52 | } 53 | 54 | if (addonBefore) { 55 | addonDOM = ( 56 | 57 | {addonBefore ? ( 58 | 62 | {addonBefore} 63 | 64 | ) : null} 65 | {children} 66 | 67 | ) 68 | } 69 | if (addonAfter) { 70 | addonDOM = ( 71 | 72 | {children} 73 | {addonAfter ? ( 74 | 78 | {addonAfter} 79 | 80 | ) : null} 81 | 82 | ) 83 | } 84 | return ( 85 | <> 86 | {labelDOM} 87 | {addonDOM} 88 | {notificationDOM} 89 | 90 | ) 91 | } 92 | 93 | export default renderInputAddons 94 | -------------------------------------------------------------------------------- /src/components/Input/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Input 4 | 5 | ### Usage 6 | 7 | ```javascript 8 | import { Input } from "KnitUI" 9 | 10 | const InputWrapper = () => { 11 | const [value, setValue] = useState("") 12 | 13 | return ( 14 |
18 | setValue(e.target.value)} 20 | placeholder="basic usage" 21 | /> 22 |
23 | ) 24 | } 25 | ``` 26 | 27 | ### Props 28 | 29 | | Prop name | Type | Optional | Default | Description | 30 | | -------------- | ------------------------------------------------------------- | -------- | --------- | --------------------------------------------------------------------------- | 31 | | placeholder | string | Yes | None | This is a placeholder description | 32 | | value | string | Yes | None | This is a value of the input | 33 | | state | "success" \| "warning" \| "error" | Yes | None | To enable state of input (success, warning or error) | 34 | | label | string \| ReactNode | Yes | None | content to be shown above the input as a label | 35 | | notification | string \| ReactNode | Yes | None | content to be shown below the input as a notification | 36 | | onChange | Function | Yes | None | onChange handler | 37 | | addonAfter | string \| ReactNode | Yes | None | to show after input | 38 | | addonBefore | string \| ReactNode | Yes | None | to show before input | 39 | | inputSize | "default" \| "small" \| "lage" | Yes | "default" | the size of the inputbutton | 40 | | type | "default" \| "search" \| "number" \| "password" \| "textarea" | Yes | "default" | Type of input field to be displayed (Native HTML input types not supported) | 41 | | disabled | boolean | Yes | None | To disable the input | 42 | | textareaResize | "both" \| "horizontal" \| "vertical" \| "none" | Yes | "both" | To limit the resize for textarea | 43 | 44 | **Note: Default HTML attributes of the input element are supported.** 45 | -------------------------------------------------------------------------------- /src/components/Input/RenderInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import { IInputProps } from "./types" 4 | 5 | interface IRenderInput extends IInputProps { 6 | isOverflow: boolean 7 | } 8 | 9 | import { StyledInput, StyledTextArea } from "./styledComponents" 10 | 11 | const RenderInput = React.forwardRef( 12 | (props, ref) => { 13 | const { 14 | placeholder, 15 | value, 16 | onChange, 17 | state, 18 | label, 19 | inputSize = "default", 20 | type = "default", 21 | textareaResize = "both", 22 | notification, 23 | className, 24 | style, 25 | addonAfter, 26 | addonBefore, 27 | disabled, 28 | ...rest 29 | } = props 30 | const customProps = { 31 | inputSize, 32 | state, 33 | } 34 | 35 | const commonProps = { 36 | ref, 37 | placeholder, 38 | customProps, 39 | className, 40 | style, 41 | value, 42 | onChange, 43 | disabled, 44 | ...rest, 45 | } 46 | 47 | if (props.type === "textarea") { 48 | return 49 | } 50 | 51 | return ( 52 | 62 | ) 63 | } 64 | ) 65 | 66 | export default RenderInput 67 | -------------------------------------------------------------------------------- /src/components/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import Input from "./Input" 2 | export default Input 3 | -------------------------------------------------------------------------------- /src/components/Input/styledComponents.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components" 2 | 3 | import Icon from "../Icon" 4 | 5 | import { IStyledInput } from "./types" 6 | 7 | import { 8 | getPadding, 9 | getInputBorder, 10 | getHeight, 11 | getFontSize, 12 | getLineHeight, 13 | getBackgroundColor, 14 | getBorderRadius, 15 | getColor, 16 | getBoxShadow, 17 | getSelectionBackground, 18 | getAddonPadding, 19 | } from "./utils" 20 | 21 | export const InputWrapper = styled.span` 22 | position: relative; 23 | display: flex; 24 | align-items: center; 25 | outline: none; 26 | ` 27 | 28 | // Common styles for and