diff options
Diffstat (limited to 'packages')
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 @@ | |||
1 | node_modules/ | ||
2 | lib | ||
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 @@ | |||
1 | import * as mdiIcons from '@mdi/js'; | ||
2 | import Icon from '@mdi/react'; | ||
3 | import { Theme } from '@meetfranz/theme'; | ||
4 | import classnames from 'classnames'; | ||
5 | import CSS from 'csstype'; | ||
6 | import React, { Component } from 'react'; | ||
7 | import injectStyle, { withTheme } from 'react-jss'; | ||
8 | import Loader from 'react-loader'; | ||
9 | |||
10 | import { IFormField, IWithStyle } from '../typings/generic'; | ||
11 | |||
12 | type ButtonType = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'inverted'; | ||
13 | |||
14 | interface 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 | |||
29 | interface IState { | ||
30 | busy: boolean; | ||
31 | } | ||
32 | |||
33 | const 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 | |||
137 | class 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 | |||
275 | export 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 @@ | |||
1 | import { Classes } from 'jss'; | ||
2 | import React, { Component } from 'react'; | ||
3 | import injectSheet from 'react-jss'; | ||
4 | |||
5 | import styles from './styles'; | ||
6 | |||
7 | interface IProps { | ||
8 | classes: Classes; | ||
9 | message: string; | ||
10 | } | ||
11 | |||
12 | class 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 | |||
29 | export 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 @@ | |||
1 | import { Theme } from '../../../theme/lib'; | ||
2 | |||
3 | export 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 @@ | |||
1 | export { Input } from './input'; | ||
2 | export { Toggle } from './toggle'; | ||
3 | export { Button } from './button'; | ||
4 | export { 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 @@ | |||
1 | import { mdiEye, mdiEyeOff } from '@mdi/js'; | ||
2 | import Icon from '@mdi/react'; | ||
3 | import classnames from 'classnames'; | ||
4 | import React, { Component, createRef } from 'react'; | ||
5 | import injectSheet from 'react-jss'; | ||
6 | |||
7 | import { IFormField, IWithStyle } from '../typings/generic'; | ||
8 | |||
9 | import { Error } from '../error'; | ||
10 | import { Label } from '../label'; | ||
11 | import { Wrapper } from '../wrapper'; | ||
12 | import { scorePasswordFunc } from './scorePassword'; | ||
13 | |||
14 | import styles from './styles'; | ||
15 | |||
16 | interface IData { | ||
17 | [index: string]: string; | ||
18 | } | ||
19 | |||
20 | interface 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 | |||
31 | interface IState { | ||
32 | showPassword: boolean; | ||
33 | passwordScore: number; | ||
34 | } | ||
35 | |||
36 | class 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 | |||
211 | export 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 @@ | |||
1 | interface ILetters { | ||
2 | [key: string]: number; | ||
3 | } | ||
4 | |||
5 | interface IVariations { | ||
6 | [index: string]: boolean; | ||
7 | digits: boolean; | ||
8 | lower: boolean; | ||
9 | nonWords: boolean; | ||
10 | upper: boolean; | ||
11 | } | ||
12 | |||
13 | export 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 @@ | |||
1 | import { Theme } from '@meetfranz/theme'; | ||
2 | import CSS from 'csstype'; | ||
3 | |||
4 | const 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 | |||
12 | export 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 @@ | |||
1 | import classnames from 'classnames'; | ||
2 | import { Classes } from 'jss'; | ||
3 | import React, { Component } from 'react'; | ||
4 | import injectSheet from 'react-jss'; | ||
5 | |||
6 | import { IFormField } from '../typings/generic'; | ||
7 | |||
8 | import styles from './styles'; | ||
9 | |||
10 | interface ILabel extends IFormField, React.LabelHTMLAttributes<HTMLLabelElement> { | ||
11 | classes: Classes; | ||
12 | isRequired: boolean; | ||
13 | } | ||
14 | |||
15 | class 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 | |||
51 | export 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 @@ | |||
1 | import { Theme } from '../../../theme/lib'; | ||
2 | |||
3 | export 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 @@ | |||
1 | import { mdiArrowRightDropCircleOutline, mdiCloseCircle, mdiMagnify } from '@mdi/js'; | ||
2 | import Icon from '@mdi/react'; | ||
3 | import { Theme } from '@meetfranz/theme'; | ||
4 | import classnames from 'classnames'; | ||
5 | import React, { Component, createRef } from 'react'; | ||
6 | import injectStyle from 'react-jss'; | ||
7 | |||
8 | import { IFormField, IWithStyle } from '../typings/generic'; | ||
9 | |||
10 | import { NONAME } from 'dns'; | ||
11 | import { Error } from '../error'; | ||
12 | import { Label } from '../label'; | ||
13 | import { Wrapper } from '../wrapper'; | ||
14 | |||
15 | interface IOptions { | ||
16 | [index: string]: string; | ||
17 | } | ||
18 | |||
19 | interface IData { | ||
20 | [index: string]: string; | ||
21 | } | ||
22 | |||
23 | interface 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 | |||
38 | interface IState { | ||
39 | open: boolean; | ||
40 | value: string; | ||
41 | needle: string; | ||
42 | selected: number; | ||
43 | options: IOptions; | ||
44 | } | ||
45 | |||
46 | const 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 | |||
140 | class 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 | |||
445 | export 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 @@ | |||
1 | import { Theme } from '@meetfranz/theme'; | ||
2 | import classnames from 'classnames'; | ||
3 | import CSS from 'csstype'; | ||
4 | import React, { Component } from 'react'; | ||
5 | import injectStyle from 'react-jss'; | ||
6 | |||
7 | import { IFormField, IWithStyle, Omit } from '../typings/generic'; | ||
8 | |||
9 | import { Error } from '../error'; | ||
10 | import { Label } from '../label'; | ||
11 | import { Wrapper } from '../wrapper'; | ||
12 | |||
13 | interface IProps extends React.InputHTMLAttributes<HTMLInputElement>, IFormField, IWithStyle { | ||
14 | className?: string; | ||
15 | } | ||
16 | |||
17 | const 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 | |||
57 | class 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 | |||
117 | export 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 @@ | |||
1 | import { Theme } from '@meetfranz/theme/lib'; | ||
2 | import { Classes } from 'jss'; | ||
3 | |||
4 | export interface IFormField { | ||
5 | showLabel?: boolean; | ||
6 | label?: string; | ||
7 | error?: string; | ||
8 | required?: boolean; | ||
9 | } | ||
10 | |||
11 | export interface IWithStyle { | ||
12 | classes: Classes; | ||
13 | theme: Theme; | ||
14 | } | ||
15 | |||
16 | export type Merge<M, N> = Omit<M, Extract<keyof M, keyof N>> & N; | ||
17 | export 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 @@ | |||
1 | import classnames from 'classnames'; | ||
2 | import React, { Component } from 'react'; | ||
3 | import injectStyle from 'react-jss'; | ||
4 | import { IWithStyle } from '../typings/generic'; | ||
5 | |||
6 | import styles from './styles'; | ||
7 | |||
8 | interface IProps extends IWithStyle { | ||
9 | children: React.ReactNode; | ||
10 | className?: string; | ||
11 | identifier: string; | ||
12 | } | ||
13 | |||
14 | class 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 | |||
37 | export 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 @@ | |||
1 | export 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 @@ | |||
1 | theme: | ||
2 | cwd: ./theme | ||
3 | cmd: npm run dev | ||
4 | |||
5 | forms: | ||
6 | cwd: ./forms | ||
7 | cmd: npm run dev | ||
8 | |||
9 | ui: | ||
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 @@ | |||
1 | node_modules/ | ||
2 | lib | ||
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 | ``` | ||
8 | const 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 @@ | |||
1 | import * as darkThemeConfig from './themes/dark'; | ||
2 | import * as defaultThemeConfig from './themes/default'; | ||
3 | import * as legacyStyles from './themes/legacy'; | ||
4 | |||
5 | export enum ThemeType { | ||
6 | default = 'default', | ||
7 | dark = 'dark', | ||
8 | } | ||
9 | |||
10 | export 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 | |||
18 | export 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 @@ | |||
1 | import color from 'color'; | ||
2 | import { cloneDeep, merge } from 'lodash'; | ||
3 | |||
4 | import * as defaultStyles from '../default'; | ||
5 | import * as legacyStyles from '../legacy'; | ||
6 | |||
7 | export const colorBackground = legacyStyles.darkThemeGrayDarkest; | ||
8 | export const colorContentBackground = legacyStyles.darkThemeGrayDarkest; | ||
9 | export const colorBackgroundSubscriptionContainer = legacyStyles.themeBrandInfo; | ||
10 | |||
11 | export const colorHeadline = legacyStyles.darkThemeTextColor; | ||
12 | export const colorText = legacyStyles.darkThemeTextColor; | ||
13 | |||
14 | // Loader | ||
15 | export const colorFullscreenLoaderSpinner = '#FFF'; | ||
16 | export const colorWebviewLoaderBackground = color(legacyStyles.darkThemeGrayDarkest).alpha(0.5).rgb().string(); | ||
17 | |||
18 | // Input | ||
19 | export const labelColor = legacyStyles.darkThemeTextColor; | ||
20 | export const inputColor = legacyStyles.darkThemeGrayLightest; | ||
21 | export const inputBackground = legacyStyles.themeGrayDark; | ||
22 | export const inputBorder = `1px solid ${legacyStyles.darkThemeGrayLight}`; | ||
23 | export const inputPrefixColor = color(legacyStyles.darkThemeGrayLighter).lighten(0.3).hex(); | ||
24 | export const inputPrefixBackground = legacyStyles.darkThemeGray; | ||
25 | export const inputDisabledOpacity = 0.5; | ||
26 | export const inputScorePasswordBackground = legacyStyles.darkThemeGrayDark; | ||
27 | export const inputModifierColor = color(legacyStyles.darkThemeGrayLighter).lighten(0.3).hex(); | ||
28 | export const inputPlaceholderColor = color(legacyStyles.darkThemeGrayLighter).darken(0.1).hex(); | ||
29 | |||
30 | // Toggle | ||
31 | export const toggleBackground = legacyStyles.darkThemeGray; | ||
32 | export const toggleButton = legacyStyles.darkThemeGrayLighter; | ||
33 | |||
34 | // Button | ||
35 | export const buttonPrimaryTextColor = legacyStyles.darkThemeTextColor; | ||
36 | |||
37 | export const buttonSecondaryBackground = legacyStyles.darkThemeGrayLighter; | ||
38 | export const buttonSecondaryTextColor = legacyStyles.darkThemeTextColor; | ||
39 | |||
40 | export const buttonDangerTextColor = legacyStyles.darkThemeTextColor; | ||
41 | |||
42 | export const buttonWarningTextColor = legacyStyles.darkThemeTextColor; | ||
43 | |||
44 | export 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 | ||
54 | export const selectBackground = inputBackground; | ||
55 | export const selectBorder = inputBorder; | ||
56 | export const selectColor = inputColor; | ||
57 | export const selectToggleColor = inputPrefixColor; | ||
58 | export const selectPopupBackground = legacyStyles.darkThemeGrayLight; | ||
59 | export const selectOptionColor = '#FFF'; | ||
60 | export const selectOptionBorder = `1px solid ${color(legacyStyles.darkThemeGrayLight).darken(0.2).hex()}`; | ||
61 | export const selectOptionItemHover = color(legacyStyles.darkThemeGrayLight).darken(0.2).hex(); | ||
62 | export const selectOptionItemHoverColor = selectColor; | ||
63 | export const selectSearchColor = inputBackground; | ||
64 | |||
65 | // Modal | ||
66 | export const colorModalOverlayBackground = color(legacyStyles.darkThemeBlack).alpha(0.8).rgb().string(); | ||
67 | |||
68 | // Services | ||
69 | export 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 | ||
80 | export const serviceIcon = merge({}, defaultStyles.serviceIcon, { | ||
81 | isCustom: { | ||
82 | border: `1px solid ${legacyStyles.darkThemeGrayDark}`, | ||
83 | }, | ||
84 | }); | ||
85 | |||
86 | // Workspaces | ||
87 | const drawerBg = color(colorBackground).lighten(0.3).hex(); | ||
88 | |||
89 | export 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 | ||
116 | export 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 @@ | |||
1 | import color from 'color'; | ||
2 | import { cloneDeep } from 'lodash'; | ||
3 | |||
4 | import * as legacyStyles from '../legacy'; | ||
5 | |||
6 | export interface IStyleTypes { | ||
7 | [index: string]: { | ||
8 | accent: string; | ||
9 | contrast: string; | ||
10 | border?: string; | ||
11 | }; | ||
12 | } | ||
13 | |||
14 | export const brandPrimary = '#3498db'; | ||
15 | export const brandSuccess = '#5cb85c'; | ||
16 | export const brandInfo = '#5bc0de'; | ||
17 | export const brandWarning = '#FF9F00'; | ||
18 | export const brandDanger = '#d9534f'; | ||
19 | |||
20 | export const uiFontSize = 14; | ||
21 | |||
22 | export const borderRadius = legacyStyles.themeBorderRadius; | ||
23 | export const borderRadiusSmall = legacyStyles.themeBorderRadiusSmall; | ||
24 | |||
25 | export const colorBackground = legacyStyles.themeGrayLighter; | ||
26 | export const colorContentBackground = '#FFFFFF'; | ||
27 | export const colorHeadline = legacyStyles.themeGrayDark; | ||
28 | |||
29 | export const colorText = legacyStyles.themeTextColor; | ||
30 | |||
31 | // Subscription Container Component | ||
32 | export const colorSubscriptionContainerBackground = 'none'; | ||
33 | export const colorSubscriptionContainerBorder = `1px solid ${brandPrimary}`; | ||
34 | export const colorSubscriptionContainerTitle = brandPrimary; | ||
35 | export const colorSubscriptionContainerActionButtonBackground = brandPrimary; | ||
36 | export const colorSubscriptionContainerActionButtonColor = '#FFF'; | ||
37 | |||
38 | // Loader | ||
39 | export const colorAppLoaderSpinner = '#FFF'; | ||
40 | export const colorFullscreenLoaderSpinner = legacyStyles.themeGrayDark; | ||
41 | export const colorWebviewLoaderBackground = color(legacyStyles.themeGrayLighter).alpha(0.8).rgb().string(); | ||
42 | |||
43 | // Input | ||
44 | export const labelColor = legacyStyles.themeGrayLight; | ||
45 | export const inputColor = legacyStyles.themeGray; | ||
46 | export const inputHeight = 40; | ||
47 | export const inputBackground = legacyStyles.themeGrayLightest; | ||
48 | export const inputBorder = `1px solid ${legacyStyles.themeGrayLighter}`; | ||
49 | export const inputModifierColor = legacyStyles.themeGrayLight; | ||
50 | export const inputPlaceholderColor = color(legacyStyles.themeGrayLight).lighten(0.3).hex(); | ||
51 | export const inputPrefixColor = legacyStyles.themeGrayLight; | ||
52 | export const inputPrefixBackground = legacyStyles.themeGrayLighter; | ||
53 | export const inputDisabledOpacity = 0.5; | ||
54 | export const inputScorePasswordBackground = legacyStyles.themeGrayLighter; | ||
55 | |||
56 | // Toggle | ||
57 | export const toggleBackground = legacyStyles.themeGrayLighter; | ||
58 | export const toggleButton = legacyStyles.themeGrayLight; | ||
59 | export const toggleButtonActive = brandPrimary; | ||
60 | export const toggleWidth = 40; | ||
61 | export const toggleHeight = 14; | ||
62 | |||
63 | // Style Types | ||
64 | export 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 | ||
93 | export const buttonPrimaryBackground = brandPrimary; | ||
94 | export const buttonPrimaryTextColor = '#FFF'; | ||
95 | |||
96 | export const buttonSecondaryBackground = legacyStyles.themeGrayLighter; | ||
97 | export const buttonSecondaryTextColor = legacyStyles.themeGray; | ||
98 | |||
99 | export const buttonSuccessBackground = brandSuccess; | ||
100 | export const buttonSuccessTextColor = '#FFF'; | ||
101 | |||
102 | export const buttonDangerBackground = brandDanger; | ||
103 | export const buttonDangerTextColor = '#FFF'; | ||
104 | |||
105 | export const buttonWarningBackground = brandWarning; | ||
106 | export const buttonWarningTextColor = '#FFF'; | ||
107 | |||
108 | export const buttonInvertedBackground = 'none'; | ||
109 | export const buttonInvertedTextColor = brandPrimary; | ||
110 | export const buttonInvertedBorder = `1px solid ${brandPrimary}`; | ||
111 | |||
112 | export const buttonHeight = inputHeight; | ||
113 | |||
114 | export const buttonLoaderColor = { | ||
115 | primary: '#FFF', | ||
116 | secondary: buttonSecondaryTextColor, | ||
117 | success: '#FFF', | ||
118 | warning: '#FFF', | ||
119 | danger: '#FFF', | ||
120 | inverted: brandPrimary, | ||
121 | }; | ||
122 | |||
123 | // Select | ||
124 | export const selectBackground = inputBackground; | ||
125 | export const selectBorder = inputBorder; | ||
126 | export const selectHeight = inputHeight; | ||
127 | export const selectColor = inputColor; | ||
128 | export const selectToggleColor = inputPrefixColor; | ||
129 | export const selectPopupBackground = '#FFF'; | ||
130 | export const selectOptionColor = inputColor; | ||
131 | export const selectOptionBorder = `1px solid ${legacyStyles.themeGrayLightest}`; | ||
132 | export const selectOptionItemHover = legacyStyles.themeGrayLighter; | ||
133 | export const selectOptionItemHoverColor = selectColor; | ||
134 | export const selectOptionItemActive = brandPrimary; | ||
135 | export const selectOptionItemActiveColor = '#FFF'; | ||
136 | export const selectSearchBackground = legacyStyles.themeGrayLighter; | ||
137 | export const selectSearchColor = inputColor; | ||
138 | export const selectDisabledOpacity = inputDisabledOpacity; | ||
139 | |||
140 | // Badge | ||
141 | export const badgeFontSize = uiFontSize - 2; | ||
142 | export const badgeBorderRadius = 50; | ||
143 | |||
144 | // Modal | ||
145 | export const colorModalOverlayBackground = color('#000').alpha(0.5).rgb().string(); | ||
146 | |||
147 | // Services | ||
148 | export 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 | ||
161 | export 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 | ||
171 | const drawerBg = color(colorBackground).lighten(0.1).hex(); | ||
172 | |||
173 | export 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 | ||
205 | export 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 */ | ||
2 | export const themeBrandPrimary = '#3498db'; | ||
3 | export const themeBrandSuccess = '#5cb85c'; | ||
4 | export const themeBrandInfo = '#5bc0de'; | ||
5 | export const themeBrandWarning = '#FF9F00'; | ||
6 | export const themeBrandDanger = '#d9534f'; | ||
7 | |||
8 | export const themeGrayDark = '#373a3c'; | ||
9 | export const themeGray = '#55595c'; | ||
10 | export const themeGrayLight = '#818a91'; | ||
11 | export const themeGrayLighter = '#eceeef'; | ||
12 | export const themeGrayLightest = '#f7f7f9'; | ||
13 | |||
14 | export const themeBorderRadius = '6px'; | ||
15 | export const themeBorderRadiusSmall = '3px'; | ||
16 | |||
17 | export const themeSidebarWidth = '68px'; | ||
18 | |||
19 | export const themeTextColor = themeGrayDark; | ||
20 | |||
21 | export const themeTransitionTime = '.5s'; | ||
22 | |||
23 | export const themeInsetShadow = 'inset 0 2px 5px rgba(0, 0, 0, .03)'; | ||
24 | |||
25 | export const darkThemeBlack = '#1A1A1A'; | ||
26 | |||
27 | export const darkThemeGrayDarkest = '#1E1E1E'; | ||
28 | export const darkThemeGrayDarker = '#2D2F31'; | ||
29 | export const darkThemeGrayDark = '#383A3B'; | ||
30 | |||
31 | export const darkThemeGray = '#47494B'; | ||
32 | |||
33 | export const darkThemeGrayLight = '#515355'; | ||
34 | export const darkThemeGrayLighter = '#8a8b8b'; | ||
35 | export const darkThemeGrayLightest = '#FFFFFF'; | ||
36 | |||
37 | export const darkThemeGraySmoke = '#CED0D1'; | ||
38 | export 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 @@ | |||
1 | const expect = require('expect.js'); | ||
2 | |||
3 | const { colorBackground: colorBackgroundDefault } = require('../lib/themes/default'); | ||
4 | const { colorBackground: colorBackgroundDark } = require('../lib/themes/dark'); | ||
5 | const { default: theme } = require('../lib'); | ||
6 | |||
7 | describe('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 | |||
7 | import { Component } from 'react'; | ||
8 | |||
9 | interface 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 | |||
33 | interface LoaderProps extends LoaderOptions { | ||
34 | loaded: boolean; | ||
35 | options?: LoaderOptions; | ||
36 | className?: string; | ||
37 | } | ||
38 | |||
39 | declare class ReactLoader extends Component<LoaderProps> { | ||
40 | } | ||
41 | |||
42 | declare namespace ReactLoader { | ||
43 | } | ||
44 | |||
45 | export = 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 @@ | |||
1 | node_modules/ | ||
2 | lib | ||
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 @@ | |||
1 | import { Theme } from '@meetfranz/theme'; | ||
2 | import classnames from 'classnames'; | ||
3 | import React, { Component } from 'react'; | ||
4 | import injectStyle from 'react-jss'; | ||
5 | |||
6 | import { Icon, Badge } from '../'; | ||
7 | import { IWithStyle } from '../typings/generic'; | ||
8 | |||
9 | interface IProps extends IWithStyle { | ||
10 | badgeClasses?: string; | ||
11 | iconClasses?: string; | ||
12 | inverted?: boolean; | ||
13 | } | ||
14 | |||
15 | const 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 | |||
33 | class 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 | |||
64 | export 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 @@ | |||
1 | import { Theme } from '@meetfranz/theme'; | ||
2 | import classnames from 'classnames'; | ||
3 | import React, { Component } from 'react'; | ||
4 | import injectStyle from 'react-jss'; | ||
5 | |||
6 | import { IWithStyle } from '../typings/generic'; | ||
7 | |||
8 | interface IProps extends IWithStyle { | ||
9 | type: string; | ||
10 | className?: string; | ||
11 | children: React.ReactNode; | ||
12 | } | ||
13 | |||
14 | const 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 | |||
29 | const 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 | |||
48 | class 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 | |||
76 | export 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 @@ | |||
1 | import { Theme } from '@meetfranz/theme'; | ||
2 | import classnames from 'classnames'; | ||
3 | import React, { Component } from 'react'; | ||
4 | import injectStyle from 'react-jss'; | ||
5 | |||
6 | import { IWithStyle, Omit } from '../typings/generic'; | ||
7 | |||
8 | interface IProps extends IWithStyle { | ||
9 | level?: number; | ||
10 | className?: string; | ||
11 | children: string | React.ReactNode; | ||
12 | id?: string; | ||
13 | } | ||
14 | |||
15 | const 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 | |||
38 | class 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 | |||
64 | const Headline = injectStyle(styles)(HeadlineComponent); | ||
65 | |||
66 | const createH = (level: number) => (props: Omit<IProps, 'classes' | 'theme'>) => <Headline level={level} {...props}>{props.children}</Headline>; | ||
67 | |||
68 | export const H1 = createH(1); | ||
69 | export const H2 = createH(2); | ||
70 | export const H3 = createH(3); | ||
71 | export 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 @@ | |||
1 | import * as mdiIcons from '@mdi/js'; | ||
2 | import MdiIcon from '@mdi/react'; | ||
3 | import { Theme } from '@meetfranz/theme'; | ||
4 | import classnames from 'classnames'; | ||
5 | import React, { Component } from 'react'; | ||
6 | import injectStyle from 'react-jss'; | ||
7 | |||
8 | import { IWithStyle } from '../typings/generic'; | ||
9 | |||
10 | interface IProps extends IWithStyle { | ||
11 | icon: keyof typeof mdiIcons; | ||
12 | size?: number; | ||
13 | className?: string; | ||
14 | } | ||
15 | |||
16 | const styles = (theme: Theme) => ({ | ||
17 | icon: { | ||
18 | fill: theme.colorText, | ||
19 | }, | ||
20 | }); | ||
21 | |||
22 | class 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 | |||
55 | export 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 @@ | |||
1 | export { Icon } from './icon'; | ||
2 | export { Infobox } from './infobox'; | ||
3 | export * from './headline'; | ||
4 | export { Loader } from './loader'; | ||
5 | export { Badge } from './badge'; | ||
6 | export { 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 @@ | |||
1 | import { Theme } from '@meetfranz/theme'; | ||
2 | import classnames from 'classnames'; | ||
3 | import React, { Component } from 'react'; | ||
4 | import injectStyle from 'react-jss'; | ||
5 | |||
6 | import { Icon } from '../'; | ||
7 | import { IWithStyle } from '../typings/generic'; | ||
8 | |||
9 | interface 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 | |||
22 | interface IState { | ||
23 | isDismissing: boolean; | ||
24 | dismissed: boolean; | ||
25 | } | ||
26 | |||
27 | const 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 | |||
46 | const 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 | |||
99 | class 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 | |||
204 | export 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 @@ | |||
1 | import { Theme } from '@meetfranz/theme'; | ||
2 | import classnames from 'classnames'; | ||
3 | import React, { Component } from 'react'; | ||
4 | import injectStyle, { withTheme } from 'react-jss'; | ||
5 | import ReactLoader from 'react-loader'; | ||
6 | |||
7 | import { IWithStyle } from '../typings/generic'; | ||
8 | |||
9 | interface IProps extends IWithStyle { | ||
10 | className?: string; | ||
11 | color?: string; | ||
12 | } | ||
13 | |||
14 | const styles = (theme: Theme) => ({ | ||
15 | container: { | ||
16 | position: 'relative', | ||
17 | height: 60, | ||
18 | }, | ||
19 | }); | ||
20 | |||
21 | class 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 | |||
50 | export 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 @@ | |||
1 | import { Theme } from '@meetfranz/theme/lib'; | ||
2 | import { Classes } from 'jss'; | ||
3 | |||
4 | export interface IWithStyle { | ||
5 | classes: Classes; | ||
6 | theme: Theme; | ||
7 | } | ||
8 | |||
9 | export type Merge<M, N> = Omit<M, Extract<keyof M, keyof N>> & N; | ||
10 | export 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 | } | ||