aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-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.tsx193
-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
-rw-r--r--packages/misty.yml11
-rw-r--r--packages/theme/.gitignore2
-rw-r--r--packages/theme/README.md11
-rw-r--r--packages/theme/package-lock.json52
-rw-r--r--packages/theme/package.json29
-rw-r--r--packages/theme/src/index.ts18
-rw-r--r--packages/theme/src/themes/dark/index.ts65
-rw-r--r--packages/theme/src/themes/default/index.ts142
-rw-r--r--packages/theme/src/themes/legacy/index.ts38
-rw-r--r--packages/theme/test/index.test.js17
-rw-r--r--packages/theme/tsconfig.json7
-rw-r--r--packages/theme/tslint.json3
-rw-r--r--packages/typings/package.json22
-rw-r--r--packages/typings/types/mobx-react-form.d.ts1
-rw-r--r--packages/typings/types/react-html-attributes.d.ts1
-rw-r--r--packages/typings/types/react-jss.d.ts1
-rw-r--r--packages/typings/types/react-loader.d.ts45
-rw-r--r--packages/ui/.gitignore2
-rw-r--r--packages/ui/package-lock.json207
-rw-r--r--packages/ui/package.json38
-rw-r--r--packages/ui/src/badge/index.tsx76
-rw-r--r--packages/ui/src/headline/index.tsx71
-rw-r--r--packages/ui/src/icon/index.tsx55
-rw-r--r--packages/ui/src/index.ts5
-rw-r--r--packages/ui/src/infobox/index.tsx192
-rw-r--r--packages/ui/src/loader/index.tsx48
-rw-r--r--packages/ui/src/typings/generic.ts10
-rw-r--r--packages/ui/tsconfig.json12
-rw-r--r--packages/ui/tslint.json3
48 files changed, 2780 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..e78929777
--- /dev/null
+++ b/packages/forms/package.json
@@ -0,0 +1,39 @@
1{
2 "name": "@meetfranz/forms",
3 "version": "1.0.9",
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..478738cad
--- /dev/null
+++ b/packages/forms/src/input/index.tsx
@@ -0,0 +1,193 @@
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 onFocus: () => {},
41 scorePassword: false,
42 showLabel: true,
43 showPasswordToggle: false,
44 type: 'text',
45 disabled: false,
46 };
47
48 state = {
49 passwordScore: 0,
50 showPassword: false,
51 };
52
53 private inputRef = createRef<HTMLInputElement>();
54
55 componentDidMount() {
56 const { focus, data } = this.props;
57
58 if (this.inputRef && this.inputRef.current) {
59 if (focus) {
60 this.inputRef.current.focus();
61 }
62
63 if (data) {
64 Object.keys(data).map(key => this.inputRef.current!.dataset[key] = data[key]);
65 }
66 }
67 }
68
69 onChange(e: React.ChangeEvent<HTMLInputElement>) {
70 const {
71 scorePassword,
72 onChange,
73 } = this.props;
74
75 if (onChange) {
76 onChange(e);
77 }
78
79 if (this.inputRef && this.inputRef.current && scorePassword) {
80 this.setState({ passwordScore: scorePasswordFunc(this.inputRef.current.value) });
81 }
82 }
83
84 render() {
85 const {
86 classes,
87 className,
88 disabled,
89 error,
90 id,
91 inputClassName,
92 label,
93 prefix,
94 scorePassword,
95 suffix,
96 showLabel,
97 showPasswordToggle,
98 type,
99 value,
100 name,
101 placeholder,
102 spellCheck,
103 onBlur,
104 onFocus,
105 } = this.props;
106
107 const {
108 showPassword,
109 passwordScore,
110 } = this.state;
111
112 const inputType = type === 'password' && showPassword ? 'text' : type;
113
114 return (
115 <Wrapper
116 className={className}
117 identifier="franz-input"
118 >
119 <Label
120 title={label}
121 showLabel={showLabel}
122 htmlFor={id}
123 >
124 <div
125 className={classnames({
126 [`${inputClassName}`]: inputClassName,
127 [`${classes.hasPasswordScore}`]: scorePassword,
128 [`${classes.wrapper}`]: true,
129 [`${classes.disabled}`]: disabled,
130 [`${classes.hasError}`]: error,
131 })}>
132 {prefix && (
133 <span className={classes.prefix}>
134 {prefix}
135 </span>
136 )}
137 <input
138 id={id}
139 type={inputType}
140 name={name}
141 value={value as string}
142 placeholder={placeholder}
143 spellCheck={spellCheck}
144 className={classes.input}
145 ref={this.inputRef}
146 onChange={this.onChange.bind(this)}
147 onFocus={onFocus}
148 onBlur={onBlur}
149 disabled={disabled}
150 />
151 {suffix && (
152 <span className={classes.suffix}>
153 {suffix}
154 </span>
155 )}
156 {showPasswordToggle && (
157 <button
158 type="button"
159 className={classes.formModifier}
160 onClick={() => this.setState(prevState => ({ showPassword: !prevState.showPassword }))}
161 tabIndex={-1}
162 >
163 <Icon
164 path={!showPassword ? mdiEye : mdiEyeOff}
165 size={1}
166 />
167 </button>
168 )}
169 </div>
170 {scorePassword && (
171 <div className={classnames({
172 [`${classes.passwordScore}`]: true,
173 [`${classes.hasError}`]: error,
174 })}>
175 <meter
176 value={passwordScore < 5 ? 5 : passwordScore}
177 low={30}
178 high={75}
179 optimum={100}
180 max={100}
181 />
182 </div>
183 )}
184 </Label>
185 {error && (
186 <Error message={error} />
187 )}
188 </Wrapper>
189 );
190 }
191}
192
193export 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}
diff --git a/packages/misty.yml b/packages/misty.yml
new file mode 100644
index 000000000..2d8cff014
--- /dev/null
+++ b/packages/misty.yml
@@ -0,0 +1,11 @@
1theme:
2 cwd: ./theme
3 cmd: npm run dev
4
5forms:
6 cwd: ./forms
7 cmd: npm run dev
8
9ui:
10 cwd: ./ui
11 cmd: npm run dev
diff --git a/packages/theme/.gitignore b/packages/theme/.gitignore
new file mode 100644
index 000000000..d01826a6b
--- /dev/null
+++ b/packages/theme/.gitignore
@@ -0,0 +1,2 @@
1node_modules/
2lib
diff --git a/packages/theme/README.md b/packages/theme/README.md
new file mode 100644
index 000000000..6f9cc406b
--- /dev/null
+++ b/packages/theme/README.md
@@ -0,0 +1,11 @@
1# `theme`
2
3> TODO: description
4
5## Usage
6
7```
8const theme = require('theme');
9
10// TODO: DEMONSTRATE API
11```
diff --git a/packages/theme/package-lock.json b/packages/theme/package-lock.json
new file mode 100644
index 000000000..f74af2f24
--- /dev/null
+++ b/packages/theme/package-lock.json
@@ -0,0 +1,52 @@
1{
2 "name": "@meetfranz/theme",
3 "version": "1.0.4",
4 "lockfileVersion": 1,
5 "requires": true,
6 "dependencies": {
7 "color": {
8 "version": "3.1.0",
9 "resolved": "https://registry.npmjs.org/color/-/color-3.1.0.tgz",
10 "integrity": "sha512-CwyopLkuRYO5ei2EpzpIh6LqJMt6Mt+jZhO5VI5f/wJLZriXQE32/SSqzmrh+QB+AZT81Cj8yv+7zwToW8ahZg==",
11 "requires": {
12 "color-convert": "^1.9.1",
13 "color-string": "^1.5.2"
14 }
15 },
16 "color-convert": {
17 "version": "1.9.3",
18 "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
19 "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
20 "requires": {
21 "color-name": "1.1.3"
22 }
23 },
24 "color-name": {
25 "version": "1.1.3",
26 "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
27 "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
28 },
29 "color-string": {
30 "version": "1.5.3",
31 "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz",
32 "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==",
33 "requires": {
34 "color-name": "^1.0.0",
35 "simple-swizzle": "^0.2.2"
36 }
37 },
38 "is-arrayish": {
39 "version": "0.3.2",
40 "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
41 "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
42 },
43 "simple-swizzle": {
44 "version": "0.2.2",
45 "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
46 "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
47 "requires": {
48 "is-arrayish": "^0.3.1"
49 }
50 }
51 }
52}
diff --git a/packages/theme/package.json b/packages/theme/package.json
new file mode 100644
index 000000000..5c21a787f
--- /dev/null
+++ b/packages/theme/package.json
@@ -0,0 +1,29 @@
1{
2 "name": "@meetfranz/theme",
3 "version": "1.0.9",
4 "description": "Theme configuration for Franz",
5 "author": "Stefan Malzner <stefan@adlk.io>",
6 "homepage": "https://github.com/meetfranz/franz",
7 "license": "Apache-2.0",
8 "main": "lib/index.js",
9 "publishConfig": {
10 "access": "public"
11 },
12 "repository": {
13 "type": "git",
14 "url": "git+https://github.com/meetfranz/franz.git"
15 },
16 "scripts": {
17 "dev": "tsc -w",
18 "prepare": "tsc",
19 "preprepare": "npm run test",
20 "test": "npx mocha"
21 },
22 "bugs": {
23 "url": "https://github.com/meetfranz/franz/issues"
24 },
25 "dependencies": {
26 "color": "^3.1.0"
27 },
28 "gitHead": "14b151cad6a5a849bb476aaa3fc53bf1eead7f4b"
29}
diff --git a/packages/theme/src/index.ts b/packages/theme/src/index.ts
new file mode 100644
index 000000000..524a9edf1
--- /dev/null
+++ b/packages/theme/src/index.ts
@@ -0,0 +1,18 @@
1import * as darkThemeConfig from './themes/dark';
2import * as defaultThemeConfig from './themes/default';
3import * as legacyStyles from './themes/legacy';
4
5export enum ThemeType {
6 default = 'default',
7 dark = 'dark',
8}
9
10export function theme(themeId: ThemeType) {
11 if (themeId === ThemeType.dark) {
12 return Object.assign({}, defaultThemeConfig, darkThemeConfig, { legacyStyles });
13 }
14
15 return Object.assign({}, defaultThemeConfig, { legacyStyles });
16}
17
18export type Theme = typeof defaultThemeConfig;
diff --git a/packages/theme/src/themes/dark/index.ts b/packages/theme/src/themes/dark/index.ts
new file mode 100644
index 000000000..3a56719b2
--- /dev/null
+++ b/packages/theme/src/themes/dark/index.ts
@@ -0,0 +1,65 @@
1import color from 'color';
2
3import * as defaultStyles from '../default';
4import * as legacyStyles from '../legacy';
5
6export const colorBackground = legacyStyles.darkThemeGrayDarkest;
7export const colorContentBackground = legacyStyles.darkThemeGrayDarkest;
8export const colorBackgroundSubscriptionContainer = legacyStyles.themeBrandInfo;
9
10export const colorHeadline = legacyStyles.darkThemeTextColor;
11export const colorText = legacyStyles.darkThemeTextColor;
12
13// Loader
14export const colorFullscreenLoaderSpinner = '#FFF';
15export const colorWebviewLoaderBackground = color(legacyStyles.darkThemeGrayDarkest).alpha(0.5).rgb().string();
16
17// Input
18export const labelColor = legacyStyles.darkThemeTextColor;
19export const inputColor = legacyStyles.darkThemeGrayLightest;
20export const inputBackground = legacyStyles.themeGrayDark;
21export const inputBorder = `1px solid ${legacyStyles.darkThemeGrayLight}`;
22export const inputPrefixColor = color(legacyStyles.darkThemeGrayLighter).lighten(0.3).hex();
23export const inputPrefixBackground = legacyStyles.darkThemeGray;
24export const inputDisabledOpacity = 0.5;
25export const inputScorePasswordBackground = legacyStyles.darkThemeGrayDark;
26export const inputModifierColor = color(legacyStyles.darkThemeGrayLighter).lighten(0.3).hex();
27export const inputPlaceholderColor = color(legacyStyles.darkThemeGrayLighter).darken(0.1).hex();
28
29// Toggle
30export const toggleBackground = legacyStyles.darkThemeGray;
31export const toggleButton = legacyStyles.darkThemeGrayLighter;
32
33// Button
34export const buttonPrimaryTextColor = legacyStyles.darkThemeTextColor;
35
36export const buttonSecondaryBackground = legacyStyles.darkThemeGrayLighter;
37export const buttonSecondaryTextColor = legacyStyles.darkThemeTextColor;
38
39export const buttonDangerTextColor = legacyStyles.darkThemeTextColor;
40
41export const buttonWarningTextColor = legacyStyles.darkThemeTextColor;
42
43export const buttonLoaderColor = {
44 primary: '#FFF',
45 secondary: buttonSecondaryTextColor,
46 success: '#FFF',
47 warning: '#FFF',
48 danger: '#FFF',
49 inverted: defaultStyles.brandPrimary,
50};
51
52// Select
53export const selectBackground = inputBackground;
54export const selectBorder = inputBorder;
55export const selectColor = inputColor;
56export const selectToggleColor = inputPrefixColor;
57export const selectPopupBackground = legacyStyles.darkThemeGrayLight;
58export const selectOptionColor = '#FFF';
59export const selectOptionBorder = `1px solid ${color(legacyStyles.darkThemeGrayLight).darken(0.2).hex()}`;
60export const selectOptionItemHover = color(legacyStyles.darkThemeGrayLight).darken(0.2).hex();
61export const selectOptionItemHoverColor = selectColor;
62export const selectSearchColor = inputBackground;
63
64// Modal
65export const colorModalOverlayBackground = color(legacyStyles.darkThemeBlack).alpha(0.8).rgb().string();
diff --git a/packages/theme/src/themes/default/index.ts b/packages/theme/src/themes/default/index.ts
new file mode 100644
index 000000000..8a71e61cf
--- /dev/null
+++ b/packages/theme/src/themes/default/index.ts
@@ -0,0 +1,142 @@
1import color from 'color';
2
3import * as legacyStyles from '../legacy';
4
5export interface IStyleTypes {
6 [index: string]: {
7 accent: string;
8 contrast: string;
9 border?: string;
10 };
11}
12
13export const brandPrimary = '#3498db';
14export const brandSuccess = '#5cb85c';
15export const brandInfo = '#5bc0de';
16export const brandWarning = '#FF9F00';
17export const brandDanger = '#d9534f';
18
19export const uiFontSize = 14;
20
21export const borderRadius = legacyStyles.themeBorderRadius;
22export const borderRadiusSmall = legacyStyles.themeBorderRadiusSmall;
23
24export const colorBackground = legacyStyles.themeGrayLighter;
25export const colorContentBackground = '#FFFFFF';
26export const colorHeadline = legacyStyles.themeGrayDark;
27
28export const colorText = legacyStyles.themeTextColor;
29
30// Subscription Container Component
31export const colorSubscriptionContainerBackground = 'none';
32export const colorSubscriptionContainerBorder = `1px solid ${brandPrimary}`;
33export const colorSubscriptionContainerTitle = brandPrimary;
34export const colorSubscriptionContainerActionButtonBackground = brandPrimary;
35export const colorSubscriptionContainerActionButtonColor = '#FFF';
36
37// Loader
38export const colorAppLoaderSpinner = '#FFF';
39export const colorFullscreenLoaderSpinner = legacyStyles.themeGrayDark;
40export const colorWebviewLoaderBackground = color(legacyStyles.themeGrayLighter).alpha(0.8).rgb().string();
41
42// Input
43export const labelColor = legacyStyles.themeGrayLight;
44export const inputColor = legacyStyles.themeGray;
45export const inputHeight = '35px';
46export const inputBackground = legacyStyles.themeGrayLightest;
47export const inputBorder = `1px solid ${legacyStyles.themeGrayLighter}`;
48export const inputModifierColor = legacyStyles.themeGrayLight;
49export const inputPlaceholderColor = color(legacyStyles.themeGrayLight).lighten(0.3).hex();
50export const inputPrefixColor = legacyStyles.themeGrayLight;
51export const inputPrefixBackground = legacyStyles.themeGrayLighter;
52export const inputDisabledOpacity = 0.5;
53export const inputScorePasswordBackground = legacyStyles.themeGrayLighter;
54
55// Toggle
56export const toggleBackground = legacyStyles.themeGrayLighter;
57export const toggleButton = legacyStyles.themeGrayLight;
58export const toggleButtonActive = brandPrimary;
59export const toggleWidth = 40;
60export const toggleHeight = 14;
61
62// Style Types
63export const styleTypes: IStyleTypes = {
64 primary: {
65 accent: brandPrimary,
66 contrast: '#FFF',
67 },
68 secondary: {
69 accent: legacyStyles.themeGrayLighter,
70 contrast: legacyStyles.themeGray,
71 },
72 success: {
73 accent: brandSuccess,
74 contrast: '#FFF',
75 },
76 warning: {
77 accent: brandWarning,
78 contrast: '#FFF',
79 },
80 danger: {
81 accent: brandDanger,
82 contrast: '#FFF',
83 },
84 inverted: {
85 accent: 'none',
86 contrast: brandPrimary,
87 border: `1px solid ${brandPrimary}`,
88 },
89};
90
91// Button
92export const buttonPrimaryBackground = brandPrimary;
93export const buttonPrimaryTextColor = '#FFF';
94
95export const buttonSecondaryBackground = legacyStyles.themeGrayLighter;
96export const buttonSecondaryTextColor = legacyStyles.themeGray;
97
98export const buttonSuccessBackground = brandSuccess;
99export const buttonSuccessTextColor = '#FFF';
100
101export const buttonDangerBackground = brandDanger;
102export const buttonDangerTextColor = '#FFF';
103
104export const buttonWarningBackground = brandWarning;
105export const buttonWarningTextColor = '#FFF';
106
107export const buttonInvertedBackground = 'none';
108export const buttonInvertedTextColor = brandPrimary;
109export const buttonInvertedBorder = `1px solid ${brandPrimary}`;
110
111export const buttonLoaderColor = {
112 primary: '#FFF',
113 secondary: buttonSecondaryTextColor,
114 success: '#FFF',
115 warning: '#FFF',
116 danger: '#FFF',
117 inverted: brandPrimary,
118};
119
120// Select
121export const selectBackground = inputBackground;
122export const selectBorder = inputBorder;
123export const selectHeight = inputHeight;
124export const selectColor = inputColor;
125export const selectToggleColor = inputPrefixColor;
126export const selectPopupBackground = '#FFF';
127export const selectOptionColor = inputColor;
128export const selectOptionBorder = `1px solid ${legacyStyles.themeGrayLightest}`;
129export const selectOptionItemHover = legacyStyles.themeGrayLighter;
130export const selectOptionItemHoverColor = selectColor;
131export const selectOptionItemActive = brandPrimary;
132export const selectOptionItemActiveColor = '#FFF';
133export const selectSearchBackground = legacyStyles.themeGrayLighter;
134export const selectSearchColor = inputColor;
135export const selectDisabledOpacity = inputDisabledOpacity;
136
137// Badge
138export const badgeFontSize = uiFontSize - 2;
139export const badgeBorderRadius = 50;
140
141// Modal
142export const colorModalOverlayBackground = color('#000').alpha(0.5).rgb().string();
diff --git a/packages/theme/src/themes/legacy/index.ts b/packages/theme/src/themes/legacy/index.ts
new file mode 100644
index 000000000..2114b92c1
--- /dev/null
+++ b/packages/theme/src/themes/legacy/index.ts
@@ -0,0 +1,38 @@
1/* legacy config, injected into sass */
2export const themeBrandPrimary = '#3498db';
3export const themeBrandSuccess = '#5cb85c';
4export const themeBrandInfo = '#5bc0de';
5export const themeBrandWarning = '#FF9F00';
6export const themeBrandDanger = '#d9534f';
7
8export const themeGrayDark = '#373a3c';
9export const themeGray = '#55595c';
10export const themeGrayLight = '#818a91';
11export const themeGrayLighter = '#eceeef';
12export const themeGrayLightest = '#f7f7f9';
13
14export const themeBorderRadius = '6px';
15export const themeBorderRadiusSmall = '3px';
16
17export const themeSidebarWidth = '68px';
18
19export const themeTextColor = themeGrayDark;
20
21export const themeTransitionTime = '.5s';
22
23export const themeInsetShadow = 'inset 0 2px 5px rgba(0, 0, 0, .03)';
24
25export const darkThemeBlack = '#1A1A1A';
26
27export const darkThemeGrayDarkest = '#1E1E1E';
28export const darkThemeGrayDarker = '#2D2F31';
29export const darkThemeGrayDark = '#383A3B';
30
31export const darkThemeGray = '#47494B';
32
33export const darkThemeGrayLight = '#515355';
34export const darkThemeGrayLighter = '#8a8b8b';
35export const darkThemeGrayLightest = '#FFFFFF';
36
37export const darkThemeGraySmoke = '#CED0D1';
38export const darkThemeTextColor = '#FFFFFF';
diff --git a/packages/theme/test/index.test.js b/packages/theme/test/index.test.js
new file mode 100644
index 000000000..3906433c1
--- /dev/null
+++ b/packages/theme/test/index.test.js
@@ -0,0 +1,17 @@
1const expect = require('expect.js');
2
3const { colorBackground: colorBackgroundDefault } = require('../lib/themes/default');
4const { colorBackground: colorBackgroundDark } = require('../lib/themes/dark');
5const { default: theme } = require('../lib');
6
7describe('Load theme', () => {
8 it('Should load default theme', () => {
9 const { colorBackground } = theme('default');
10 expect(colorBackground).to.be(colorBackgroundDefault);
11 });
12
13 it('Should load dark theme', () => {
14 const { colorBackground } = theme('dark');
15 expect(colorBackground).to.be(colorBackgroundDark);
16 });
17});
diff --git a/packages/theme/tsconfig.json b/packages/theme/tsconfig.json
new file mode 100644
index 000000000..d80ee9ee7
--- /dev/null
+++ b/packages/theme/tsconfig.json
@@ -0,0 +1,7 @@
1{
2 "extends": "../../tsconfig.settings.json",
3 "compilerOptions": {
4 "outDir": "lib",
5 "rootDir": "src"
6 },
7}
diff --git a/packages/theme/tslint.json b/packages/theme/tslint.json
new file mode 100644
index 000000000..0946f2096
--- /dev/null
+++ b/packages/theme/tslint.json
@@ -0,0 +1,3 @@
1{
2 "extends": "../../tslint.json"
3}
diff --git a/packages/typings/package.json b/packages/typings/package.json
new file mode 100644
index 000000000..54257c54c
--- /dev/null
+++ b/packages/typings/package.json
@@ -0,0 +1,22 @@
1{
2 "name": "@meetfranz/typings",
3 "version": "0.0.7",
4 "description": "TypeScript typings for internal and external projects",
5 "author": "Stefan Malzner <stefan@adlk.io>",
6 "homepage": "https://github.com/meetfranz/franz",
7 "license": "Apache-2.0",
8 "directories": {
9 "types": "types"
10 },
11 "publishConfig": {
12 "access": "public"
13 },
14 "repository": {
15 "type": "git",
16 "url": "git+https://github.com/meetfranz/franz.git"
17 },
18 "bugs": {
19 "url": "https://github.com/meetfranz/franz/issues"
20 },
21 "gitHead": "14b151cad6a5a849bb476aaa3fc53bf1eead7f4b"
22}
diff --git a/packages/typings/types/mobx-react-form.d.ts b/packages/typings/types/mobx-react-form.d.ts
new file mode 100644
index 000000000..4e19dc1c2
--- /dev/null
+++ b/packages/typings/types/mobx-react-form.d.ts
@@ -0,0 +1 @@
declare module 'mobx-react-form';
diff --git a/packages/typings/types/react-html-attributes.d.ts b/packages/typings/types/react-html-attributes.d.ts
new file mode 100644
index 000000000..6f8f20fe4
--- /dev/null
+++ b/packages/typings/types/react-html-attributes.d.ts
@@ -0,0 +1 @@
declare module 'react-html-attributes';
diff --git a/packages/typings/types/react-jss.d.ts b/packages/typings/types/react-jss.d.ts
new file mode 100644
index 000000000..9a77ddb87
--- /dev/null
+++ b/packages/typings/types/react-jss.d.ts
@@ -0,0 +1 @@
declare module 'react-jss';
diff --git a/packages/typings/types/react-loader.d.ts b/packages/typings/types/react-loader.d.ts
new file mode 100644
index 000000000..8dc36b71f
--- /dev/null
+++ b/packages/typings/types/react-loader.d.ts
@@ -0,0 +1,45 @@
1// Type definitions for react-loader 2.4
2// Project: https://github.com/quickleft/react-loader
3// Definitions by: Sudarsan Balaji <https://github.com/artfuldev>
4// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5// TypeScript Version: 2.8
6
7import { Component } from 'react';
8
9interface LoaderOptions {
10 lines?: number;
11 length?: number;
12 width?: number;
13 radius?: number;
14 scale?: number;
15 corners?: number;
16 color?: string;
17 opacity?: number;
18 rotate?: number;
19 direction?: number;
20 speed?: number;
21 trail?: number;
22 fps?: number;
23 zIndex?: number;
24 top?: string;
25 left?: string;
26 shadow?: boolean;
27 hwaccel?: boolean;
28 position?: string;
29 loadedClassName?: string;
30 parentClassName?: string;
31}
32
33interface LoaderProps extends LoaderOptions {
34 loaded: boolean;
35 options?: LoaderOptions;
36 className?: string;
37}
38
39declare class ReactLoader extends Component<LoaderProps> {
40}
41
42declare namespace ReactLoader {
43}
44
45export = ReactLoader;
diff --git a/packages/ui/.gitignore b/packages/ui/.gitignore
new file mode 100644
index 000000000..d01826a6b
--- /dev/null
+++ b/packages/ui/.gitignore
@@ -0,0 +1,2 @@
1node_modules/
2lib
diff --git a/packages/ui/package-lock.json b/packages/ui/package-lock.json
new file mode 100644
index 000000000..8fa68a29b
--- /dev/null
+++ b/packages/ui/package-lock.json
@@ -0,0 +1,207 @@
1{
2 "name": "@meetfranz/ui",
3 "version": "0.0.0",
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 "iconv-lite": {
106 "version": "0.4.24",
107 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
108 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
109 "requires": {
110 "safer-buffer": ">= 2.1.2 < 3"
111 }
112 },
113 "is-stream": {
114 "version": "1.1.0",
115 "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
116 "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
117 },
118 "isomorphic-fetch": {
119 "version": "2.2.1",
120 "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
121 "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
122 "requires": {
123 "node-fetch": "^1.0.1",
124 "whatwg-fetch": ">=0.10.0"
125 }
126 },
127 "js-tokens": {
128 "version": "4.0.0",
129 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
130 "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
131 },
132 "loose-envify": {
133 "version": "1.4.0",
134 "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
135 "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
136 "requires": {
137 "js-tokens": "^3.0.0 || ^4.0.0"
138 }
139 },
140 "node-fetch": {
141 "version": "1.7.3",
142 "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
143 "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
144 "requires": {
145 "encoding": "^0.1.11",
146 "is-stream": "^1.0.1"
147 }
148 },
149 "object-assign": {
150 "version": "4.1.1",
151 "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
152 "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
153 },
154 "promise": {
155 "version": "7.3.1",
156 "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
157 "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
158 "requires": {
159 "asap": "~2.0.3"
160 }
161 },
162 "prop-types": {
163 "version": "15.6.2",
164 "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
165 "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
166 "requires": {
167 "loose-envify": "^1.3.1",
168 "object-assign": "^4.1.1"
169 }
170 },
171 "react-loader": {
172 "version": "2.4.5",
173 "resolved": "https://registry.npmjs.org/react-loader/-/react-loader-2.4.5.tgz",
174 "integrity": "sha1-zT5VHGzQc4wcDxPwc2VPk4KL5ak=",
175 "requires": {
176 "create-react-class": "^15.5.2",
177 "prop-types": "^15.5.8",
178 "spin.js": "2.x"
179 }
180 },
181 "safer-buffer": {
182 "version": "2.1.2",
183 "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
184 "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
185 },
186 "setimmediate": {
187 "version": "1.0.5",
188 "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
189 "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
190 },
191 "spin.js": {
192 "version": "2.3.2",
193 "resolved": "https://registry.npmjs.org/spin.js/-/spin.js-2.3.2.tgz",
194 "integrity": "sha1-bKpW1SBnNFD9XPvGlx5tB3LDeho="
195 },
196 "ua-parser-js": {
197 "version": "0.7.19",
198 "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz",
199 "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ=="
200 },
201 "whatwg-fetch": {
202 "version": "3.0.0",
203 "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
204 "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
205 }
206 }
207}
diff --git a/packages/ui/package.json b/packages/ui/package.json
new file mode 100644
index 000000000..62a1e5a6e
--- /dev/null
+++ b/packages/ui/package.json
@@ -0,0 +1,38 @@
1{
2 "name": "@meetfranz/ui",
3 "version": "0.0.4",
4 "description": "React UI 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-loader": "^2.4.5"
30 },
31 "peerDependencies": {
32 "classnames": "^2.2.6",
33 "react": "^16.7.0",
34 "react-dom": "16.7.0",
35 "react-jss": "^8.6.1"
36 },
37 "gitHead": "14b151cad6a5a849bb476aaa3fc53bf1eead7f4b"
38}
diff --git a/packages/ui/src/badge/index.tsx b/packages/ui/src/badge/index.tsx
new file mode 100644
index 000000000..fc52ecc73
--- /dev/null
+++ b/packages/ui/src/badge/index.tsx
@@ -0,0 +1,76 @@
1import { Theme } from '@meetfranz/theme';
2import classnames from 'classnames';
3import React, { Component } from 'react';
4import injectStyle from 'react-jss';
5
6import { IWithStyle } from '../typings/generic';
7
8interface IProps extends IWithStyle {
9 type: string;
10 className?: string;
11 children: React.ReactNode;
12}
13
14const badgeStyles = (theme: Theme) => {
15 const styles = {};
16 Object.keys(theme.styleTypes).map((style) => {
17 Object.assign(styles, {
18 [style]: {
19 background: theme.styleTypes[style].accent,
20 color: theme.styleTypes[style].contrast,
21 border: theme.styleTypes[style].border,
22 },
23 });
24 });
25
26 return styles;
27};
28
29const styles = (theme: Theme) => ({
30 badge: {
31 display: 'inline-block',
32 padding: [3, 8, 4],
33 fontSize: theme.badgeFontSize,
34 borderRadius: theme.badgeBorderRadius,
35 margin: [0, 4],
36
37 '&:first-child': {
38 marginLeft: 0,
39 },
40
41 '&:last-child': {
42 marginRight: 0,
43 },
44 },
45 ...badgeStyles(theme),
46});
47
48class BadgeComponent extends Component<IProps> {
49 public static defaultProps = {
50 type: 'primary',
51 };
52
53 render() {
54 const {
55 classes,
56 children,
57 type,
58 className,
59 } = this.props;
60
61 return (
62 <div
63 className={classnames({
64 [classes.badge]: true,
65 [classes[type]]: true,
66 [`${className}`]: className,
67 })}
68 data-type="franz-badge"
69 >
70 {children}
71 </div>
72 );
73 }
74}
75
76export const Badge = injectStyle(styles)(BadgeComponent);
diff --git a/packages/ui/src/headline/index.tsx b/packages/ui/src/headline/index.tsx
new file mode 100644
index 000000000..7eabfcf80
--- /dev/null
+++ b/packages/ui/src/headline/index.tsx
@@ -0,0 +1,71 @@
1import { Theme } from '@meetfranz/theme';
2import classnames from 'classnames';
3import React, { Component } from 'react';
4import injectStyle from 'react-jss';
5
6import { IWithStyle, Omit } from '../typings/generic';
7
8interface IProps extends IWithStyle {
9 level?: number;
10 className?: string;
11 children: string | React.ReactNode;
12 id?: string;
13}
14
15const styles = (theme: Theme) => ({
16 headline: {
17 fontWeight: 'lighter',
18 color: theme.colorText,
19 marginTop: 0,
20 marginBottom: 10,
21 textAlign: 'left',
22 },
23 h1: {
24 fontSize: 30,
25 marginTop: 0,
26 },
27 h2: {
28 fontSize: 20,
29 },
30 h3: {
31 fontSize: 18,
32 },
33 h4: {
34 fontSize: theme.uiFontSize,
35 },
36});
37
38class HeadlineComponent extends Component<IProps> {
39 render() {
40 const {
41 classes,
42 level,
43 className,
44 children,
45 id,
46 } = this.props;
47
48 return React.createElement(
49 `h${level}`,
50 {
51 id,
52 className: classnames({
53 [classes.headline]: true,
54 [classes[level ? `h${level}` : 'h1']]: true,
55 [`${className}`]: className,
56 }),
57 'data-type': 'franz-headline',
58 },
59 children,
60 );
61 }
62}
63
64const Headline = injectStyle(styles)(HeadlineComponent);
65
66const createH = (level: number) => (props: Omit<IProps, 'classes' | 'theme'>) => <Headline level={level} {...props}>{props.children}</Headline>;
67
68export const H1 = createH(1);
69export const H2 = createH(2);
70export const H3 = createH(3);
71export const H4 = createH(4);
diff --git a/packages/ui/src/icon/index.tsx b/packages/ui/src/icon/index.tsx
new file mode 100644
index 000000000..e30d3396d
--- /dev/null
+++ b/packages/ui/src/icon/index.tsx
@@ -0,0 +1,55 @@
1import * as mdiIcons from '@mdi/js';
2import MdiIcon from '@mdi/react';
3import { Theme } from '@meetfranz/theme';
4import classnames from 'classnames';
5import React, { Component } from 'react';
6import injectStyle from 'react-jss';
7
8import { IWithStyle } from '../typings/generic';
9
10interface IProps extends IWithStyle {
11 icon: keyof typeof mdiIcons;
12 size?: number;
13 className?: string;
14}
15
16const styles = (theme: Theme) => ({
17 icon: {
18 fill: theme.colorText,
19 },
20});
21
22class IconComponent extends Component<IProps> {
23 public static defaultProps = {
24 size: 1,
25 };
26
27 render() {
28 const {
29 classes,
30 icon: iconName,
31 size,
32 className,
33 } = this.props;
34
35 let icon = '';
36 if (iconName && mdiIcons[iconName]) {
37 icon = mdiIcons[iconName];
38 } else if (iconName && !mdiIcons[iconName]) {
39 console.warn(`Icon '${iconName}' was not found`);
40 }
41
42 return (
43 <MdiIcon
44 path={icon}
45 size={size}
46 className={classnames({
47 [classes.icon]: true,
48 [`${className}`]: className,
49 })}
50 />
51 );
52 }
53}
54
55export const Icon = injectStyle(styles)(IconComponent);
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
new file mode 100644
index 000000000..1eeec5144
--- /dev/null
+++ b/packages/ui/src/index.ts
@@ -0,0 +1,5 @@
1export { Icon } from './icon';
2export { Infobox } from './infobox';
3export * from './headline';
4export { Loader } from './loader';
5export { Badge } from './badge';
diff --git a/packages/ui/src/infobox/index.tsx b/packages/ui/src/infobox/index.tsx
new file mode 100644
index 000000000..53ed16341
--- /dev/null
+++ b/packages/ui/src/infobox/index.tsx
@@ -0,0 +1,192 @@
1import { Theme } from '@meetfranz/theme';
2import classnames from 'classnames';
3import React, { Component } from 'react';
4import injectStyle from 'react-jss';
5
6import { Icon } from '../';
7import { IWithStyle } from '../typings/generic';
8
9interface IProps extends IWithStyle {
10 icon?: string;
11 type?: string;
12 dismissable?: boolean;
13 onDismiss?: () => void;
14 ctaOnClick?: () => void;
15 ctaLabel?: string;
16 ctaLoading?: boolean;
17 children: React.ReactNode;
18}
19
20interface IState {
21 isDismissing: boolean;
22 dismissed: boolean;
23}
24
25const buttonStyles = (theme: Theme) => {
26 const styles = {};
27 Object.keys(theme.styleTypes).map((style) => {
28 Object.assign(styles, {
29 [style]: {
30 background: theme.styleTypes[style].accent,
31 color: theme.styleTypes[style].contrast,
32 border: theme.styleTypes[style].border,
33
34 '& svg': {
35 fill: theme.styleTypes[style].contrast,
36 },
37 },
38 });
39 });
40
41 return styles;
42};
43
44const styles = (theme: Theme) => ({
45 wrapper: {
46 position: 'relative',
47 overflow: 'hidden',
48 },
49 infobox: {
50 alignItems: 'center',
51 borderRadius: theme.borderRadiusSmall,
52 display: 'flex',
53 height: 'auto',
54 marginBottom: 30,
55 padding: '15px 20px',
56 top: 0,
57 transition: 'all 0.5s',
58 opacity: 1,
59 },
60 dismissing: {
61 // position: 'absolute',
62 marginTop: -100,
63 opacity: 0,
64 },
65 content: {
66 flex: 1,
67 },
68 icon: {
69 marginRight: 10,
70 },
71 close: {
72 color: (props: IProps) => theme.styleTypes[props.type ? props.type : 'primary'].contrast,
73 marginRight: -5,
74 border: 0,
75 background: 'none',
76 },
77 cta: {
78 borderColor: (props: IProps) => theme.styleTypes[props.type ? props.type : 'primary'].contrast,
79 borderRadius: theme.borderRadiusSmall,
80 borderStyle: 'solid',
81 borderWidth: 1,
82 background: 'none',
83 color: (props: IProps) => theme.styleTypes[props.type ? props.type : 'primary'].contrast,
84 marginLeft: 15,
85 padding: [4, 10],
86 fontSize: theme.uiFontSize,
87 transition: 'opacity 0.3s',
88
89 '&:hover': {
90 opacity: 0.6,
91 },
92 },
93 ...buttonStyles(theme),
94});
95
96class InfoboxComponent extends Component<IProps, IState> {
97 public static defaultProps = {
98 type: 'primary',
99 dismissable: false,
100 ctaOnClick: () => {},
101 onDismiss: () => {},
102 ctaLabel: '',
103 ctaLoading: false,
104 };
105
106 state = {
107 isDismissing: false,
108 dismissed: false,
109 };
110
111 dismiss() {
112 const {
113 onDismiss,
114 } = this.props;
115
116 this.setState({
117 isDismissing: true,
118 });
119
120 if (onDismiss) {
121 onDismiss();
122 }
123
124 setTimeout(() => {
125 this.setState({
126 dismissed: true,
127 });
128 }, 3000);
129 }
130
131 render() {
132 const {
133 classes,
134 children,
135 icon,
136 type,
137 ctaLabel,
138 ctaLoading,
139 ctaOnClick,
140 dismissable,
141 } = this.props;
142
143 const {
144 isDismissing,
145 dismissed,
146 } = this.state;
147
148 if (dismissed) {
149 return null;
150 }
151
152 return (
153 <div className={classes.wrapper}>
154 <div
155 className={classnames({
156 [classes.infobox]: true,
157 [classes[`${type}`]]: type,
158 [classes.dismissing]: isDismissing,
159 })}
160 data-type="franz-infobox"
161 >
162 {icon && (
163 <Icon icon={icon} className={classes.icon} />
164 )}
165 <div className={classes.content}>
166 {children}
167 </div>
168 {ctaLabel && (
169 <button
170 className={classes.cta}
171 onClick={ctaOnClick}
172 type="button"
173 >
174 {ctaLabel}
175 </button>
176 )}
177 {dismissable && (
178 <button
179 type="button"
180 onClick={this.dismiss.bind(this)}
181 className={classes.close}
182 >
183 <Icon icon="mdiClose" />
184 </button>
185 )}
186 </div>
187 </div>
188 );
189 }
190}
191
192export const Infobox = injectStyle(styles)(InfoboxComponent);
diff --git a/packages/ui/src/loader/index.tsx b/packages/ui/src/loader/index.tsx
new file mode 100644
index 000000000..46545c786
--- /dev/null
+++ b/packages/ui/src/loader/index.tsx
@@ -0,0 +1,48 @@
1import { Theme } from '@meetfranz/theme';
2import classnames from 'classnames';
3import React, { Component } from 'react';
4import injectStyle, { withTheme } from 'react-jss';
5import ReactLoader from 'react-loader';
6
7import { IWithStyle } from '../typings/generic';
8
9interface IProps extends IWithStyle {
10 className?: string;
11}
12
13const styles = (theme: Theme) => ({
14 container: {
15 position: 'relative',
16 height: 60,
17 },
18});
19
20class LoaderComponent extends Component<IProps> {
21 render() {
22 const {
23 classes,
24 className,
25 theme,
26 } = this.props;
27
28 return (
29 <div
30 className={classnames({
31 [classes.container]: true,
32 [`${className}`]: className,
33 })}
34 data-type="franz-loader"
35 >
36 <ReactLoader
37 loaded={false}
38 width={4}
39 scale={0.75}
40 color={theme.colorText}
41 parentClassName={classes.loader}
42 />
43 </div>
44 );
45 }
46}
47
48export const Loader = injectStyle(styles)(withTheme(LoaderComponent));
diff --git a/packages/ui/src/typings/generic.ts b/packages/ui/src/typings/generic.ts
new file mode 100644
index 000000000..d5f953b9f
--- /dev/null
+++ b/packages/ui/src/typings/generic.ts
@@ -0,0 +1,10 @@
1import { Theme } from '@meetfranz/theme/lib';
2import { Classes } from 'jss';
3
4export interface IWithStyle {
5 classes: Classes;
6 theme: Theme;
7}
8
9export type Merge<M, N> = Omit<M, Extract<keyof M, keyof N>> & N;
10export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json
new file mode 100644
index 000000000..8b9507eac
--- /dev/null
+++ b/packages/ui/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/ui/tslint.json b/packages/ui/tslint.json
new file mode 100644
index 000000000..0946f2096
--- /dev/null
+++ b/packages/ui/tslint.json
@@ -0,0 +1,3 @@
1{
2 "extends": "../../tslint.json"
3}