@ebay/muse-lib-react 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +19 -0
  2. package/build/dev/asset-manifest.json +11 -0
  3. package/build/dev/favicon.png +0 -0
  4. package/build/dev/lib-manifest.json +3374 -0
  5. package/build/dev/main.js +75839 -0
  6. package/build/dev/main.js.map +1 -0
  7. package/build/dev/muse.d.ts +170 -0
  8. package/build/dev/static/media/logo.b23b880b0dac2229042c.png +0 -0
  9. package/build/dev/static/media/subAppLoading2.bf08007b83457287ade1cedb71bc70f7.svg +7 -0
  10. package/build/dist/asset-manifest.json +11 -0
  11. package/build/dist/favicon.png +0 -0
  12. package/build/dist/lib-manifest.json +3603 -0
  13. package/build/dist/main.js +3 -0
  14. package/build/dist/main.js.LICENSE.txt +134 -0
  15. package/build/dist/main.js.map +1 -0
  16. package/build/dist/muse.d.ts +170 -0
  17. package/build/dist/static/media/logo.b23b880b0dac2229042c.png +0 -0
  18. package/build/dist/static/media/subAppLoading2.bf08007b83457287ade1cedb71bc70f7.svg +7 -0
  19. package/build/test/asset-manifest.json +11 -0
  20. package/build/test/favicon.png +0 -0
  21. package/build/test/lib-manifest.json +3587 -0
  22. package/build/test/main.js +3 -0
  23. package/build/test/main.js.LICENSE.txt +134 -0
  24. package/build/test/main.js.map +1 -0
  25. package/build/test/muse.d.ts +170 -0
  26. package/build/test/static/media/logo.b23b880b0dac2229042c.png +0 -0
  27. package/build/test/static/media/subAppLoading2.bf08007b83457287ade1cedb71bc70f7.svg +7 -0
  28. package/package.json +88 -0
  29. package/src/Root.js +195 -0
  30. package/src/common/configStore.js +40 -0
  31. package/src/common/history.js +25 -0
  32. package/src/common/rootReducer.js +40 -0
  33. package/src/common/routeConfig.js +135 -0
  34. package/src/common/store.js +21 -0
  35. package/src/features/common/ErrorBoundary.js +24 -0
  36. package/src/features/common/ErrorBoundary.less +6 -0
  37. package/src/features/common/Nodes.js +20 -0
  38. package/src/features/common/PageNotFound.js +3 -0
  39. package/src/features/common/PageNotFound.less +6 -0
  40. package/src/features/common/index.js +4 -0
  41. package/src/features/common/redux/actions.js +0 -0
  42. package/src/features/common/redux/constants.js +0 -0
  43. package/src/features/common/redux/initialState.js +12 -0
  44. package/src/features/common/redux/reducer.js +24 -0
  45. package/src/features/common/route.js +9 -0
  46. package/src/features/common/style.less +3 -0
  47. package/src/features/common/useExtPoint.js +28 -0
  48. package/src/features/common/utils.js +20 -0
  49. package/src/features/home/App.js +33 -0
  50. package/src/features/home/App.less +11 -0
  51. package/src/features/home/Homepage.js +46 -0
  52. package/src/features/home/Homepage.less +63 -0
  53. package/src/features/home/index.js +2 -0
  54. package/src/features/home/redux/actions.js +0 -0
  55. package/src/features/home/redux/constants.js +0 -0
  56. package/src/features/home/redux/initialState.js +4 -0
  57. package/src/features/home/redux/reducer.js +16 -0
  58. package/src/features/home/route.js +6 -0
  59. package/src/features/home/style.less +3 -0
  60. package/src/features/sub-app/C2SProxyFailed.js +10 -0
  61. package/src/features/sub-app/C2SProxyFailed.less +19 -0
  62. package/src/features/sub-app/FixedSubAppContainer.js +142 -0
  63. package/src/features/sub-app/FixedSubAppContainer.less +12 -0
  64. package/src/features/sub-app/LoadingSkeleton.js +21 -0
  65. package/src/features/sub-app/LoadingSkeleton.less +20 -0
  66. package/src/features/sub-app/SubAppContainer.js +203 -0
  67. package/src/features/sub-app/SubAppContainer.less +29 -0
  68. package/src/features/sub-app/SubAppContext.js +4 -0
  69. package/src/features/sub-app/hooks/useParentRouteChange.js +23 -0
  70. package/src/features/sub-app/index.js +5 -0
  71. package/src/features/sub-app/redux/actions.js +1 -0
  72. package/src/features/sub-app/redux/constants.js +2 -0
  73. package/src/features/sub-app/redux/hooks.js +1 -0
  74. package/src/features/sub-app/redux/initialState.js +12 -0
  75. package/src/features/sub-app/redux/reducer.js +25 -0
  76. package/src/features/sub-app/redux/setSubAppState.js +43 -0
  77. package/src/features/sub-app/route.js +43 -0
  78. package/src/features/sub-app/style.less +5 -0
  79. package/src/features/sub-app/subAppLoading.svg +7 -0
  80. package/src/features/sub-app/subAppLoading2.html +83 -0
  81. package/src/features/sub-app/subAppLoading2.svg +7 -0
  82. package/src/features/sub-app/subAppLoading3.svg +7 -0
  83. package/src/features/sub-app/subAppLoading4.svg +7 -0
  84. package/src/features/sub-app/urlUtils.js +101 -0
  85. package/src/images/logo.png +0 -0
  86. package/src/index.css +3 -0
  87. package/src/index.js +41 -0
  88. package/src/muse-ext.d.ts +1 -0
  89. package/src/muse.d.ts +163 -0
  90. package/src/react-app-env.d.ts +1 -0
  91. package/src/styles/global.less +9 -0
  92. package/src/styles/index.less +5 -0
  93. package/src/styles/mixins.less +0 -0
  94. package/src/utils.js +26 -0
