aboutsummaryrefslogtreecommitdiffstats
path: root/packages/forms
diff options
context:
space:
mode:
Diffstat (limited to 'packages/forms')
-rw-r--r--packages/forms/.gitignore2
-rw-r--r--packages/forms/package-lock.json220
-rw-r--r--packages/forms/package.json39
-rw-r--r--packages/forms/src/button/index.tsx274
-rw-r--r--packages/forms/src/error/index.tsx29
-rw-r--r--packages/forms/src/error/styles.ts9
-rw-r--r--packages/forms/src/index.ts4
-rw-r--r--packages/forms/src/input/index.tsx190
-rw-r--r--packages/forms/src/input/scorePassword.ts42
-rw-r--r--packages/forms/src/input/styles.ts96
-rw-r--r--packages/forms/src/label/index.tsx47
-rw-r--r--packages/forms/src/label/styles.ts14
-rw-r--r--packages/forms/src/select/index.tsx437
-rw-r--r--packages/forms/src/toggle/index.tsx117
-rw-r--r--packages/forms/src/typings/generic.ts16
-rw-r--r--packages/forms/src/wrapper/index.tsx37
-rw-r--r--packages/forms/src/wrapper/styles.ts5
-rw-r--r--packages/forms/tsconfig.json12
-rw-r--r--packages/forms/tslint.json3
19 files changed, 1593 insertions, 0 deletions
diff --git a/packages/forms/.gitignore b/packages/forms/.gitignore
new file mode 100644
index 000000000..d01826a6b
--- /dev/null
+++ b/packages/forms/.gitignore
@@ -0,0 +1,2 @@
1node_modules/
2lib
diff --git a/packages/forms/package-lock.json b/packages/forms/package-lock.json
new file mode 100644
index 000000000..b5b24c239
--- /dev/null
+++ b/packages/forms/package-lock.json
@@ -0,0 +1,220 @@
1{
2 "name": "@meetfranz/forms",
3 "version": "1.0.6",
4 "lockfileVersion": 1,
5 "requires": true,
6 "dependencies": {
7 "@mdi/js": {
8 "version": "3.3.92",
9 "resolved": "https://registry.npmjs.org/@mdi/js/-/js-3.3.92.tgz",
10 "integrity": "sha512-l+12IwTycHlijWMiRWBAssm0RSgkQiwMthIy/EcBAdSqtnsHnFjHq+aI2MBZ8/AYX0QBxNUv4+EN0SXZgNkWDg=="
11 },
12 "@mdi/react": {
13 "version": "1.1.0",
14 "resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.1.0.tgz",
15 "integrity": "sha512-c0+avMYEZ6i7Pg1ULLFs+p7k8bDPiie9rrgGYs8VWQhw2tUUYz7r0lIPVzD3bzMghWfyhfkArj88K5Of0WTMNw=="
16 },
17 "@meetfranz/theme": {
18 "version": "file:../theme",
19 "requires": {
20 "color": "^3.1.0"
21 },
22 "dependencies": {
23 "color": {
24 "version": "3.1.0",
25 "bundled": true,
26 "requires": {
27 "color-convert": "^1.9.1",
28 "color-string": "^1.5.2"
29 }
30 },
31 "color-convert": {
32 "version": "1.9.3",
33 "bundled": true,
34 "requires": {
35 "color-name": "1.1.3"
36 }
37 },
38 "color-name": {
39 "version": "1.1.3",
40 "bundled": true
41 },
42 "color-string": {
43 "version": "1.5.3",
44 "bundled": true,
45 "requires": {
46 "color-name": "^1.0.0",
47 "simple-swizzle": "^0.2.2"
48 }
49 },
50 "is-arrayish": {
51 "version": "0.3.2",
52 "bundled": true
53 },
54 "simple-swizzle": {
55 "version": "0.2.2",
56 "bundled": true,
57 "requires": {
58 "is-arrayish": "^0.3.1"
59 }
60 }
61 }
62 },
63 "asap": {
64 "version": "2.0.6",
65 "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
66 "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
67 },
68 "core-js": {
69 "version": "1.2.7",
70 "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
71 "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
72 },
73 "create-react-class": {
74 "version": "15.6.3",
75 "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz",
76 "integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==",
77 "requires": {
78 "fbjs": "^0.8.9",
79 "loose-envify": "^1.3.1",
80 "object-assign": "^4.1.1"
81 }
82 },
83 "encoding": {
84 "version": "0.1.12",
85 "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
86 "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
87 "requires": {
88 "iconv-lite": "~0.4.13"
89 }
90 },
91 "fbjs": {
92 "version": "0.8.17",
93 "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
94 "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
95 "requires": {
96 "core-js": "^1.0.0",
97 "isomorphic-fetch": "^2.1.1",
98 "loose-envify": "^1.0.0",
99 "object-assign": "^4.1.0",
100 "promise": "^7.1.1",
101 "setimmediate": "^1.0.5",
102 "ua-parser-js": "^0.7.18"
103 }
104 },
105 "html-element-attributes": {
106 "version": "1.3.1",
107 "resolved": "https://registry.npmjs.org/html-element-attributes/-/html-element-attributes-1.3.1.tgz",
108 "integrity": "sha512-UrRKgp5sQmRnDy4TEwAUsu14XBUlzKB8U3hjIYDjcZ3Hbp86Jtftzxfgrv6E/ii/h78tsaZwAnAE8HwnHr0dPA=="
109 },
110 "iconv-lite": {
111 "version": "0.4.24",
112 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
113 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
114 "requires": {
115 "safer-buffer": ">= 2.1.2 < 3"
116 }
117 },
118 "is-stream": {
119 "version": "1.1.0",
120 "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
121 "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
122 },
123 "isomorphic-fetch": {
124 "version": "2.2.1",
125 "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
126 "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
127 "requires": {
128 "node-fetch": "^1.0.1",
129 "whatwg-fetch": ">=0.10.0"
130 }
131 },
132 "js-tokens": {
133 "version": "4.0.0",
134 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
135 "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
136 },
137 "loose-envify": {
138 "version": "1.4.0",
139 "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
140 "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
141 "requires": {
142 "js-tokens": "^3.0.0 || ^4.0.0"
143 }
144 },
145 "node-fetch": {
146 "version": "1.7.3",
147 "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
148 "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
149 "requires": {
150 "encoding": "^0.1.11",
151 "is-stream": "^1.0.1"
152 }
153 },
154 "object-assign": {
155 "version": "4.1.1",
156 "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
157 "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
158 },
159 "promise": {
160 "version": "7.3.1",
161 "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
162 "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
163 "requires": {
164 "asap": "~2.0.3"
165 }
166 },
167 "prop-types": {
168 "version": "15.6.2",
169 "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
170 "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
171 "requires": {
172 "loose-envify": "^1.3.1",
173 "object-assign": "^4.1.1"
174 }
175 },
176 "react-html-attributes": {
177 "version": "1.4.3",
178 "resolved": "https://registry.npmjs.org/react-html-attributes/-/react-html-attributes-1.4.3.tgz",
179 "integrity": "sha1-jDbDX85rdQk40oavQo7R2nYlGG4=",
180 "requires": {
181 "html-element-attributes": "^1.0.0"
182 }
183 },
184 "react-loader": {
185 "version": "2.4.5",
186 "resolved": "https://registry.npmjs.org/react-loader/-/react-loader-2.4.5.tgz",
187 "integrity": "sha1-zT5VHGzQc4wcDxPwc2VPk4KL5ak=",
188 "requires": {
189 "create-react-class": "^15.5.2",
190 "prop-types": "^15.5.8",
191 "spin.js": "2.x"
192 }
193 },
194 "safer-buffer": {
195 "version": "2.1.2",
196 "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
197 "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
198 },
199 "setimmediate": {
200 "version": "1.0.5",
201 "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
202 "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
203 },
204 "spin.js": {
205 "version": "2.3.2",
206 "resolved": "https://registry.npmjs.org/spin.js/-/spin.js-2.3.2.tgz",
207 "integrity": "sha1-bKpW1SBnNFD9XPvGlx5tB3LDeho="
208 },
209 "ua-parser-js": {
210 "version": "0.7.19",
211 "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz",
212 "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ=="
213 },
214 "whatwg-fetch": {
215 "version": "3.0.0",
216 "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
217 "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
218 }
219 }
220}
diff --git a/packages/forms/package.json b/packages/forms/package.json
new file mode 100644
index 000000000..be7f9f99e
--- /dev/null
+++ b/packages/forms/package.json
@@ -0,0 +1,39 @@
1{
2 "name": "@meetfranz/forms",
3 "version": "1.0.7",
4 "description": "React form components for Franz",
5 "main": "lib/index.js",
6 "scripts": {
7 "dev": "tsc -w",
8 "prepare": "tsc"
9 },
10 "publishConfig": {
11 "access": "public"
12 },
13 "repository": {
14 "type": "git",
15 "url": "git+https://github.com/meetfranz/franz.git"
16 },
17 "keywords": [
18 "Franz",
19 "Forms",
20 "React",
21 "UI"
22 ],
23 "author": "Stefan Malzner <stefan@adlk.io>",
24 "license": "Apache-2.0",
25 "dependencies": {
26 "@mdi/js": "^3.3.92",
27 "@mdi/react": "^1.1.0",
28 "@meetfranz/theme": "^1.0.7",
29 "react-html-attributes": "^1.4.3",
30 "react-loader": "^2.4.5"
31 },
32 "peerDependencies": {
33 "classnames": "^2.2.6",
34 "react": "^16.7.0",
35 "react-dom": "16.7.0",
36 "react-jss": "^8.6.1"
37 },
38 "gitHead": "14b151cad6a5a849bb476aaa3fc53bf1eead7f4b"
39}
diff --git a/packages/forms/src/button/index.tsx b/packages/forms/src/button/index.tsx
new file mode 100644
index 000000000..7a7f83dab
--- /dev/null
+++ b/packages/forms/src/button/index.tsx
@@ -0,0 +1,274 @@
1import * as mdiIcons from '@mdi/js';
2import Icon from '@mdi/react';
3import { Theme } from '@meetfranz/theme';
4import classnames from 'classnames';
5import CSS from 'csstype';
6import React, { Component } from 'react';
7import injectStyle, { withTheme } from 'react-jss';
8import Loader from 'react-loader';
9
10import { IFormField, IWithStyle } from '../typings/generic';
11
12type ButtonType = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'inverted';
13
14interface IProps extends IFormField, IWithStyle {
15 className?: string;
16 disabled?: boolean;
17 id?: string;
18 type?: string;
19 onClick: (event: React.MouseEvent<HTMLButtonElement> | React.MouseEvent<HTMLAnchorElement>) => void;
20 buttonType?: ButtonType;
21 stretch?: boolean;
22 loaded?: boolean;
23 busy?: boolean;
24 icon?: keyof typeof mdiIcons;
25 href?: string;
26 target?: string;
27}
28
29interface IState {
30 busy: boolean;
31}
32
33const styles = (theme: Theme) => ({
34 button: {
35 borderRadius: theme.borderRadiusSmall,
36 border: 'none',
37 display: 'inline-flex',
38 position: 'relative' as CSS.PositionProperty,
39 transition: 'background .5s, opacity 0.3s',
40 textAlign: 'center' as CSS.TextAlignProperty,
41 outline: 'none',
42 alignItems: 'center',
43 padding: 0,
44 width: (props: IProps) => (props.stretch ? '100%' : 'auto') as CSS.WidthProperty<string>,
45 fontSize: theme.uiFontSize,
46 textDecoration: 'none',
47
48 '&:hover': {
49 opacity: 0.8,
50 },
51 '&:active': {
52 opacity: 0.5,
53 transition: 'none',
54 },
55 },
56 label: {
57 margin: '10px 20px',
58 width: '100%',
59 display: 'flex',
60 alignItems: 'center',
61 justifyContent: 'center',
62 },
63 primary: {
64 background: theme.buttonPrimaryBackground,
65 color: theme.buttonPrimaryTextColor,
66
67 '& svg': {
68 fill: theme.buttonPrimaryTextColor,
69 },
70 },
71 secondary: {
72 background: theme.buttonSecondaryBackground,
73 color: theme.buttonSecondaryTextColor,
74
75 '& svg': {
76 fill: theme.buttonSecondaryTextColor,
77 },
78 },
79 success: {
80 background: theme.buttonSuccessBackground,
81 color: theme.buttonSuccessTextColor,
82
83 '& svg': {
84 fill: theme.buttonSuccessTextColor,
85 },
86 },
87 danger: {
88 background: theme.buttonDangerBackground,
89 color: theme.buttonDangerTextColor,
90
91 '& svg': {
92 fill: theme.buttonDangerTextColor,
93 },
94 },
95 warning: {
96 background: theme.buttonWarningBackground,
97 color: theme.buttonWarningTextColor,
98
99 '& svg': {
100 fill: theme.buttonWarningTextColor,
101 },
102 },
103 inverted: {
104 background: theme.buttonInvertedBackground,
105 color: theme.buttonInvertedTextColor,
106 border: theme.buttonInvertedBorder,
107
108 '& svg': {
109 fill: theme.buttonInvertedTextColor,
110 },
111 },
112 disabled: {
113 opacity: theme.inputDisabledOpacity,
114 },
115 loader: {
116 position: 'relative' as CSS.PositionProperty,
117 width: 20,
118 height: 18,
119 zIndex: 9999,
120 },
121 loaderContainer: {
122 width: (props: IProps): string => (!props.busy ? '0' : '40px'),
123 height: 20,
124 overflow: 'hidden',
125 transition: 'all 0.3s',
126 marginLeft: (props: IProps): number => !props.busy ? 10 : 20,
127 marginRight: (props: IProps): number => !props.busy ? -10 : -20,
128 position: (props: IProps): CSS.PositionProperty => props.stretch ? 'absolute' : 'inherit',
129 },
130 icon: {
131 marginLeft: -5,
132 marginRight: 10,
133 },
134});
135
136class ButtonComponent extends Component<IProps> {
137 public static defaultProps = {
138 type: 'button',
139 disabled: false,
140 onClick: () => null,
141 buttonType: 'primary' as ButtonType,
142 stretch: false,
143 busy: false,
144 // target: '_self'
145 };
146
147 state = {
148 busy: false,
149 };
150
151 componentWillMount() {
152 this.setState({ busy: this.props.busy });
153 }
154
155 componentWillReceiveProps(nextProps: IProps) {
156 if (nextProps.busy !== this.props.busy) {
157 if (this.props.busy) {
158 setTimeout(() => {
159 this.setState({ busy: nextProps.busy });
160 }, 300);
161 } else {
162 this.setState({ busy: nextProps.busy });
163 }
164 }
165 }
166
167 render() {
168 const {
169 classes,
170 className,
171 theme,
172 disabled,
173 id,
174 label,
175 type,
176 onClick,
177 buttonType,
178 loaded,
179 icon: iconName,
180 busy: busyProp,
181 href,
182 target,
183 } = this.props;
184
185 const {
186 busy,
187 } = this.state;
188
189 let icon = '';
190 if (iconName && mdiIcons[iconName]) {
191 icon = mdiIcons[iconName];
192 } else if (iconName && !mdiIcons[iconName]) {
193 console.warn(`Icon '${iconName}' was not found`);
194 }
195
196 let showLoader = false;
197 if (loaded) {
198 showLoader = !loaded;
199 console.warn('Franz Button prop `loaded` will be deprecated in the future. Please use `busy` instead');
200 }
201 if (busy) {
202 showLoader = busy;
203 }
204
205 const content = (
206 <>
207 <div className={classes.loaderContainer}>
208 {showLoader && (
209 <Loader
210 loaded={false}
211 width={4}
212 scale={0.45}
213 color={theme.buttonLoaderColor[buttonType!]}
214 parentClassName={classes.loader}
215 />
216 )}
217 </div>
218 <div className={classes.label}>
219 {icon && (
220 <Icon
221 path={icon}
222 size={1}
223 className={classes.icon}
224 />
225 )}
226 {label}
227 </div>
228 </>
229 );
230
231 let wrapperComponent = null;
232
233 if (!href) {
234 wrapperComponent = (
235 <button
236 id={id}
237 type={type}
238 onClick={onClick}
239 className={classnames({
240 [`${classes.button}`]: true,
241 [`${classes[buttonType as ButtonType]}`]: true,
242 [`${classes.disabled}`]: disabled,
243 [`${className}`]: className,
244 })}
245 disabled={disabled}
246 data-type="franz-button"
247 >
248 {content}
249 </button>
250 );
251 } else {
252 wrapperComponent = (
253 <a
254 href={href}
255 target={target}
256 onClick={onClick}
257 className={classnames({
258 [`${classes.button}`]: true,
259 [`${classes[buttonType as ButtonType]}`]: true,
260 [`${className}`]: className,
261 })}
262 rel={target === '_blank' ? 'noopener' : ''}
263 data-type="franz-button"
264 >
265 {content}
266 </a>
267 );
268 }
269
270 return wrapperComponent;
271 }
272}
273
274export const Button = injectStyle(styles)(withTheme(ButtonComponent));
diff --git a/packages/forms/src/error/index.tsx b/packages/forms/src/error/index.tsx
new file mode 100644
index 000000000..a487bb281
--- /dev/null
+++ b/packages/forms/src/error/index.tsx
@@ -0,0 +1,29 @@
1import { Classes } from 'jss';
2import React, { Component } from 'react';
3import injectSheet from 'react-jss';
4
5import styles from './styles';
6
7interface IProps {
8 classes: Classes;
9 message: string;
10}
11
12class ErrorComponent extends Component<IProps> {
13 render() {
14 const {
15 classes,
16 message,
17 } = this.props;
18
19 return (
20 <p
21 className={classes.message}
22 >
23 {message}
24 </p>
25 );
26 }
27}
28
29export const Error = injectSheet(styles)(ErrorComponent);
diff --git a/packages/forms/src/error/styles.ts b/packages/forms/src/error/styles.ts
new file mode 100644
index 000000000..5104838a5
--- /dev/null
+++ b/packages/forms/src/error/styles.ts
@@ -0,0 +1,9 @@
1import { Theme } from '../../../theme/lib';
2
3export default (theme: Theme) => ({
4 message: {
5 color: theme.brandDanger,
6 margin: '5px 0 0',
7 fontSize: theme.uiFontSize,
8 },
9});
diff --git a/packages/forms/src/index.ts b/packages/forms/src/index.ts
new file mode 100644
index 000000000..ea47fe25e
--- /dev/null
+++ b/packages/forms/src/index.ts
@@ -0,0 +1,4 @@
1export { Input } from './input';
2export { Toggle } from './toggle';
3export { Button } from './button';
4export { Select } from './select';
diff --git a/packages/forms/src/input/index.tsx b/packages/forms/src/input/index.tsx
new file mode 100644
index 000000000..de7e7848e
--- /dev/null
+++ b/packages/forms/src/input/index.tsx
@@ -0,0 +1,190 @@
1import { mdiEye, mdiEyeOff } from '@mdi/js';
2import Icon from '@mdi/react';
3import classnames from 'classnames';
4import React, { Component, createRef } from 'react';
5import injectSheet from 'react-jss';
6
7import { IFormField, IWithStyle } from '../typings/generic';
8
9import { Error } from '../error';
10import { Label } from '../label';
11import { Wrapper } from '../wrapper';
12import { scorePasswordFunc } from './scorePassword';
13
14import styles from './styles';
15
16interface IData {
17 [index: string]: string;
18}
19
20interface IProps extends React.InputHTMLAttributes<HTMLInputElement>, IFormField, IWithStyle {
21 focus?: boolean;
22 prefix?: string;
23 suffix?: string;
24 scorePassword?: boolean;
25 showPasswordToggle?: boolean;
26 data: IData;
27 inputClassName?: string;
28}
29
30interface IState {
31 showPassword: boolean;
32 passwordScore: number;
33}
34
35class InputComponent extends Component<IProps, IState> {
36 public static defaultProps = {
37 focus: false,
38 onChange: () => {},
39 onBlur: () => {},
40 scorePassword: false,
41 showLabel: true,
42 showPasswordToggle: false,
43 type: 'text',
44 disabled: false,
45 };
46
47 state = {
48 passwordScore: 0,
49 showPassword: false,
50 };
51
52 private inputRef = createRef<HTMLInputElement>();
53
54 componentDidMount() {
55 const { focus, data } = this.props;
56
57 if (this.inputRef && this.inputRef.current) {
58 if (focus) {
59 this.inputRef.current.focus();
60 }
61
62 if (data) {
63 Object.keys(data).map(key => this.inputRef.current!.dataset[key] = data[key]);
64 }
65 }
66 }
67
68 onChange(e: React.ChangeEvent<HTMLInputElement>) {
69 const {
70 scorePassword,
71 onChange,
72 } = this.props;
73
74 if (onChange) {
75 onChange(e);
76 }
77
78 if (this.inputRef && this.inputRef.current && scorePassword) {
79 this.setState({ passwordScore: scorePasswordFunc(this.inputRef.current.value) });
80 }
81 }
82
83 render() {
84 const {
85 classes,
86 className,
87 disabled,
88 error,
89 id,
90 inputClassName,
91 label,
92 prefix,
93 scorePassword,
94 suffix,
95 showLabel,
96 showPasswordToggle,
97 type,
98 value,
99 name,
100 placeholder,
101 spellCheck,
102 onBlur,
103 } = this.props;
104
105 const {
106 showPassword,
107 passwordScore,
108 } = this.state;
109
110 const inputType = type === 'password' && showPassword ? 'text' : type;
111
112 return (
113 <Wrapper
114 className={className}
115 identifier="franz-input"
116 >
117 <Label
118 title={label}
119 showLabel={showLabel}
120 htmlFor={id}
121 >
122 <div
123 className={classnames({
124 [`${inputClassName}`]: inputClassName,
125 [`${classes.hasPasswordScore}`]: scorePassword,
126 [`${classes.wrapper}`]: true,
127 [`${classes.disabled}`]: disabled,
128 [`${classes.hasError}`]: error,
129 })}>
130 {prefix && (
131 <span className={classes.prefix}>
132 {prefix}
133 </span>
134 )}
135 <input
136 id={id}
137 type={inputType}
138 name={name}
139 defaultValue={value as string}
140 placeholder={placeholder}
141 spellCheck={spellCheck}
142 className={classes.input}
143 ref={this.inputRef}
144 onChange={this.onChange.bind(this)}
145 onBlur={onBlur}
146 disabled={disabled}
147 />
148 {suffix && (
149 <span className={classes.suffix}>
150 {suffix}
151 </span>
152 )}
153 {showPasswordToggle && (
154 <button
155 type="button"
156 className={classes.formModifier}
157 onClick={() => this.setState(prevState => ({ showPassword: !prevState.showPassword }))}
158 tabIndex={-1}
159 >
160 <Icon
161 path={!showPassword ? mdiEye : mdiEyeOff}
162 size={1}
163 />
164 </button>
165 )}
166 </div>
167 {scorePassword && (
168 <div className={classnames({
169 [`${classes.passwordScore}`]: true,
170 [`${classes.hasError}`]: error,
171 })}>
172 <meter
173 value={passwordScore < 5 ? 5 : passwordScore}
174 low={30}
175 high={75}
176 optimum={100}
177 max={100}
178 />
179 </div>
180 )}
181 </Label>
182 {error && (
183 <Error message={error} />
184 )}
185 </Wrapper>
186 );
187 }
188}
189
190export const Input = injectSheet(styles)(InputComponent);
diff --git a/packages/forms/src/input/scorePassword.ts b/packages/forms/src/input/scorePassword.ts
new file mode 100644
index 000000000..0b7719ec1
--- /dev/null
+++ b/packages/forms/src/input/scorePassword.ts
@@ -0,0 +1,42 @@
1interface ILetters {
2 [key: string]: number;
3}
4
5interface IVariations {
6 [index: string]: boolean;
7 digits: boolean;
8 lower: boolean;
9 nonWords: boolean;
10 upper: boolean;
11}
12
13export function scorePasswordFunc(password: string): number {
14 let score: number = 0;
15 if (!password) {
16 return score;
17 }
18
19 // award every unique letter until 5 repetitions
20 const letters: ILetters = {};
21 for (let i = 0; i < password.length; i += 1) {
22 letters[password[i]] = (letters[password[i]] || 0) + 1;
23 score += 5.0 / letters[password[i]];
24 }
25
26 // bonus points for mixing it up
27 const variations: IVariations = {
28 digits: /\d/.test(password),
29 lower: /[a-z]/.test(password),
30 nonWords: /\W/.test(password),
31 upper: /[A-Z]/.test(password),
32 };
33
34 let variationCount = 0;
35 Object.keys(variations).forEach((key) => {
36 variationCount += (variations[key] === true) ? 1 : 0;
37 });
38
39 score += (variationCount - 1) * 10;
40
41 return Math.round(score);
42}
diff --git a/packages/forms/src/input/styles.ts b/packages/forms/src/input/styles.ts
new file mode 100644
index 000000000..a64d2c340
--- /dev/null
+++ b/packages/forms/src/input/styles.ts
@@ -0,0 +1,96 @@
1import { Theme } from '@meetfranz/theme';
2import CSS from 'csstype';
3
4const prefixStyles = (theme: Theme) => ({
5 background: theme.inputPrefixBackground,
6 color: theme.inputPrefixColor,
7 lineHeight: theme.inputHeight,
8 padding: '0 10px',
9 fontSize: theme.uiFontSize,
10});
11
12export default (theme: Theme) => ({
13 disabled: {
14 opacity: theme.inputDisabledOpacity,
15 },
16 formModifier: {
17 background: 'none',
18 border: 0,
19 borderLeft: theme.inputBorder,
20 padding: '4px 20px 0',
21 outline: 'none',
22
23 '&:active': {
24 opacity: 0.5,
25 },
26
27 '& svg': {
28 fill: theme.inputModifierColor,
29 },
30 },
31 input: {
32 background: 'none',
33 border: 0,
34 fontSize: theme.uiFontSize,
35 outline: 'none',
36 padding: 8,
37 width: '100%',
38 color: theme.inputColor,
39
40 '&::placeholder': {
41 color: theme.inputPlaceholderColor,
42 },
43 },
44 passwordScore: {
45 background: theme.inputScorePasswordBackground,
46 border: theme.inputBorder,
47 borderTopWidth: 0,
48 borderBottomLeftRadius: theme.borderRadiusSmall,
49 borderBottomRightRadius: theme.borderRadiusSmall,
50 display: 'block',
51 flexBasis: '100%',
52 height: 5,
53 overflow: 'hidden',
54
55 '& meter': {
56 display: 'block',
57 height: '100%',
58 width: '100%',
59
60 '&::-webkit-meter-bar': {
61 background: 'none',
62 },
63
64 '&::-webkit-meter-even-less-good-value': {
65 background: theme.brandDanger,
66 },
67
68 '&::-webkit-meter-suboptimum-value': {
69 background: theme.brandWarning,
70 },
71
72 '&::-webkit-meter-optimum-value': {
73 background: theme.brandSuccess,
74 },
75 },
76 },
77 prefix: prefixStyles(theme),
78 suffix: prefixStyles(theme),
79 wrapper: {
80 background: theme.inputBackground,
81 border: theme.inputBorder,
82 borderRadius: theme.borderRadiusSmall,
83 boxSizing: 'border-box' as CSS.BoxSizingProperty,
84 display: 'flex',
85 height: theme.inputHeight,
86 order: 1,
87 width: '100%',
88 },
89 hasPasswordScore: {
90 borderBottomLeftRadius: 0,
91 borderBottomRightRadius: 0,
92 },
93 hasError: {
94 borderColor: theme.brandDanger,
95 },
96});
diff --git a/packages/forms/src/label/index.tsx b/packages/forms/src/label/index.tsx
new file mode 100644
index 000000000..36fcfbedf
--- /dev/null
+++ b/packages/forms/src/label/index.tsx
@@ -0,0 +1,47 @@
1import classnames from 'classnames';
2import { Classes } from 'jss';
3import React, { Component } from 'react';
4import injectSheet from 'react-jss';
5
6import { IFormField } from '../typings/generic';
7
8import styles from './styles';
9
10interface ILabel extends IFormField, React.LabelHTMLAttributes<HTMLLabelElement> {
11 classes: Classes;
12}
13
14class LabelComponent extends Component<ILabel> {
15 static defaultProps = {
16 showLabel: true,
17 };
18
19 render() {
20 const {
21 title,
22 showLabel,
23 classes,
24 className,
25 children,
26 htmlFor,
27 } = this.props;
28
29 return (
30 <label
31 className={classnames({
32 [`${className}`]: className,
33 })}
34 htmlFor={htmlFor}
35 >
36 {showLabel && (
37 <span className={classes.label}>{title}</span>
38 )}
39 <div className={classes.content}>
40 {children}
41 </div>
42 </label>
43 );
44 }
45}
46
47export const Label = injectSheet(styles)(LabelComponent);
diff --git a/packages/forms/src/label/styles.ts b/packages/forms/src/label/styles.ts
new file mode 100644
index 000000000..f3998de04
--- /dev/null
+++ b/packages/forms/src/label/styles.ts
@@ -0,0 +1,14 @@
1import { Theme } from '../../../theme/lib';
2
3export default (theme: Theme) => ({
4 content: {
5 marginTop: 5,
6 },
7 label: {
8 color: theme.labelColor,
9 fontSize: theme.uiFontSize,
10 },
11 hasError: {
12 color: theme.brandDanger,
13 },
14});
diff --git a/packages/forms/src/select/index.tsx b/packages/forms/src/select/index.tsx
new file mode 100644
index 000000000..4a9e3c56e
--- /dev/null
+++ b/packages/forms/src/select/index.tsx
@@ -0,0 +1,437 @@
1import { mdiArrowRightDropCircleOutline, mdiCloseCircle, mdiMagnify } from '@mdi/js';
2import Icon from '@mdi/react';
3import { Theme } from '@meetfranz/theme';
4import classnames from 'classnames';
5import React, { Component, createRef } from 'react';
6import injectStyle from 'react-jss';
7
8import { IFormField, IWithStyle } from '../typings/generic';
9
10import { NONAME } from 'dns';
11import { Error } from '../error';
12import { Label } from '../label';
13import { Wrapper } from '../wrapper';
14
15interface IOptions {
16 [index: string]: string;
17}
18
19interface IData {
20 [index: string]: string;
21}
22
23interface IProps extends IFormField, IWithStyle {
24 actionText: string;
25 className?: string;
26 inputClassName?: string;
27 defaultValue?: string;
28 disabled?: boolean;
29 id?: string;
30 name: string;
31 options: IOptions;
32 value: string;
33 onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
34 showSearch: boolean;
35 data: IData;
36}
37
38interface IState {
39 open: boolean;
40 value: string;
41 needle: string;
42 selected: number;
43 options: IOptions;
44}
45
46const styles = (theme: Theme) => ({
47 select: {
48 background: theme.selectBackground,
49 border: theme.selectBorder,
50 borderRadius: theme.borderRadiusSmall,
51 height: theme.selectHeight,
52 fontSize: theme.uiFontSize,
53 width: '100%',
54 display: 'flex',
55 alignItems: 'center',
56 textAlign: 'left',
57 color: theme.selectColor,
58 },
59 popup: {
60 opacity: 0,
61 height: 0,
62 overflowX: 'scroll',
63 border: theme.selectBorder,
64 borderTop: 0,
65 transition: 'all 0.3s',
66 },
67 open: {
68 opacity: 1,
69 height: 350,
70 background: theme.selectPopupBackground,
71 },
72 option: {
73 padding: 10,
74 borderBottom: theme.selectOptionBorder,
75 color: theme.selectOptionColor,
76
77 '&:hover': {
78 background: theme.selectOptionItemHover,
79 color: theme.selectOptionItemHoverColor,
80 },
81 '&:active': {
82 background: theme.selectOptionItemActive,
83 color: theme.selectOptionItemActiveColor,
84 },
85 },
86 selected: {
87 background: theme.selectOptionItemActive,
88 color: theme.selectOptionItemActiveColor,
89 },
90 toggle: {
91 marginLeft: 'auto',
92 fill: theme.selectToggleColor,
93 transition: 'transform 0.3s',
94 },
95 toggleOpened: {
96 transform: 'rotateZ(90deg)',
97 },
98 searchContainer: {
99 display: 'flex',
100 background: theme.selectSearchBackground,
101 alignItems: 'center',
102 paddingLeft: 10,
103 color: theme.selectColor,
104
105 '& svg': {
106 fill: theme.selectSearchColor,
107 },
108 },
109 search: {
110 border: 0,
111 width: '100%',
112 fontSize: theme.uiFontSize,
113 background: 'none',
114 marginLeft: 10,
115 padding: [10, 0],
116 color: theme.selectSearchColor,
117 },
118 clearNeedle: {
119 background: 'none',
120 border: 0,
121 },
122 focused: {
123 fontWeight: 'bold',
124 background: theme.selectOptionItemHover,
125 color: theme.selectOptionItemHoverColor,
126 },
127 hasError: {
128 borderColor: theme.brandDanger,
129 },
130 disabled: {
131 opacity: theme.selectDisabledOpacity,
132 },
133});
134
135class SelectComponent extends Component<IProps> {
136 public static defaultProps = {
137 onChange: () => {},
138 showLabel: true,
139 disabled: false,
140 error: '',
141 };
142
143 state = {
144 open: false,
145 value: '',
146 needle: '',
147 selected: 0,
148 options: null,
149 };
150
151 private componentRef = createRef<HTMLDivElement>();
152 private inputRef = createRef<HTMLInputElement>();
153 private searchInputRef = createRef<HTMLInputElement>();
154 private scrollContainerRef = createRef<HTMLDivElement>();
155 private activeOptionRef = createRef<HTMLDivElement>();
156
157 private keyListener: any;
158
159 componentWillReceiveProps(nextProps: IProps) {
160 if (nextProps.value && nextProps.value !== this.props.value) {
161 this.setState({
162 value: nextProps.value,
163 });
164 }
165 }
166
167 componentDidUpdate(prevProps: IProps, prevState: IState) {
168 const {
169 open,
170 } = this.state;
171
172 if (this.searchInputRef && this.searchInputRef.current) {
173 if (open) {
174 this.searchInputRef.current.focus();
175 }
176 }
177 }
178
179 componentDidMount() {
180 if (this.inputRef && this.inputRef.current) {
181 const {
182 data,
183 } = this.props;
184
185 if (data) {
186 Object.keys(data).map(key => this.inputRef.current!.dataset[key] = data[key]);
187 }
188 }
189
190 window.addEventListener('keydown', this.arrowKeysHandler.bind(this), false);
191 }
192
193 componentWillMount() {
194 const {
195 value,
196 } = this.props;
197
198 if (this.componentRef && this.componentRef.current) {
199 this.componentRef.current.removeEventListener('keydown', this.keyListener);
200 }
201
202 if (value) {
203 this.setState({
204 value,
205 });
206 }
207
208 this.setFilter();
209 }
210
211 componentWillUnmount() {
212 window.removeEventListener('keydown', this.arrowKeysHandler.bind(this));
213 }
214
215 setFilter(needle: string = '') {
216 const { options } = this.props;
217
218 let filteredOptions = {};
219 if (needle) {
220 Object.keys(options).map((key) => {
221 if (key.toLocaleLowerCase().startsWith(needle.toLocaleLowerCase()) || options[key].toLocaleLowerCase().startsWith(needle.toLocaleLowerCase())) {
222 Object.assign(filteredOptions, {
223 [`${key}`]: options[key],
224 });
225 }
226 });
227 } else {
228 filteredOptions = options;
229 }
230
231 this.setState({
232 needle,
233 options: filteredOptions,
234 selected: 0,
235 });
236 }
237
238 select(key: string) {
239 this.setState((state: IState) => ({
240 value: key,
241 open: false,
242 }));
243
244 this.setFilter();
245
246 if (this.props.onChange) {
247 this.props.onChange(key as any);
248 }
249 }
250
251 arrowKeysHandler(e: KeyboardEvent) {
252 const {
253 selected,
254 open,
255 options,
256 } = this.state;
257
258 if (!open) return;
259
260 if (e.keyCode === 38 || e.keyCode === 40) {
261 e.preventDefault();
262 }
263
264 if (this.componentRef && this.componentRef.current) {
265 if (e.keyCode === 38 && selected > 0) {
266 this.setState((state: IState) => ({
267 selected: state.selected - 1,
268 }));
269 } else if (e.keyCode === 40 && selected < Object.keys(options!).length - 1) {
270 this.setState((state: IState) => ({
271 selected: state.selected + 1,
272 }));
273 } else if (e.keyCode === 13) {
274 this.select(Object.keys(options!)[selected]);
275 }
276
277 if (this.activeOptionRef && this.activeOptionRef.current && this.scrollContainerRef && this.scrollContainerRef.current) {
278 const containerTopOffset = this.scrollContainerRef.current.offsetTop;
279 const optionTopOffset = this.activeOptionRef.current.offsetTop;
280
281 const topOffset = optionTopOffset - containerTopOffset;
282
283 this.scrollContainerRef.current.scrollTop = topOffset - 35;
284 }
285 }
286
287 switch (e.keyCode){
288 case 37: case 39: case 38: case 40: // Arrow keys
289 case 32: break; // Space
290 default: break; // do not block other keys
291 }
292 }
293
294 render() {
295 const {
296 actionText,
297 classes,
298 className,
299 defaultValue,
300 disabled,
301 error,
302 id,
303 inputClassName,
304 name,
305 label,
306 showLabel,
307 showSearch,
308 onChange,
309 } = this.props;
310
311 const {
312 open,
313 needle,
314 value,
315 selected,
316 options,
317 } = this.state;
318
319 let selection = '';
320 if (!value && defaultValue && options![defaultValue]) {
321 selection = options![defaultValue];
322 } else if (value && options![value]) {
323 selection = options![value];
324 } else {
325 selection = actionText;
326 }
327
328 return (
329 <Wrapper
330 className={className}
331 identifier="franz-select"
332 >
333 <Label
334 title={label}
335 showLabel={showLabel}
336 htmlFor={id}
337 >
338 <div
339 className={classnames({
340 [`${classes.hasError}`]: error,
341 [`${classes.disabled}`]: disabled,
342 })}
343 ref={this.componentRef}
344 >
345 <button
346 type="button"
347 className={classnames({
348 [`${inputClassName}`]: inputClassName,
349 [`${classes.select}`]: true,
350 [`${classes.hasError}`]: error,
351 })}
352 onClick= {!disabled ? () => this.setState((state: IState) => ({
353 open: !state.open,
354 })) : () => {}}
355 >
356 {selection}
357 <Icon
358 path={mdiArrowRightDropCircleOutline}
359 size={0.8}
360 className={classnames({
361 [`${classes.toggle}`]: true,
362 [`${classes.toggleOpened}`]: open,
363 })}
364 />
365 </button>
366 {showSearch && open && (
367 <div className={classes.searchContainer}>
368 <Icon
369 path={mdiMagnify}
370 size={0.8}
371 />
372 <input
373 type="text"
374 value={needle}
375 onChange={e => this.setFilter(e.currentTarget.value)}
376 placeholder="Search"
377 className={classes.search}
378 ref={this.searchInputRef}
379 />
380 {needle && (
381 <button
382 type="button"
383 className={classes.clearNeedle}
384 onClick={() => this.setFilter()}
385 >
386 <Icon
387 path={mdiCloseCircle}
388 size={0.7}
389 />
390 </button>
391 )}
392 </div>
393 )}
394 <div
395 className={classnames({
396 [`${classes.popup}`]: true,
397 [`${classes.open}`]: open,
398 })}
399 ref={this.scrollContainerRef}
400 >
401 {Object.keys(options!).map(((key, i) => (
402 <div
403 key={key}
404 onClick={() => this.select(key)}
405 className={classnames({
406 [`${classes.option}`]: true,
407 [`${classes.selected}`]: options![key] === selection,
408 [`${classes.focused}`]: selected === i,
409 })}
410 onMouseOver={() => this.setState({ selected: i })}
411 ref={selected === i ? this.activeOptionRef : null}
412 >
413 {options![key]}
414 </div>
415 )))}
416 </div>
417 </div>
418 <input
419 className={classes.input}
420 id={id}
421 name={name}
422 type="hidden"
423 defaultValue={value}
424 onChange={onChange}
425 disabled={disabled}
426 ref={this.inputRef}
427 />
428 </Label>
429 {error && (
430 <Error message={error} />
431 )}
432 </Wrapper>
433 );
434 }
435}
436
437export const Select = injectStyle(styles)(SelectComponent);
diff --git a/packages/forms/src/toggle/index.tsx b/packages/forms/src/toggle/index.tsx
new file mode 100644
index 000000000..6487f1d07
--- /dev/null
+++ b/packages/forms/src/toggle/index.tsx
@@ -0,0 +1,117 @@
1import { Theme } from '@meetfranz/theme';
2import classnames from 'classnames';
3import CSS from 'csstype';
4import React, { Component, createRef } from 'react';
5import injectStyle from 'react-jss';
6
7import { IFormField, IWithStyle, Omit } from '../typings/generic';
8
9import { Error } from '../error';
10import { Label } from '../label';
11import { Wrapper } from '../wrapper';
12
13interface IProps extends React.InputHTMLAttributes<HTMLInputElement>, IFormField, IWithStyle {
14 className?: string;
15}
16
17const styles = (theme: Theme) => ({
18 toggle: {
19 background: theme.toggleBackground,
20 borderRadius: theme.borderRadius,
21 height: theme.toggleHeight,
22 position: 'relative' as CSS.PositionProperty,
23 width: theme.toggleWidth,
24 },
25 button: {
26 background: theme.toggleButton,
27 borderRadius: '100%',
28 boxShadow: '0 1px 4px rgba(0, 0, 0, .3)',
29 width: theme.toggleHeight - 2,
30 height: theme.toggleHeight - 2,
31 left: 1,
32 top: 1,
33 position: 'absolute' as CSS.PositionProperty,
34 transition: 'all .5s',
35 },
36 buttonActive: {
37 background: theme.toggleButtonActive,
38 left: (theme.toggleWidth - theme.toggleHeight) + 1,
39 },
40 input: {
41 visibility: 'hidden' as any,
42 },
43 disabled: {
44 opacity: theme.inputDisabledOpacity,
45 },
46 toggleLabel: {
47 display: 'flex',
48
49 '& > span': {
50 order: 1,
51 marginLeft: 15,
52 marginTop: 2,
53 },
54 },
55});
56
57class ToggleComponent extends Component<IProps> {
58 public static defaultProps = {
59 onChange: () => {},
60 showLabel: true,
61 disabled: false,
62 error: '',
63 };
64
65 render() {
66 const {
67 classes,
68 className,
69 disabled,
70 error,
71 id,
72 label,
73 showLabel,
74 checked,
75 value,
76 onChange,
77 } = this.props;
78
79 return (
80 <Wrapper
81 className={className}
82 identifier="franz-toggle"
83 >
84 <Label
85 title={label}
86 showLabel={showLabel}
87 htmlFor={id}
88 className={classes.toggleLabel}
89 >
90 <div className={classnames({
91 [`${classes.toggle}`]: true,
92 [`${classes.disabled}`]: disabled,
93 })}>
94 <div className={classnames({
95 [`${classes.button}`]: true,
96 [`${classes.buttonActive}`]: checked,
97 })} />
98 <input
99 className={classes.input}
100 id={id || name}
101 type="checkbox"
102 checked={checked}
103 value={value}
104 onChange={onChange}
105 disabled={disabled}
106 />
107 </div>
108 </Label>
109 {error && (
110 <Error message={error} />
111 )}
112 </Wrapper>
113 );
114 }
115}
116
117export const Toggle = injectStyle(styles)(ToggleComponent);
diff --git a/packages/forms/src/typings/generic.ts b/packages/forms/src/typings/generic.ts
new file mode 100644
index 000000000..b7f2fc452
--- /dev/null
+++ b/packages/forms/src/typings/generic.ts
@@ -0,0 +1,16 @@
1import { Theme } from '@meetfranz/theme/lib';
2import { Classes } from 'jss';
3
4export interface IFormField {
5 showLabel?: boolean;
6 label?: string;
7 error?: string;
8}
9
10export interface IWithStyle {
11 classes: Classes;
12 theme: Theme;
13}
14
15export type Merge<M, N> = Omit<M, Extract<keyof M, keyof N>> & N;
16export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
diff --git a/packages/forms/src/wrapper/index.tsx b/packages/forms/src/wrapper/index.tsx
new file mode 100644
index 000000000..d9c61381d
--- /dev/null
+++ b/packages/forms/src/wrapper/index.tsx
@@ -0,0 +1,37 @@
1import classnames from 'classnames';
2import React, { Component } from 'react';
3import injectStyle from 'react-jss';
4import { IWithStyle } from '../typings/generic';
5
6import styles from './styles';
7
8interface IProps extends IWithStyle {
9 children: React.ReactNode;
10 className?: string;
11 identifier: string;
12}
13
14class WrapperComponent extends Component<IProps> {
15 render() {
16 const {
17 children,
18 classes,
19 className,
20 identifier,
21 } = this.props;
22
23 return (
24 <div
25 className={classnames({
26 [`${classes.container}`]: true,
27 [`${className}`]: className,
28 })}
29 data-type={identifier}
30 >
31 {children}
32 </div>
33 );
34 }
35}
36
37export const Wrapper = injectStyle(styles)(WrapperComponent);
diff --git a/packages/forms/src/wrapper/styles.ts b/packages/forms/src/wrapper/styles.ts
new file mode 100644
index 000000000..72306b252
--- /dev/null
+++ b/packages/forms/src/wrapper/styles.ts
@@ -0,0 +1,5 @@
1export default {
2 container: {
3 marginBottom: 20,
4 },
5};
diff --git a/packages/forms/tsconfig.json b/packages/forms/tsconfig.json
new file mode 100644
index 000000000..8b9507eac
--- /dev/null
+++ b/packages/forms/tsconfig.json
@@ -0,0 +1,12 @@
1{
2 "extends": "../../tsconfig.settings.json",
3 "compilerOptions": {
4 "outDir": "lib",
5 "rootDir": "src"
6 },
7 "references": [
8 {
9 "path": "../theme"
10 }
11 ]
12}
diff --git a/packages/forms/tslint.json b/packages/forms/tslint.json
new file mode 100644
index 000000000..0946f2096
--- /dev/null
+++ b/packages/forms/tslint.json
@@ -0,0 +1,3 @@
1{
2 "extends": "../../tslint.json"
3}