aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorLibravatar Amine Mouafik <amine@mouafik.fr>2019-05-12 20:00:41 +0700
committerLibravatar Amine Mouafik <amine@mouafik.fr>2019-05-12 20:00:41 +0700
commitd8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61 (patch)
tree3974d449d8ef389fc61bf880ae758b5debc22a80 /packages
parentUse dark background in SVG logo (diff)
parentUpdate CHANGELOG.md (diff)
downloadferdium-app-d8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61.tar.gz
ferdium-app-d8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61.tar.zst
ferdium-app-d8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61.zip
Merge tag 'v5.1.0'
# Conflicts: # README.md # src/components/layout/AppLayout.js
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.tsx275
-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.tsx211
-rw-r--r--packages/forms/src/input/scorePassword.ts42
-rw-r--r--packages/forms/src/input/styles.ts101
-rw-r--r--packages/forms/src/label/index.tsx51
-rw-r--r--packages/forms/src/label/styles.ts12
-rw-r--r--packages/forms/src/select/index.tsx445
-rw-r--r--packages/forms/src/toggle/index.tsx117
-rw-r--r--packages/forms/src/typings/generic.ts17
-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.ts120
-rw-r--r--packages/theme/src/themes/default/index.ts209
-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/ProBadge.tsx64
-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.ts6
-rw-r--r--packages/ui/src/infobox/index.tsx204
-rw-r--r--packages/ui/src/loader/index.tsx50
-rw-r--r--packages/ui/src/typings/generic.ts10
-rw-r--r--packages/ui/tsconfig.json12
-rw-r--r--packages/ui/tslint.json3
49 files changed, 3016 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..0ac9a846f
--- /dev/null
+++ b/packages/forms/package.json
@@ -0,0 +1,39 @@
1{
2 "name": "@meetfranz/forms",
3 "version": "1.0.15",
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.13",
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": "e9b9079dc921e85961954727a7b2a8eabe5b9798"
39}
diff --git a/packages/forms/src/button/index.tsx b/packages/forms/src/button/index.tsx
new file mode 100644
index 000000000..6959cde73
--- /dev/null
+++ b/packages/forms/src/button/index.tsx
@@ -0,0 +1,275 @@
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 height: theme.buttonHeight,
48
49 '&:hover': {
50 opacity: 0.8,
51 },
52 '&:active': {
53 opacity: 0.5,
54 transition: 'none',
55 },
56 },
57 label: {
58 margin: '10px 20px',
59 width: '100%',
60 display: 'flex',
61 alignItems: 'center',
62 justifyContent: 'center',
63 },
64 primary: {
65 background: theme.buttonPrimaryBackground,
66 color: theme.buttonPrimaryTextColor,
67
68 '& svg': {
69 fill: theme.buttonPrimaryTextColor,
70 },
71 },
72 secondary: {
73 background: theme.buttonSecondaryBackground,
74 color: theme.buttonSecondaryTextColor,
75
76 '& svg': {
77 fill: theme.buttonSecondaryTextColor,
78 },
79 },
80 success: {
81 background: theme.buttonSuccessBackground,
82 color: theme.buttonSuccessTextColor,
83
84 '& svg': {
85 fill: theme.buttonSuccessTextColor,
86 },
87 },
88 danger: {
89 background: theme.buttonDangerBackground,
90 color: theme.buttonDangerTextColor,
91
92 '& svg': {
93 fill: theme.buttonDangerTextColor,
94 },
95 },
96 warning: {
97 background: theme.buttonWarningBackground,
98 color: theme.buttonWarningTextColor,
99
100 '& svg': {
101 fill: theme.buttonWarningTextColor,
102 },
103 },
104 inverted: {
105 background: theme.buttonInvertedBackground,
106 color: theme.buttonInvertedTextColor,
107 border: theme.buttonInvertedBorder,
108
109 '& svg': {
110 fill: theme.buttonInvertedTextColor,
111 },
112 },
113 disabled: {
114 opacity: theme.inputDisabledOpacity,
115 },
116 loader: {
117 position: 'relative' as CSS.PositionProperty,
118 width: 20,
119 height: 18,
120 zIndex: 9999,
121 },
122 loaderContainer: {
123 width: (props: IProps): string => (!props.busy ? '0' : '40px'),
124 height: 20,
125 overflow: 'hidden',
126 transition: 'all 0.3s',
127 marginLeft: (props: IProps): number => !props.busy ? 10 : 20,
128 marginRight: (props: IProps): number => !props.busy ? -10 : -20,
129 position: (props: IProps): CSS.PositionProperty => props.stretch ? 'absolute' : 'inherit',
130 },
131 icon: {
132 marginLeft: -5,
133 marginRight: 10,
134 },
135});
136
137class ButtonComponent extends Component<IProps> {
138 public static defaultProps = {
139 type: 'button',
140 disabled: false,
141 onClick: () => null,
142 buttonType: 'primary' as ButtonType,
143 stretch: false,
144 busy: false,
145 // target: '_self'
146 };
147
148 state = {
149 busy: false,
150 };
151
152 componentWillMount() {
153 this.setState({ busy: this.props.busy });
154 }
155
156 componentWillReceiveProps(nextProps: IProps) {
157 if (nextProps.busy !== this.props.busy) {
158 if (this.props.busy) {
159 setTimeout(() => {
160 this.setState({ busy: nextProps.busy });
161 }, 300);
162 } else {
163 this.setState({ busy: nextProps.busy });
164 }
165 }
166 }
167
168 render() {
169 const {
170 classes,
171 className,
172 theme,
173 disabled,
174 id,
175 label,
176 type,
177 onClick,
178 buttonType,
179 loaded,
180 icon: iconName,
181 busy: busyProp,
182 href,
183 target,
184 } = this.props;
185
186 const {
187 busy,
188 } = this.state;
189
190 let icon = '';
191 if (iconName && mdiIcons[iconName]) {
192 icon = mdiIcons[iconName];
193 } else if (iconName && !mdiIcons[iconName]) {
194 console.warn(`Icon '${iconName}' was not found`);
195 }
196
197 let showLoader = false;
198 if (loaded) {
199 showLoader = !loaded;
200 console.warn('Franz Button prop `loaded` will be deprecated in the future. Please use `busy` instead');
201 }
202 if (busy) {
203 showLoader = busy;
204 }
205
206 const content = (
207 <>
208 <div className={classes.loaderContainer}>
209 {showLoader && (
210 <Loader
211 loaded={false}
212 width={4}
213 scale={0.45}
214 color={theme.buttonLoaderColor[buttonType!]}
215 parentClassName={classes.loader}
216 />
217 )}
218 </div>
219 <div className={classes.label}>
220 {icon && (
221 <Icon
222 path={icon}
223 size={1}
224 className={classes.icon}
225 />
226 )}
227 {label}
228 </div>
229 </>
230 );
231
232 let wrapperComponent = null;
233
234 if (!href) {
235 wrapperComponent = (
236 <button
237 id={id}
238 type={type}
239 onClick={onClick}
240 className={classnames({
241 [`${classes.button}`]: true,
242 [`${classes[buttonType as ButtonType]}`]: true,
243 [`${classes.disabled}`]: disabled,
244 [`${className}`]: className,
245 })}
246 disabled={disabled}
247 data-type="franz-button"
248 >
249 {content}
250 </button>
251 );
252 } else {
253 wrapperComponent = (
254 <a
255 href={href}
256 target={target}
257 onClick={onClick}
258 className={classnames({
259 [`${classes.button}`]: true,
260 [`${classes[buttonType as ButtonType]}`]: true,
261 [`${className}`]: className,
262 })}
263 rel={target === '_blank' ? 'noopener' : ''}
264 data-type="franz-button"
265 >
266 {content}
267 </a>
268 );
269 }
270
271 return wrapperComponent;
272 }
273}
274
275export 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..a2d7c62d5
--- /dev/null
+++ b/packages/forms/src/input/index.tsx
@@ -0,0 +1,211 @@
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 onEnterKey?: Function;
29}
30
31interface IState {
32 showPassword: boolean;
33 passwordScore: number;
34}
35
36class InputComponent extends Component<IProps, IState> {
37 static defaultProps = {
38 focus: false,
39 onChange: () => {},
40 onBlur: () => {},
41 onFocus: () => {},
42 scorePassword: false,
43 showLabel: true,
44 showPasswordToggle: false,
45 type: 'text',
46 disabled: false,
47 };
48
49 state = {
50 passwordScore: 0,
51 showPassword: false,
52 };
53
54 private inputRef = createRef<HTMLInputElement>();
55
56 componentDidMount() {
57 const { focus, data } = this.props;
58
59 if (this.inputRef && this.inputRef.current) {
60 if (focus) {
61 this.inputRef.current.focus();
62 }
63
64 if (data) {
65 Object.keys(data).map(key => this.inputRef.current!.dataset[key] = data[key]);
66 }
67 }
68 }
69
70 onChange(e: React.ChangeEvent<HTMLInputElement>) {
71 const {
72 scorePassword,
73 onChange,
74 } = this.props;
75
76 if (onChange) {
77 onChange(e);
78 }
79
80 if (this.inputRef && this.inputRef.current && scorePassword) {
81 this.setState({ passwordScore: scorePasswordFunc(this.inputRef.current.value) });
82 }
83 }
84
85 onInputKeyPress(e: React.KeyboardEvent) {
86 if (e.key === "Enter") {
87 const { onEnterKey } = this.props;
88 onEnterKey && onEnterKey();
89 }
90 }
91
92 render() {
93 const {
94 classes,
95 className,
96 disabled,
97 error,
98 id,
99 inputClassName,
100 label,
101 prefix,
102 scorePassword,
103 suffix,
104 showLabel,
105 showPasswordToggle,
106 type,
107 value,
108 name,
109 placeholder,
110 spellCheck,
111 onBlur,
112 onFocus,
113 min,
114 max,
115 step,
116 required,
117 } = this.props;
118
119 const {
120 showPassword,
121 passwordScore,
122 } = this.state;
123
124 const inputType = type === 'password' && showPassword ? 'text' : type;
125
126 return (
127 <Wrapper
128 className={className}
129 identifier="franz-input"
130 >
131 <Label
132 title={label}
133 showLabel={showLabel}
134 htmlFor={id}
135 className={classes.label}
136 isRequired={required}
137 >
138 <div
139 className={classnames({
140 [`${inputClassName}`]: inputClassName,
141 [`${classes.hasPasswordScore}`]: scorePassword,
142 [`${classes.wrapper}`]: true,
143 [`${classes.disabled}`]: disabled,
144 [`${classes.hasError}`]: error,
145 })}>
146 {prefix && (
147 <span className={classes.prefix}>
148 {prefix}
149 </span>
150 )}
151 <input
152 id={id}
153 type={inputType}
154 name={name}
155 value={value as string}
156 placeholder={placeholder}
157 spellCheck={spellCheck}
158 className={classes.input}
159 ref={this.inputRef}
160 onChange={this.onChange.bind(this)}
161 onFocus={onFocus}
162 onBlur={onBlur}
163 disabled={disabled}
164 onKeyPress={this.onInputKeyPress.bind(this)}
165 min={min}
166 max={max}
167 step={step}
168 />
169 {suffix && (
170 <span className={classes.suffix}>
171 {suffix}
172 </span>
173 )}
174 {showPasswordToggle && (
175 <button
176 type="button"
177 className={classes.formModifier}
178 onClick={() => this.setState(prevState => ({ showPassword: !prevState.showPassword }))}
179 tabIndex={-1}
180 >
181 <Icon
182 path={!showPassword ? mdiEye : mdiEyeOff}
183 size={1}
184 />
185 </button>
186 )}
187 </div>
188 {scorePassword && (
189 <div className={classnames({
190 [`${classes.passwordScore}`]: true,
191 [`${classes.hasError}`]: error,
192 })}>
193 <meter
194 value={passwordScore < 5 ? 5 : passwordScore}
195 low={30}
196 high={75}
197 optimum={100}
198 max={100}
199 />
200 </div>
201 )}
202 </Label>
203 {error && (
204 <Error message={error} />
205 )}
206 </Wrapper>
207 );
208 }
209}
210
211export 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..e2ab30a4f
--- /dev/null
+++ b/packages/forms/src/input/styles.ts
@@ -0,0 +1,101 @@
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}px`,
8 padding: '0 10px',
9 fontSize: theme.uiFontSize,
10});
11
12export default (theme: Theme) => ({
13 label: {
14 '& > div': {
15 marginTop: 5,
16 }
17 },
18 disabled: {
19 opacity: theme.inputDisabledOpacity,
20 },
21 formModifier: {
22 background: 'none',
23 border: 0,
24 borderLeft: theme.inputBorder,
25 padding: '4px 20px 0',
26 outline: 'none',
27
28 '&:active': {
29 opacity: 0.5,
30 },
31
32 '& svg': {
33 fill: theme.inputModifierColor,
34 },
35 },
36 input: {
37 background: 'none',
38 border: 0,
39 fontSize: theme.uiFontSize,
40 outline: 'none',
41 padding: 8,
42 width: '100%',
43 color: theme.inputColor,
44
45 '&::placeholder': {
46 color: theme.inputPlaceholderColor,
47 },
48 },
49 passwordScore: {
50 background: theme.inputScorePasswordBackground,
51 border: theme.inputBorder,
52 borderTopWidth: 0,
53 borderBottomLeftRadius: theme.borderRadiusSmall,
54 borderBottomRightRadius: theme.borderRadiusSmall,
55 display: 'block',
56 flexBasis: '100%',
57 height: 5,
58 overflow: 'hidden',
59
60 '& meter': {
61 display: 'block',
62 height: '100%',
63 width: '100%',
64
65 '&::-webkit-meter-bar': {
66 background: 'none',
67 },
68
69 '&::-webkit-meter-even-less-good-value': {
70 background: theme.brandDanger,
71 },
72
73 '&::-webkit-meter-suboptimum-value': {
74 background: theme.brandWarning,
75 },
76
77 '&::-webkit-meter-optimum-value': {
78 background: theme.brandSuccess,
79 },
80 },
81 },
82 prefix: prefixStyles(theme),
83 suffix: prefixStyles(theme),
84 wrapper: {
85 background: theme.inputBackground,
86 border: theme.inputBorder,
87 borderRadius: theme.borderRadiusSmall,
88 boxSizing: 'border-box' as CSS.BoxSizingProperty,
89 display: 'flex',
90 height: theme.inputHeight,
91 order: 1,
92 width: '100%',
93 },
94 hasPasswordScore: {
95 borderBottomLeftRadius: 0,
96 borderBottomRightRadius: 0,
97 },
98 hasError: {
99 borderColor: theme.brandDanger,
100 },
101});
diff --git a/packages/forms/src/label/index.tsx b/packages/forms/src/label/index.tsx
new file mode 100644
index 000000000..1b33ba22c
--- /dev/null
+++ b/packages/forms/src/label/index.tsx
@@ -0,0 +1,51 @@
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 isRequired: boolean;
13}
14
15class LabelComponent extends Component<ILabel> {
16 static defaultProps = {
17 showLabel: true,
18 };
19
20 render() {
21 const {
22 title,
23 showLabel,
24 classes,
25 className,
26 children,
27 htmlFor,
28 isRequired,
29 } = this.props;
30
31 if (!showLabel) return children;
32
33 return (
34 <label
35 className={classnames({
36 [`${className}`]: className,
37 })}
38 htmlFor={htmlFor}
39 >
40 {showLabel && (
41 <span className={classes.label}>{title}{isRequired && ' *'}</span>
42 )}
43 <div className={classes.content}>
44 {children}
45 </div>
46 </label>
47 );
48 }
49}
50
51export 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..c64c9b285
--- /dev/null
+++ b/packages/forms/src/label/styles.ts
@@ -0,0 +1,12 @@
1import { Theme } from '../../../theme/lib';
2
3export default (theme: Theme) => ({
4 content: {},
5 label: {
6 color: theme.labelColor,
7 fontSize: theme.uiFontSize,
8 },
9 hasError: {
10 color: theme.brandDanger,
11 },
12});
diff --git a/packages/forms/src/select/index.tsx b/packages/forms/src/select/index.tsx
new file mode 100644
index 000000000..0e5ded176
--- /dev/null
+++ b/packages/forms/src/select/index.tsx
@@ -0,0 +1,445 @@
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 label: {
60 '& > div': {
61 marginTop: 5,
62 }
63 },
64 popup: {
65 opacity: 0,
66 height: 0,
67 overflowX: 'scroll',
68 border: theme.selectBorder,
69 borderTop: 0,
70 transition: 'all 0.3s',
71 },
72 open: {
73 opacity: 1,
74 height: 350,
75 background: theme.selectPopupBackground,
76 },
77 option: {
78 padding: 10,
79 borderBottom: theme.selectOptionBorder,
80 color: theme.selectOptionColor,
81
82 '&:hover': {
83 background: theme.selectOptionItemHover,
84 color: theme.selectOptionItemHoverColor,
85 },
86 '&:active': {
87 background: theme.selectOptionItemActive,
88 color: theme.selectOptionItemActiveColor,
89 },
90 },
91 selected: {
92 background: theme.selectOptionItemActive,
93 color: theme.selectOptionItemActiveColor,
94 },
95 toggle: {
96 marginLeft: 'auto',
97 fill: theme.selectToggleColor,
98 transition: 'transform 0.3s',
99 },
100 toggleOpened: {
101 transform: 'rotateZ(90deg)',
102 },
103 searchContainer: {
104 display: 'flex',
105 background: theme.selectSearchBackground,
106 alignItems: 'center',
107 paddingLeft: 10,
108 color: theme.selectColor,
109
110 '& svg': {
111 fill: theme.selectSearchColor,
112 },
113 },
114 search: {
115 border: 0,
116 width: '100%',
117 fontSize: theme.uiFontSize,
118 background: 'none',
119 marginLeft: 10,
120 padding: [10, 0],
121 color: theme.selectSearchColor,
122 },
123 clearNeedle: {
124 background: 'none',
125 border: 0,
126 },
127 focused: {
128 fontWeight: 'bold',
129 background: theme.selectOptionItemHover,
130 color: theme.selectOptionItemHoverColor,
131 },
132 hasError: {
133 borderColor: theme.brandDanger,
134 },
135 disabled: {
136 opacity: theme.selectDisabledOpacity,
137 },
138});
139
140class SelectComponent extends Component<IProps> {
141 public static defaultProps = {
142 onChange: () => {},
143 showLabel: true,
144 disabled: false,
145 error: '',
146 };
147
148 state = {
149 open: false,
150 value: '',
151 needle: '',
152 selected: 0,
153 options: null,
154 };
155
156 private componentRef = createRef<HTMLDivElement>();
157 private inputRef = createRef<HTMLInputElement>();
158 private searchInputRef = createRef<HTMLInputElement>();
159 private scrollContainerRef = createRef<HTMLDivElement>();
160 private activeOptionRef = createRef<HTMLDivElement>();
161
162 private keyListener: any;
163
164 componentWillReceiveProps(nextProps: IProps) {
165 if (nextProps.value && nextProps.value !== this.props.value) {
166 this.setState({
167 value: nextProps.value,
168 });
169 }
170 }
171
172 componentDidUpdate(prevProps: IProps, prevState: IState) {
173 const {
174 open,
175 } = this.state;
176
177 if (this.searchInputRef && this.searchInputRef.current) {
178 if (open) {
179 this.searchInputRef.current.focus();
180 }
181 }
182 }
183
184 componentDidMount() {
185 if (this.inputRef && this.inputRef.current) {
186 const {
187 data,
188 } = this.props;
189
190 if (data) {
191 Object.keys(data).map(key => this.inputRef.current!.dataset[key] = data[key]);
192 }
193 }
194
195 window.addEventListener('keydown', this.arrowKeysHandler.bind(this), false);
196 }
197
198 componentWillMount() {
199 const {
200 value,
201 } = this.props;
202
203 if (this.componentRef && this.componentRef.current) {
204 this.componentRef.current.removeEventListener('keydown', this.keyListener);
205 }
206
207 if (value) {
208 this.setState({
209 value,
210 });
211 }
212
213 this.setFilter();
214 }
215
216 componentWillUnmount() {
217 window.removeEventListener('keydown', this.arrowKeysHandler.bind(this));
218 }
219
220 setFilter(needle: string = '') {
221 const { options } = this.props;
222
223 let filteredOptions = {};
224 if (needle) {
225 Object.keys(options).map((key) => {
226 if (key.toLocaleLowerCase().startsWith(needle.toLocaleLowerCase()) || options[key].toLocaleLowerCase().startsWith(needle.toLocaleLowerCase())) {
227 Object.assign(filteredOptions, {
228 [`${key}`]: options[key],
229 });
230 }
231 });
232 } else {
233 filteredOptions = options;
234 }
235
236 this.setState({
237 needle,
238 options: filteredOptions,
239 selected: 0,
240 });
241 }
242
243 select(key: string) {
244 this.setState((state: IState) => ({
245 value: key,
246 open: false,
247 }));
248
249 this.setFilter();
250
251 if (this.props.onChange) {
252 this.props.onChange(key as any);
253 }
254 }
255
256 arrowKeysHandler(e: KeyboardEvent) {
257 const {
258 selected,
259 open,
260 options,
261 } = this.state;
262
263 if (!open) return;
264
265 if (e.keyCode === 38 || e.keyCode === 40) {
266 e.preventDefault();
267 }
268
269 if (this.componentRef && this.componentRef.current) {
270 if (e.keyCode === 38 && selected > 0) {
271 this.setState((state: IState) => ({
272 selected: state.selected - 1,
273 }));
274 } else if (e.keyCode === 40 && selected < Object.keys(options!).length - 1) {
275 this.setState((state: IState) => ({
276 selected: state.selected + 1,
277 }));
278 } else if (e.keyCode === 13) {
279 this.select(Object.keys(options!)[selected]);
280 }
281
282 if (this.activeOptionRef && this.activeOptionRef.current && this.scrollContainerRef && this.scrollContainerRef.current) {
283 const containerTopOffset = this.scrollContainerRef.current.offsetTop;
284 const optionTopOffset = this.activeOptionRef.current.offsetTop;
285
286 const topOffset = optionTopOffset - containerTopOffset;
287
288 this.scrollContainerRef.current.scrollTop = topOffset - 35;
289 }
290 }
291
292 switch (e.keyCode){
293 case 37: case 39: case 38: case 40: // Arrow keys
294 case 32: break; // Space
295 default: break; // do not block other keys
296 }
297 }
298
299 render() {
300 const {
301 actionText,
302 classes,
303 className,
304 defaultValue,
305 disabled,
306 error,
307 id,
308 inputClassName,
309 name,
310 label,
311 showLabel,
312 showSearch,
313 onChange,
314 required,
315 } = this.props;
316
317 const {
318 open,
319 needle,
320 value,
321 selected,
322 options,
323 } = this.state;
324
325 let selection = '';
326 if (!value && defaultValue && options![defaultValue]) {
327 selection = options![defaultValue];
328 } else if (value && options![value]) {
329 selection = options![value];
330 } else {
331 selection = actionText;
332 }
333
334 return (
335 <Wrapper
336 className={className}
337 identifier="franz-select"
338 >
339 <Label
340 title={label}
341 showLabel={showLabel}
342 htmlFor={id}
343 className={classes.label}
344 isRequired={required}
345 >
346 <div
347 className={classnames({
348 [`${classes.hasError}`]: error,
349 [`${classes.disabled}`]: disabled,
350 })}
351 ref={this.componentRef}
352 >
353 <button
354 type="button"
355 className={classnames({
356 [`${inputClassName}`]: inputClassName,
357 [`${classes.select}`]: true,
358 [`${classes.hasError}`]: error,
359 })}
360 onClick= {!disabled ? () => this.setState((state: IState) => ({
361 open: !state.open,
362 })) : () => {}}
363 >
364 {selection}
365 <Icon
366 path={mdiArrowRightDropCircleOutline}
367 size={0.8}
368 className={classnames({
369 [`${classes.toggle}`]: true,
370 [`${classes.toggleOpened}`]: open,
371 })}
372 />
373 </button>
374 {showSearch && open && (
375 <div className={classes.searchContainer}>
376 <Icon
377 path={mdiMagnify}
378 size={0.8}
379 />
380 <input
381 type="text"
382 value={needle}
383 onChange={e => this.setFilter(e.currentTarget.value)}
384 placeholder="Search"
385 className={classes.search}
386 ref={this.searchInputRef}
387 />
388 {needle && (
389 <button
390 type="button"
391 className={classes.clearNeedle}
392 onClick={() => this.setFilter()}
393 >
394 <Icon
395 path={mdiCloseCircle}
396 size={0.7}
397 />
398 </button>
399 )}
400 </div>
401 )}
402 <div
403 className={classnames({
404 [`${classes.popup}`]: true,
405 [`${classes.open}`]: open,
406 })}
407 ref={this.scrollContainerRef}
408 >
409 {Object.keys(options!).map(((key, i) => (
410 <div
411 key={key}
412 onClick={() => this.select(key)}
413 className={classnames({
414 [`${classes.option}`]: true,
415 [`${classes.selected}`]: options![key] === selection,
416 [`${classes.focused}`]: selected === i,
417 })}
418 onMouseOver={() => this.setState({ selected: i })}
419 ref={selected === i ? this.activeOptionRef : null}
420 >
421 {options![key]}
422 </div>
423 )))}
424 </div>
425 </div>
426 <input
427 className={classes.input}
428 id={id}
429 name={name}
430 type="hidden"
431 defaultValue={value}
432 onChange={onChange}
433 disabled={disabled}
434 ref={this.inputRef}
435 />
436 </Label>
437 {error && (
438 <Error message={error} />
439 )}
440 </Wrapper>
441 );
442 }
443}
444
445export 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..d84508a5f
--- /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 } 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 alignItems: 'center',
49
50 '& > span': {
51 order: 1,
52 marginLeft: 15,
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..9688ce2c7
--- /dev/null
+++ b/packages/forms/src/typings/generic.ts
@@ -0,0 +1,17 @@
1import { Theme } from '@meetfranz/theme/lib';
2import { Classes } from 'jss';
3
4export interface IFormField {
5 showLabel?: boolean;
6 label?: string;
7 error?: string;
8 required?: boolean;
9}
10
11export interface IWithStyle {
12 classes: Classes;
13 theme: Theme;
14}
15
16export type Merge<M, N> = Omit<M, Extract<keyof M, keyof N>> & N;
17export 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..642904089
--- /dev/null
+++ b/packages/theme/package.json
@@ -0,0 +1,29 @@
1{
2 "name": "@meetfranz/theme",
3 "version": "1.0.13",
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": "e9b9079dc921e85961954727a7b2a8eabe5b9798"
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..d48dbf916
--- /dev/null
+++ b/packages/theme/src/themes/dark/index.ts
@@ -0,0 +1,120 @@
1import color from 'color';
2import { cloneDeep, merge } from 'lodash';
3
4import * as defaultStyles from '../default';
5import * as legacyStyles from '../legacy';
6
7export const colorBackground = legacyStyles.darkThemeGrayDarkest;
8export const colorContentBackground = legacyStyles.darkThemeGrayDarkest;
9export const colorBackgroundSubscriptionContainer = legacyStyles.themeBrandInfo;
10
11export const colorHeadline = legacyStyles.darkThemeTextColor;
12export const colorText = legacyStyles.darkThemeTextColor;
13
14// Loader
15export const colorFullscreenLoaderSpinner = '#FFF';
16export const colorWebviewLoaderBackground = color(legacyStyles.darkThemeGrayDarkest).alpha(0.5).rgb().string();
17
18// Input
19export const labelColor = legacyStyles.darkThemeTextColor;
20export const inputColor = legacyStyles.darkThemeGrayLightest;
21export const inputBackground = legacyStyles.themeGrayDark;
22export const inputBorder = `1px solid ${legacyStyles.darkThemeGrayLight}`;
23export const inputPrefixColor = color(legacyStyles.darkThemeGrayLighter).lighten(0.3).hex();
24export const inputPrefixBackground = legacyStyles.darkThemeGray;
25export const inputDisabledOpacity = 0.5;
26export const inputScorePasswordBackground = legacyStyles.darkThemeGrayDark;
27export const inputModifierColor = color(legacyStyles.darkThemeGrayLighter).lighten(0.3).hex();
28export const inputPlaceholderColor = color(legacyStyles.darkThemeGrayLighter).darken(0.1).hex();
29
30// Toggle
31export const toggleBackground = legacyStyles.darkThemeGray;
32export const toggleButton = legacyStyles.darkThemeGrayLighter;
33
34// Button
35export const buttonPrimaryTextColor = legacyStyles.darkThemeTextColor;
36
37export const buttonSecondaryBackground = legacyStyles.darkThemeGrayLighter;
38export const buttonSecondaryTextColor = legacyStyles.darkThemeTextColor;
39
40export const buttonDangerTextColor = legacyStyles.darkThemeTextColor;
41
42export const buttonWarningTextColor = legacyStyles.darkThemeTextColor;
43
44export const buttonLoaderColor = {
45 primary: '#FFF',
46 secondary: buttonSecondaryTextColor,
47 success: '#FFF',
48 warning: '#FFF',
49 danger: '#FFF',
50 inverted: defaultStyles.brandPrimary,
51};
52
53// Select
54export const selectBackground = inputBackground;
55export const selectBorder = inputBorder;
56export const selectColor = inputColor;
57export const selectToggleColor = inputPrefixColor;
58export const selectPopupBackground = legacyStyles.darkThemeGrayLight;
59export const selectOptionColor = '#FFF';
60export const selectOptionBorder = `1px solid ${color(legacyStyles.darkThemeGrayLight).darken(0.2).hex()}`;
61export const selectOptionItemHover = color(legacyStyles.darkThemeGrayLight).darken(0.2).hex();
62export const selectOptionItemHoverColor = selectColor;
63export const selectSearchColor = inputBackground;
64
65// Modal
66export const colorModalOverlayBackground = color(legacyStyles.darkThemeBlack).alpha(0.8).rgb().string();
67
68// Services
69export const services = merge({}, defaultStyles.services, {
70 listItems: {
71 borderColor: legacyStyles.darkThemeGrayDarker,
72 hoverBgColor: legacyStyles.darkThemeGrayDarker,
73 disabled: {
74 color: legacyStyles.darkThemeGray,
75 },
76 },
77});
78
79// Service Icon
80export const serviceIcon = merge({}, defaultStyles.serviceIcon, {
81 isCustom: {
82 border: `1px solid ${legacyStyles.darkThemeGrayDark}`,
83 },
84});
85
86// Workspaces
87const drawerBg = color(colorBackground).lighten(0.3).hex();
88
89export const workspaces = merge({}, defaultStyles.workspaces, {
90 settings: {
91 listItems: cloneDeep(services.listItems),
92 },
93 drawer: {
94 background: drawerBg,
95 addButton: {
96 color: legacyStyles.darkThemeGrayLighter,
97 hoverColor: legacyStyles.darkThemeGraySmoke,
98 },
99 listItem: {
100 border: color(drawerBg).lighten(0.2).hex(),
101 hoverBackground: color(drawerBg).lighten(0.2).hex(),
102 activeBackground: defaultStyles.brandPrimary,
103 name: {
104 color: colorText,
105 activeColor: 'white',
106 },
107 services: {
108 color: color(colorText).darken(0.5).hex(),
109 active: color(defaultStyles.brandPrimary).lighten(0.5).hex(),
110 },
111 },
112 },
113});
114
115// Announcements
116export const announcements = merge({}, defaultStyles.workspaces, {
117 spotlight: {
118 background: legacyStyles.darkThemeGrayDark,
119 },
120});
diff --git a/packages/theme/src/themes/default/index.ts b/packages/theme/src/themes/default/index.ts
new file mode 100644
index 000000000..0f02fa3c8
--- /dev/null
+++ b/packages/theme/src/themes/default/index.ts
@@ -0,0 +1,209 @@
1import color from 'color';
2import { cloneDeep } from 'lodash';
3
4import * as legacyStyles from '../legacy';
5
6export interface IStyleTypes {
7 [index: string]: {
8 accent: string;
9 contrast: string;
10 border?: string;
11 };
12}
13
14export const brandPrimary = '#3498db';
15export const brandSuccess = '#5cb85c';
16export const brandInfo = '#5bc0de';
17export const brandWarning = '#FF9F00';
18export const brandDanger = '#d9534f';
19
20export const uiFontSize = 14;
21
22export const borderRadius = legacyStyles.themeBorderRadius;
23export const borderRadiusSmall = legacyStyles.themeBorderRadiusSmall;
24
25export const colorBackground = legacyStyles.themeGrayLighter;
26export const colorContentBackground = '#FFFFFF';
27export const colorHeadline = legacyStyles.themeGrayDark;
28
29export const colorText = legacyStyles.themeTextColor;
30
31// Subscription Container Component
32export const colorSubscriptionContainerBackground = 'none';
33export const colorSubscriptionContainerBorder = `1px solid ${brandPrimary}`;
34export const colorSubscriptionContainerTitle = brandPrimary;
35export const colorSubscriptionContainerActionButtonBackground = brandPrimary;
36export const colorSubscriptionContainerActionButtonColor = '#FFF';
37
38// Loader
39export const colorAppLoaderSpinner = '#FFF';
40export const colorFullscreenLoaderSpinner = legacyStyles.themeGrayDark;
41export const colorWebviewLoaderBackground = color(legacyStyles.themeGrayLighter).alpha(0.8).rgb().string();
42
43// Input
44export const labelColor = legacyStyles.themeGrayLight;
45export const inputColor = legacyStyles.themeGray;
46export const inputHeight = 40;
47export const inputBackground = legacyStyles.themeGrayLightest;
48export const inputBorder = `1px solid ${legacyStyles.themeGrayLighter}`;
49export const inputModifierColor = legacyStyles.themeGrayLight;
50export const inputPlaceholderColor = color(legacyStyles.themeGrayLight).lighten(0.3).hex();
51export const inputPrefixColor = legacyStyles.themeGrayLight;
52export const inputPrefixBackground = legacyStyles.themeGrayLighter;
53export const inputDisabledOpacity = 0.5;
54export const inputScorePasswordBackground = legacyStyles.themeGrayLighter;
55
56// Toggle
57export const toggleBackground = legacyStyles.themeGrayLighter;
58export const toggleButton = legacyStyles.themeGrayLight;
59export const toggleButtonActive = brandPrimary;
60export const toggleWidth = 40;
61export const toggleHeight = 14;
62
63// Style Types
64export const styleTypes: IStyleTypes = {
65 primary: {
66 accent: brandPrimary,
67 contrast: '#FFF',
68 },
69 secondary: {
70 accent: legacyStyles.themeGrayLighter,
71 contrast: legacyStyles.themeGray,
72 },
73 success: {
74 accent: brandSuccess,
75 contrast: '#FFF',
76 },
77 warning: {
78 accent: brandWarning,
79 contrast: '#FFF',
80 },
81 danger: {
82 accent: brandDanger,
83 contrast: '#FFF',
84 },
85 inverted: {
86 accent: 'none',
87 contrast: brandPrimary,
88 border: `1px solid ${brandPrimary}`,
89 },
90};
91
92// Button
93export const buttonPrimaryBackground = brandPrimary;
94export const buttonPrimaryTextColor = '#FFF';
95
96export const buttonSecondaryBackground = legacyStyles.themeGrayLighter;
97export const buttonSecondaryTextColor = legacyStyles.themeGray;
98
99export const buttonSuccessBackground = brandSuccess;
100export const buttonSuccessTextColor = '#FFF';
101
102export const buttonDangerBackground = brandDanger;
103export const buttonDangerTextColor = '#FFF';
104
105export const buttonWarningBackground = brandWarning;
106export const buttonWarningTextColor = '#FFF';
107
108export const buttonInvertedBackground = 'none';
109export const buttonInvertedTextColor = brandPrimary;
110export const buttonInvertedBorder = `1px solid ${brandPrimary}`;
111
112export const buttonHeight = inputHeight;
113
114export const buttonLoaderColor = {
115 primary: '#FFF',
116 secondary: buttonSecondaryTextColor,
117 success: '#FFF',
118 warning: '#FFF',
119 danger: '#FFF',
120 inverted: brandPrimary,
121};
122
123// Select
124export const selectBackground = inputBackground;
125export const selectBorder = inputBorder;
126export const selectHeight = inputHeight;
127export const selectColor = inputColor;
128export const selectToggleColor = inputPrefixColor;
129export const selectPopupBackground = '#FFF';
130export const selectOptionColor = inputColor;
131export const selectOptionBorder = `1px solid ${legacyStyles.themeGrayLightest}`;
132export const selectOptionItemHover = legacyStyles.themeGrayLighter;
133export const selectOptionItemHoverColor = selectColor;
134export const selectOptionItemActive = brandPrimary;
135export const selectOptionItemActiveColor = '#FFF';
136export const selectSearchBackground = legacyStyles.themeGrayLighter;
137export const selectSearchColor = inputColor;
138export const selectDisabledOpacity = inputDisabledOpacity;
139
140// Badge
141export const badgeFontSize = uiFontSize - 2;
142export const badgeBorderRadius = 50;
143
144// Modal
145export const colorModalOverlayBackground = color('#000').alpha(0.5).rgb().string();
146
147// Services
148export const services = {
149 listItems: {
150 padding: 10,
151 height: 57,
152 borderColor: legacyStyles.themeGrayLightest,
153 hoverBgColor: legacyStyles.themeGrayLightest,
154 disabled: {
155 color: legacyStyles.themeGrayLight,
156 },
157 },
158};
159
160// Service Icon
161export const serviceIcon = {
162 width: 35,
163 isCustom: {
164 border: `1px solid ${legacyStyles.themeGrayLighter}`,
165 borderRadius: legacyStyles.themeBorderRadius,
166 width: 37,
167 },
168};
169
170// Workspaces
171const drawerBg = color(colorBackground).lighten(0.1).hex();
172
173export const workspaces = {
174 settings: {
175 listItems: cloneDeep(services.listItems),
176 },
177 drawer: {
178 width: 300,
179 padding: 20,
180 background: drawerBg,
181 buttons: {
182 color: color(legacyStyles.themeGrayLight).lighten(0.1).hex(),
183 hoverColor: legacyStyles.themeGrayLight,
184 },
185 listItem: {
186 hoverBackground: color(drawerBg).darken(0.01).hex(),
187 activeBackground: legacyStyles.themeGrayLightest,
188 border: color(drawerBg).darken(0.05).hex(),
189 name: {
190 color: colorText,
191 activeColor: colorText,
192 },
193 services: {
194 color: color(colorText).lighten(1.5).hex(),
195 active: color(colorText).lighten(1.5).hex(),
196 },
197 },
198 },
199 switchingIndicator: {
200 spinnerColor: 'white',
201 },
202};
203
204// Announcements
205export const announcements = {
206 spotlight: {
207 background: legacyStyles.themeGrayLightest,
208 },
209};
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..5da8389d6
--- /dev/null
+++ b/packages/typings/package.json
@@ -0,0 +1,22 @@
1{
2 "name": "@meetfranz/typings",
3 "version": "0.0.11",
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": "e9b9079dc921e85961954727a7b2a8eabe5b9798"
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..514b2cc7c
--- /dev/null
+++ b/packages/ui/package.json
@@ -0,0 +1,38 @@
1{
2 "name": "@meetfranz/ui",
3 "version": "0.0.8",
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.13",
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": "e9b9079dc921e85961954727a7b2a8eabe5b9798"
38}
diff --git a/packages/ui/src/badge/ProBadge.tsx b/packages/ui/src/badge/ProBadge.tsx
new file mode 100644
index 000000000..612e23210
--- /dev/null
+++ b/packages/ui/src/badge/ProBadge.tsx
@@ -0,0 +1,64 @@
1import { Theme } from '@meetfranz/theme';
2import classnames from 'classnames';
3import React, { Component } from 'react';
4import injectStyle from 'react-jss';
5
6import { Icon, Badge } from '../';
7import { IWithStyle } from '../typings/generic';
8
9interface IProps extends IWithStyle {
10 badgeClasses?: string;
11 iconClasses?: string;
12 inverted?: boolean;
13}
14
15const styles = (theme: Theme) => ({
16 badge: {
17 height: 'auto',
18 padding: [4, 6, 2, 7],
19 borderRadius: theme.borderRadiusSmall,
20 },
21 invertedBadge: {
22 background: theme.styleTypes.primary.contrast,
23 color: theme.styleTypes.primary.accent,
24 },
25 icon: {
26 fill: theme.styleTypes.primary.contrast,
27 },
28 invertedIcon: {
29 fill: theme.styleTypes.primary.accent,
30 },
31});
32
33class ProBadgeComponent extends Component<IProps> {
34 render() {
35 const {
36 classes,
37 badgeClasses,
38 iconClasses,
39 inverted,
40 } = this.props;
41
42 return (
43 <Badge
44 type="primary"
45 className={classnames([
46 classes.badge,
47 inverted && classes.invertedBadge,
48 badgeClasses,
49 ])}
50 >
51 <Icon
52 icon="mdiStar"
53 className={classnames([
54 classes.icon,
55 inverted && classes.invertedIcon,
56 iconClasses,
57 ])}
58 />
59 </Badge>
60 );
61 }
62}
63
64export const ProBadge = injectStyle(styles)(ProBadgeComponent);
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..666495ce9
--- /dev/null
+++ b/packages/ui/src/index.ts
@@ -0,0 +1,6 @@
1export { Icon } from './icon';
2export { Infobox } from './infobox';
3export * from './headline';
4export { Loader } from './loader';
5export { Badge } from './badge';
6export { ProBadge } from './badge/ProBadge';
diff --git a/packages/ui/src/infobox/index.tsx b/packages/ui/src/infobox/index.tsx
new file mode 100644
index 000000000..9066a623e
--- /dev/null
+++ b/packages/ui/src/infobox/index.tsx
@@ -0,0 +1,204 @@
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 onUnmount?: () => void;
15 ctaOnClick?: () => void;
16 ctaLabel?: string;
17 ctaLoading?: boolean;
18 children: React.ReactNode;
19 className: string;
20}
21
22interface IState {
23 isDismissing: boolean;
24 dismissed: boolean;
25}
26
27const buttonStyles = (theme: Theme) => {
28 const styles = {};
29 Object.keys(theme.styleTypes).map((style) => {
30 Object.assign(styles, {
31 [style]: {
32 background: theme.styleTypes[style].accent,
33 color: theme.styleTypes[style].contrast,
34 border: theme.styleTypes[style].border,
35
36 '& svg': {
37 fill: theme.styleTypes[style].contrast,
38 },
39 },
40 });
41 });
42
43 return styles;
44};
45
46const styles = (theme: Theme) => ({
47 wrapper: {
48 position: 'relative',
49 overflow: 'hidden',
50 height: 'auto',
51 },
52 infobox: {
53 alignItems: 'center',
54 borderRadius: theme.borderRadiusSmall,
55 display: 'flex',
56 height: 'auto',
57 marginBottom: 30,
58 padding: '15px 20px',
59 top: 0,
60 transition: 'all 0.5s',
61 opacity: 1,
62 },
63 dismissing: {
64 // position: 'absolute',
65 marginTop: -100,
66 opacity: 0,
67 },
68 content: {
69 flex: 1,
70 },
71 icon: {
72 marginRight: 10,
73 },
74 close: {
75 color: (props: IProps) => theme.styleTypes[props.type ? props.type : 'primary'].contrast,
76 marginRight: -5,
77 border: 0,
78 background: 'none',
79 },
80 cta: {
81 borderColor: (props: IProps) => theme.styleTypes[props.type ? props.type : 'primary'].contrast,
82 borderRadius: theme.borderRadiusSmall,
83 borderStyle: 'solid',
84 borderWidth: 1,
85 background: 'none',
86 color: (props: IProps) => theme.styleTypes[props.type ? props.type : 'primary'].contrast,
87 marginLeft: 15,
88 padding: [4, 10],
89 fontSize: theme.uiFontSize,
90 transition: 'opacity 0.3s',
91
92 '&:hover': {
93 opacity: 0.6,
94 },
95 },
96 ...buttonStyles(theme),
97});
98
99class InfoboxComponent extends Component<IProps, IState> {
100 public static defaultProps = {
101 type: 'primary',
102 dismissable: false,
103 ctaOnClick: () => {},
104 onDismiss: () => {},
105 ctaLabel: '',
106 ctaLoading: false,
107 };
108
109 state = {
110 isDismissing: false,
111 dismissed: false,
112 };
113
114 dismiss() {
115 const {
116 onDismiss,
117 } = this.props;
118
119 this.setState({
120 isDismissing: true,
121 });
122
123 if (onDismiss) {
124 onDismiss();
125 }
126
127 setTimeout(() => {
128 this.setState({
129 dismissed: true,
130 });
131 }, 3000);
132 }
133
134 componentWillUnmount(): void {
135 const { onUnmount } = this.props;
136 if (onUnmount) onUnmount();
137 }
138
139 render() {
140 const {
141 classes,
142 children,
143 icon,
144 type,
145 ctaLabel,
146 ctaLoading,
147 ctaOnClick,
148 dismissable,
149 className,
150 } = this.props;
151
152 const {
153 isDismissing,
154 dismissed,
155 } = this.state;
156
157 if (dismissed) {
158 return null;
159 }
160
161 return (
162 <div className={classnames({
163 [classes.wrapper]: true,
164 [`${className}`]: className,
165 })}>
166 <div
167 className={classnames({
168 [classes.infobox]: true,
169 [classes[`${type}`]]: type,
170 [classes.dismissing]: isDismissing,
171 })}
172 data-type="franz-infobox"
173 >
174 {icon && (
175 <Icon icon={icon} className={classes.icon} />
176 )}
177 <div className={classes.content}>
178 {children}
179 </div>
180 {ctaLabel && (
181 <button
182 className={classes.cta}
183 onClick={ctaOnClick}
184 type="button"
185 >
186 {ctaLabel}
187 </button>
188 )}
189 {dismissable && (
190 <button
191 type="button"
192 onClick={this.dismiss.bind(this)}
193 className={classes.close}
194 >
195 <Icon icon="mdiClose" />
196 </button>
197 )}
198 </div>
199 </div>
200 );
201 }
202}
203
204export 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..4a3c8274f
--- /dev/null
+++ b/packages/ui/src/loader/index.tsx
@@ -0,0 +1,50 @@
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 color?: string;
12}
13
14const styles = (theme: Theme) => ({
15 container: {
16 position: 'relative',
17 height: 60,
18 },
19});
20
21class LoaderComponent extends Component<IProps> {
22 render() {
23 const {
24 classes,
25 className,
26 color,
27 theme,
28 } = this.props;
29
30 return (
31 <div
32 className={classnames({
33 [classes.container]: true,
34 [`${className}`]: className,
35 })}
36 data-type="franz-loader"
37 >
38 <ReactLoader
39 loaded={false}
40 width={4}
41 scale={0.75}
42 color={color || theme.colorText}
43 parentClassName={classes.loader}
44 />
45 </div>
46 );
47 }
48}
49
50export 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}