@@ -0,0 +1,135 @@
1
+ import React from 'react';
2
+ import plugin from 'js-plugin';
3
+ import { App, Homepage } from '../features/home';
4
+ import { PageNotFound } from '../features/common';
5
+ import homeRoute from '../features/home/route';
6
+ import commonRoute from '../features/common/route';
7
+ import _ from 'lodash';
8
+ import subAppRoute from '../features/sub-app/route';
9
+
10
+ // NOTE: DO NOT CHANGE the 'childRoutes' name and the declaration pattern.
11
+ // This is used for Rekit cmds to register routes config for new features, and remove config when remove features, etc.
12
+ const childRoutes = [homeRoute, commonRoute, subAppRoute];
13
+
14
+ // Handle isIndex property of route config:
15
+ // Dupicate it and put it as the first route rule.
16
+ function handleIndexRoute(route) {
17
+ if (!route.childRoutes || !route.childRoutes.length) {
18
+ return;
19
+ }
20
+
21
+ const indexRoute = _.find(route.childRoutes, child => child.isIndex);
22
+ if (indexRoute) {
23
+ const first = { ...indexRoute };
24
+ first.path = '';
25
+ first.exact = true;
26
+ first.autoIndexRoute = true; // mark it so that the simple nav won't show it.
27
+ route.childRoutes.unshift(first);
28
+ }
29
+ route.childRoutes.forEach(handleIndexRoute);
30
+ }
31
+
32
+ const normalizeRoutes = routes => {
33
+ const byId = {};
34
+ const arr = [...routes];
35
+ const hasParent = [];
36
+
37
+ // traverse routes recursively
38
+ while (arr.length > 0) {
39
+ const r = arr.shift();
40
+ // allow a route point to a parent
41
+ if (r?.id) {
42
+ byId[r.id] = r;
43
+ }
44
+ if (r.childRoutes) {
45
+ _.forEachRight([...r.childRoutes], (cr, i) => {
46
+ // Support path as an array by expanding it to multiple rules
47
+ if (_.isArray(cr?.path)) {
48
+ r.childRoutes.splice(
49
+ i,
50
+ 1,
51
+ ...cr.path.map(p => {
52
+ return { ...r, path: p };
53
+ }),
54
+ );
55
+ }
56
+ });
57
+ arr.push(...r.childRoutes);
58
+ [...r.childRoutes].forEach(cr => {
59
+ // if a route has parent, move it to the correct parent
60
+ if (cr?.path?.startsWith('/')) {
61
+ // If it's an absolute path, move it to the top level
62
+ _.pull(r.childRoutes, cr);
63
+ routes.unshift(cr);
64
+ } else if (cr.parent) {
65
+ hasParent.push(cr);
66
+ _.pull(r.childRoutes, cr);
67
+ }
68
+ });
69
+ }
70
+ }
71
+
72
+ // for all routes which have parents, put them in the correct childRoutes
73
+ hasParent.forEach(r => {
74
+ const parentId = r.parent;
75
+ if (byId[parentId]) {
76
+ if (!byId[parentId].childRoutes) byId[parentId].childRoutes = [];
77
+ byId[parentId].childRoutes.unshift(r);
78
+ } else {
79
+ console.warn(`Warning: no parent route found with id ${parentId}.`, r);
80
+ }
81
+ });
82
+ };
83
+
84
+ const routeConfig = () => {
85
+ const newChildRoutes = [...childRoutes];
86
+ // Get routes from plugins
87
+ plugin.invoke('!route').forEach(route => {
88
+ newChildRoutes.push(..._.castArray(route));
89
+ });
90
+
91
+ // Generate the root route '/'
92
+ const museGlobal = window.MUSE_GLOBAL || {};
93
+
94
+ // Find the homepage
95
+ let homepage = Homepage;
96
+ const homepagePlugins = plugin.getPlugins('home.homepage');
97
+ if (homepagePlugins.length === 1) {
98
+ homepage = homepagePlugins[0].home.homepage;
99
+ } else if (homepagePlugins.length > 1) {
100
+ const definedHomepagePlugin = _.find(homepagePlugins, { name: museGlobal.homepage });
101
+ if (definedHomepagePlugin) homepage = definedHomepagePlugin.home.homepage;
102
+ else {
103
+ homepage = () => (
104
+ <div style={{ color: 'red', padding: '20px' }}>
105
+ Failed to show homepage: multiple homepages found from:{' '}
106
+ {homepagePlugins.map(p => p.name).join(', ')}. You should load only one plugin which
107
+ defines homepage.
108
+ </div>
109
+ );
110
+ }
111
+ }
112
+
113
+ newChildRoutes.unshift({
114
+ path: '',
115
+ component: homepage,
116
+ });
117
+
118
+ const routes = [
119
+ {
120
+ path: '/',
121
+ component: App,
122
+ childRoutes: [
123
+ ..._.cloneDeep(newChildRoutes),
124
+ { path: '*', name: 'Page not found', component: PageNotFound },
125
+ ].filter(r => r.component || r.render || (r.childRoutes && r.childRoutes.length > 0)),
126
+ },
127
+ ];
128
+ routes.forEach(handleIndexRoute);
129
+
130
+ // Handle parent routes
131
+ normalizeRoutes(routes[0].childRoutes);
132
+
133
+ return routes;
134
+ };
135
+ export default routeConfig;
@@ -0,0 +1,21 @@
1
+ import configStore from './configStore';
2
+
3
+ export default {
4
+ store: null,
5
+ getStore() {
6
+ if (!this.store) this.store = configStore();
7
+ return this.store;
8
+ },
9
+ getState() {
10
+ return this.getStore().getState();
11
+ },
12
+ dispatch(action) {
13
+ return this.getStore().dispatch(action);
14
+ },
15
+ subscribe(listener) {
16
+ return this.getStore().subscribe(listener);
17
+ },
18
+ replaceReducer(reducer) {
19
+ return this.getStore().replaceReducer(reducer);
20
+ },
21
+ };
@@ -0,0 +1,24 @@
1
+ import React, { Component } from 'react';
2
+
3
+ export default class ErrorBoundary extends React.Component {
4
+ constructor(props) {
5
+ super(props);
6
+ this.state = { hasError: false };
7
+ }
8
+
9
+ static getDerivedStateFromError(error) {
10
+ // Update state so the next render will show the fallback UI.
11
+ return { hasError: true };
12
+ }
13
+
14
+ render() {
15
+ if (this.state.hasError) {
16
+ // You can render any custom fallback UI
17
+ return (
18
+ <div className="common-error-boundary">{this.props.message || 'Something went wrong.'}</div>
19
+ );
20
+ }
21
+
22
+ return this.props.children;
23
+ }
24
+ }
@@ -0,0 +1,6 @@
1
+ @import '../../styles/mixins';
2
+
3
+ .common-error-boundary {
4
+ color: red;
5
+ padding: 20px;
6
+ }
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { extendArray } from './utils';
3
+
4
+ /**
5
+ * A component that renders a list of nodes with ability to extend the list.
6
+ * @param {Array} items - The list of nodes to render.
7
+ */
8
+ export default function Nodes({ items = [], extName = 'items', extBase, extArgs }) {
9
+ extendArray(items, extName, extBase, extArgs);
10
+ const nodes = [];
11
+ items.filter(Boolean).forEach(n => {
12
+ let node;
13
+ if (n.render) node = n.render();
14
+ else if (n.content) node = n.content;
15
+ else if (n.component) node = <n.component key={n.key} {...n.props} />;
16
+ nodes.push(node);
17
+ });
18
+
19
+ return nodes;
20
+ }
@@ -0,0 +1,3 @@
1
+ export default function PageNotFound() {
2
+ return <div className="common-page-not-found">Page not found.</div>;
3
+ }
@@ -0,0 +1,6 @@
1
+ @import '../../styles/mixins';
2
+
3
+ .common-page-not-found {
4
+ color: red;
5
+ padding: 15px;
6
+ }
@@ -0,0 +1,4 @@
1
+ export { default as PageNotFound } from './PageNotFound';
2
+ export { default as ErrorBoundary } from './ErrorBoundary';
3
+ export { default as Nodes } from './Nodes';
4
+ export { default as useExtPoint } from './useExtPoint';
File without changes
File without changes
@@ -0,0 +1,12 @@
1
+ // Initial state is the place you define all initial values for the Redux store of the feature.
2
+ // In the 'standard' way, initialState is defined in reducers: http://redux.js.org/docs/basics/Reducers.html
3
+ // But when application grows, there will be multiple reducers files, it's not intuitive what data is managed by the whole store.
4
+ // So Rekit extracts the initial state definition into a separate module so that you can have
5
+ // a quick view about what data is used for the feature, at any time.
6
+
7
+ // NOTE: initialState constant is necessary so that Rekit could auto add initial state when creating async actions.
8
+
9
+ const initialState = {
10
+ };
11
+
12
+ export default initialState;
@@ -0,0 +1,24 @@
1
+ // This is the root reducer of the feature. It is used for:
2
+ // 1. Load reducers from each action in the feature and process them one by one.
3
+ // Note that this part of code is mainly maintained by Rekit, you usually don't need to edit them.
4
+ // 2. Write cross-topic reducers. If a reducer is not bound to some specific action.
5
+ // Then it could be written here.
6
+ // Learn more from the introduction of this approach:
7
+ // https://medium.com/@nate_wang/a-new-approach-for-managing-redux-actions-91c26ce8b5da.
8
+
9
+ import initialState from './initialState';
10
+
11
+ const reducers = [
12
+ ];
13
+
14
+ export default function reducer(state = initialState, action) {
15
+ let newState;
16
+ switch (action.type) {
17
+ // Handle cross-topic actions here
18
+ default:
19
+ newState = state;
20
+ break;
21
+ }
22
+ /* istanbul ignore next */
23
+ return reducers.reduce((s, r) => r(s, action), newState);
24
+ }
@@ -0,0 +1,9 @@
1
+ // This is the JSON way to define React Router rules in a Rekit app.
2
+ // Learn more from: http://rekit.js.org/docs/routing.html
3
+
4
+ export default {
5
+ path: 'common',
6
+ name: 'Common',
7
+ childRoutes: [
8
+ ],
9
+ };
@@ -0,0 +1,3 @@
1
+ @import '../../styles/mixins';
2
+ @import './PageNotFound';
3
+ @import './ErrorBoundary';
@@ -0,0 +1,28 @@
1
+ import { useState, useCallback } from 'react';
2
+ import jsPlugin from 'js-plugin';
3
+ import _ from 'lodash';
4
+
5
+ // allow use hooks in ext points
6
+ // TODO: it always flatten items, which may not be what we want?
7
+ export default function useExtPoint(extPointName, extArgs) {
8
+ const components = jsPlugin.invoke('!' + extPointName, extArgs);
9
+
10
+ const [values, setValues] = useState([]);
11
+ const handleCallback = useCallback(
12
+ value => {
13
+ setValues(_.flatten([...values, value]));
14
+ },
15
+ [values],
16
+ );
17
+
18
+ return {
19
+ extNode: (
20
+ <>
21
+ {components.map((Comp, index) => {
22
+ return <Comp key={index} callback={handleCallback} {...extArgs} />;
23
+ })}
24
+ </>
25
+ ),
26
+ values,
27
+ };
28
+ }
@@ -0,0 +1,20 @@
1
+ import jsPlugin from 'js-plugin';
2
+ import _ from 'lodash';
3
+
4
+ /**
5
+ * @description Make an array extensible by js plugins.
6
+ * @param {*} arr
7
+ * @param {*} extName
8
+ * @param {*} extBase
9
+ * @param {...any} args
10
+ */
11
+ export const extendArray = (arr, extName, extBase, ...args) => {
12
+ const capitalName = extName.charAt(0).toUpperCase() + extName.slice(1);
13
+ jsPlugin.invoke(`${extBase}.preProcess${capitalName}`, ...args);
14
+ const items = _.flatten(jsPlugin.invoke(`${extBase}.get${capitalName}`, ...args));
15
+ arr.push(...items);
16
+ jsPlugin.invoke(`${extBase}.process${capitalName}`, ...args);
17
+ jsPlugin.invoke(`${extBase}.postProcess${capitalName}`, ...args);
18
+ jsPlugin.sort(arr);
19
+ return arr;
20
+ };
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import plugin from 'js-plugin';
3
+ import useParentRouteChange from '../sub-app/hooks/useParentRouteChange';
4
+
5
+ export default function App({ children }) {
6
+ // home.rootComponent is used to define some global placeholder for something like Modals, Drawers, etc.
7
+ const rootComponentPlugins = plugin.getPlugins('rootComponent');
8
+
9
+ // home.mainLayout is used to define the main layout component
10
+ const layouts = plugin.invoke('!home.mainLayout');
11
+ useParentRouteChange();
12
+ if (layouts.length > 1) {
13
+ const ids = plugin.getPlugins('home.mainLayout').map(p => p.name);
14
+ return (
15
+ <div style={{ color: 'red', margin: '20px' }}>
16
+ Error: multiple layouts found from plugins: {ids.join(', ')}. Each application should only
17
+ have one main layout.
18
+ </div>
19
+ );
20
+ }
21
+ const Layout = layouts[0];
22
+ const ele = Layout ? <Layout>{children}</Layout> : children;
23
+ return (
24
+ <div className="home-app muse-app">
25
+ <div className="muse-app_main-page-container">{ele}</div>
26
+ <div className="muse-app_plugin-root-placeholder">
27
+ {rootComponentPlugins.map(p => (
28
+ <p.rootComponent key={p.name} />
29
+ ))}
30
+ </div>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,11 @@
1
+ @import '../../styles/mixins';
2
+
3
+ .home-app.muse-app {
4
+ .muse-app_plugin-root-placeholder {
5
+ position: absolute;
6
+ top: 0;
7
+ height: 0;
8
+ width: 0;
9
+ left: -8000px;
10
+ }
11
+ }
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import logo from '../../images/logo.png';
3
+
4
+ export default function Homepage() {
5
+ return (
6
+ <div className="home-homepage">
7
+ <header className="app-header">
8
+ <img src={logo} className="app-logo" alt="logo" />
9
+ <h1 className="app-title">Welcome to Muse!</h1>
10
+ </header>
11
+ <div className="app-intro">
12
+ <p className="memo">This is the default homepage of a Muse application.</p>
13
+ <h3>To get started:</h3>
14
+ <ul>
15
+ <li>
16
+ <a href="https://go/muse">Read docs</a> for Muse.
17
+ </li>
18
+ <li>
19
+ Join the slack channel{' '}
20
+ <a href="https://ebay-eng.slack.com/archives/C0194Q1V8G1">#muse</a>.
21
+ </li>
22
+ <li>
23
+ <a href="https://pages.github.corp.ebay.com/muse/muse-site/docs/get-started/first-plugin">
24
+ Create a plugin
25
+ </a>{' '}
26
+ and{' '}
27
+ <a href="https://pages.github.corp.ebay.com/muse/muse-site/docs/get-started/customize-layout">
28
+ define the layout/homepage
29
+ </a>{' '}
30
+ for the application.
31
+ </li>
32
+ <li>
33
+ <a href="https://pages.github.corp.ebay.com/muse/muse-site/docs/muse-management/plugin-management#build-a-plugin">
34
+ Build
35
+ </a>{' '}
36
+ and{' '}
37
+ <a href="https://pages.github.corp.ebay.com/muse/muse-site/docs/muse-management/plugin-management#deploy-a-plugin">
38
+ deploy
39
+ </a>{' '}
40
+ the Muse plugin to the application.
41
+ </li>
42
+ </ul>
43
+ </div>
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,63 @@
1
+ @import '../../styles/mixins';
2
+
3
+ .home-homepage {
4
+ text-align: center;
5
+ min-width: 600px;
6
+
7
+ .app {
8
+ text-align: center;
9
+ }
10
+
11
+ .app-logo {
12
+ height: 80px;
13
+ }
14
+
15
+ .app-header {
16
+ padding: 40px 20px 20px 20px;
17
+ }
18
+
19
+ h3 {
20
+ font-size: 20px;
21
+ font-weight: bold;
22
+ }
23
+
24
+ .app-title {
25
+ font-size: 26px;
26
+ margin-top: 10px;
27
+ }
28
+
29
+ .app-intro {
30
+ font-size: 16px;
31
+ }
32
+
33
+ ul,
34
+ li {
35
+ list-style: none;
36
+ margin: 0;
37
+ padding: 0;
38
+ }
39
+
40
+ ul {
41
+ margin-top: 20px;
42
+ }
43
+ li {
44
+ margin-top: 10px;
45
+ }
46
+
47
+ a {
48
+ color: #0288d1;
49
+ text-decoration: none;
50
+ &:hover {
51
+ text-decoration: underline;
52
+ }
53
+ }
54
+
55
+ p.memo {
56
+ color: #999;
57
+ line-height: 150%;
58
+
59
+ font-style: italic;
60
+ font-size: 14px;
61
+ margin: 20px auto;
62
+ }
63
+ }
@@ -0,0 +1,2 @@
1
+ export { default as App } from './App';
2
+ export { default as Homepage } from './Homepage';
File without changes
File without changes
@@ -0,0 +1,4 @@
1
+ const initialState = {
2
+ };
3
+
4
+ export default initialState;
@@ -0,0 +1,16 @@
1
+ import initialState from './initialState';
2
+
3
+ const reducers = [
4
+ ];
5
+
6
+ export default function reducer(state = initialState, action) {
7
+ let newState;
8
+ switch (action.type) {
9
+ // Handle cross-topic actions here
10
+ default:
11
+ newState = state;
12
+ break;
13
+ }
14
+ /* istanbul ignore next */
15
+ return reducers.reduce((s, r) => r(s, action), newState);
16
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ path: 'home',
3
+ name: 'Home',
4
+ childRoutes: [
5
+ ],
6
+ };
@@ -0,0 +1,3 @@
1
+ @import '../../styles/mixins';
2
+ @import './App';
3
+ @import './Homepage';
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+
3
+ export default function C2SProxyFailed() {
4
+ return (
5
+ <div className="sub-app-c-2-s-proxy-failed">
6
+ <h3>Error: failed to detect c2s proxy.</h3>
7
+ <p>This page needs c2s proxy, please config it and refresh the page.</p>
8
+ </div>
9
+ );
10
+ };
@@ -0,0 +1,19 @@
1
+ @import '../../styles/mixins';
2
+
3
+ .sub-app-c-2-s-proxy-failed {
4
+ position: absolute;
5
+ color: orange;
6
+ line-height: 150%;
7
+ margin: auto;
8
+ left: 50%;
9
+ margin-left: -300px;
10
+ top: 40%;
11
+ width: 600px;
12
+ padding: 20px;
13
+ border-radius: 4px;
14
+ border: 1px solid orange;
15
+
16
+ h3 {
17
+ color: orange;
18
+ }
19
+ }