@anmiles/google-api-wrapper 15.2.0 → 16.0.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 (48) hide show
  1. package/.eslintrc.js +0 -7
  2. package/CHANGELOG.md +10 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/lib/api.d.ts +3 -3
  6. package/dist/lib/api.d.ts.map +1 -0
  7. package/dist/lib/api.js +16 -10
  8. package/dist/lib/api.js.map +1 -1
  9. package/dist/lib/auth.d.ts +1 -0
  10. package/dist/lib/auth.d.ts.map +1 -0
  11. package/dist/lib/auth.js +2 -2
  12. package/dist/lib/auth.js.map +1 -1
  13. package/dist/lib/paths.d.ts +1 -0
  14. package/dist/lib/paths.d.ts.map +1 -0
  15. package/dist/lib/profiles.d.ts +1 -0
  16. package/dist/lib/profiles.d.ts.map +1 -0
  17. package/dist/lib/profiles.js.map +1 -1
  18. package/dist/lib/renderer.d.ts +5 -2
  19. package/dist/lib/renderer.d.ts.map +1 -0
  20. package/dist/lib/renderer.js +6 -3
  21. package/dist/lib/renderer.js.map +1 -1
  22. package/dist/lib/secrets.d.ts +1 -0
  23. package/dist/lib/secrets.d.ts.map +1 -0
  24. package/dist/lib/secrets.js +5 -5
  25. package/dist/lib/secrets.js.map +1 -1
  26. package/dist/templates/auth.html +2 -1
  27. package/dist/templates/css.html +111 -0
  28. package/dist/templates/page.html +1 -110
  29. package/dist/types/common.d.ts +1 -0
  30. package/dist/types/common.d.ts.map +1 -0
  31. package/dist/types/index.d.ts +1 -0
  32. package/dist/types/index.d.ts.map +1 -0
  33. package/dist/types/secrets.d.ts +1 -0
  34. package/dist/types/secrets.d.ts.map +1 -0
  35. package/jest.config.js +6 -4
  36. package/package.json +22 -25
  37. package/src/lib/__tests__/api.test.ts +7 -1
  38. package/src/lib/__tests__/renderer.test.ts +4 -3
  39. package/src/lib/__tests__/secrets.test.ts +3 -3
  40. package/src/lib/api.ts +15 -14
  41. package/src/lib/renderer.ts +6 -3
  42. package/src/lib/secrets.ts +2 -2
  43. package/src/templates/auth.html +2 -1
  44. package/src/templates/css.html +111 -0
  45. package/src/templates/page.html +1 -110
  46. package/src/types/out-url.d.ts +3 -0
  47. package/tsconfig.build.json +7 -0
  48. package/tsconfig.json +5 -19
@@ -0,0 +1,111 @@
1
+ <style type="text/css">
2
+ * {
3
+ box-sizing: border-box;
4
+ }
5
+
6
+ html, body {
7
+ width: 100%;
8
+ height: 100%;
9
+ margin: 0;
10
+ padding: 0;
11
+ }
12
+
13
+ body {
14
+ font-family: Arial, sans-serif;
15
+ font-size: 17px;
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ padding: 30px 0;
20
+ }
21
+
22
+ .box {
23
+ width: 450px;
24
+ min-height: 500px;
25
+ max-height: 100%;
26
+ padding: 82px 40px 28px 40px;
27
+ margin: 1em;
28
+ border: 1px solid #dadce0;
29
+ border-radius: 8px;
30
+ position: relative;
31
+ display: flex;
32
+ flex-direction: column;
33
+ align-items: center;
34
+ }
35
+
36
+ .box:before {
37
+ width: 100%;
38
+ height: 34px;
39
+ border-bottom: 1px solid #dadce0;
40
+ position: absolute;
41
+ top: 0;
42
+ display: block;
43
+ content: '';
44
+ }
45
+
46
+ h1 {
47
+ font-size: 24px;
48
+ line-height: 40px;
49
+ font-weight: normal;
50
+ margin: 0;
51
+ }
52
+
53
+ p {
54
+ line-height: 32px;
55
+ margin: 0;
56
+ }
57
+
58
+ ul {
59
+ width: 100%;
60
+ margin: 18px 0 30px 0;
61
+ padding-left: 0;
62
+ border-top: 1px solid #dadce0;
63
+ list-style-type: none;
64
+ overflow: auto;
65
+ }
66
+
67
+ li {
68
+ line-height: 48px;
69
+ color: brown;
70
+ border-bottom: 1px solid #dadce0;
71
+ display: flex;
72
+ align-items: center;
73
+ }
74
+
75
+ li:before {
76
+ content: 'W';
77
+ width: 28px;
78
+ height: 28px;
79
+ border-radius: 50%;
80
+ border: 2px solid currentColor;
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ font-size: 14px;
85
+ margin-right: 10px;
86
+ }
87
+
88
+ li.readonly {
89
+ color: darkgreen;
90
+ }
91
+
92
+ li.readonly:before {
93
+ content: 'R';
94
+ }
95
+
96
+ a {
97
+ width: 50%;
98
+ padding: 0 24px;
99
+ line-height: 36px;
100
+ margin: auto;
101
+ color: #ffffff;
102
+ background: #1a73e8;
103
+ border-radius: 4px;
104
+ outline: none;
105
+ display: block;
106
+ text-align: center;
107
+ text-decoration: none;
108
+ font-weight: bold;
109
+ font-size: 15px;
110
+ }
111
+ </style>
@@ -1,113 +1,4 @@
1
- <style type="text/css">
2
- * {
3
- box-sizing: border-box;
4
- }
5
-
6
- html, body {
7
- width: 100%;
8
- height: 100%;
9
- margin: 0;
10
- padding: 0;
11
- }
12
-
13
- body {
14
- font-family: Arial, sans-serif;
15
- font-size: 17px;
16
- display: flex;
17
- align-items: center;
18
- justify-content: center;
19
- padding: 30px 0;
20
- }
21
-
22
- .box {
23
- width: 450px;
24
- min-height: 500px;
25
- max-height: 100%;
26
- padding: 82px 40px 28px 40px;
27
- margin: 1em;
28
- border: 1px solid #dadce0;
29
- border-radius: 8px;
30
- position: relative;
31
- display: flex;
32
- flex-direction: column;
33
- align-items: center;
34
- }
35
-
36
- .box:before {
37
- width: 100%;
38
- height: 34px;
39
- border-bottom: 1px solid #dadce0;
40
- position: absolute;
41
- top: 0;
42
- display: block;
43
- content: '';
44
- }
45
-
46
- h1 {
47
- font-size: 24px;
48
- line-height: 40px;
49
- font-weight: normal;
50
- margin: 0;
51
- }
52
-
53
- p {
54
- line-height: 32px;
55
- margin: 0;
56
- }
57
-
58
- ul {
59
- width: 100%;
60
- margin: 18px 0 30px 0;
61
- padding-left: 0;
62
- border-top: 1px solid #dadce0;
63
- list-style-type: none;
64
- overflow: auto;
65
- }
66
-
67
- li {
68
- line-height: 48px;
69
- color: brown;
70
- border-bottom: 1px solid #dadce0;
71
- display: flex;
72
- align-items: center;
73
- }
74
-
75
- li:before {
76
- content: 'W';
77
- width: 28px;
78
- height: 28px;
79
- border-radius: 50%;
80
- border: 2px solid currentColor;
81
- display: flex;
82
- align-items: center;
83
- justify-content: center;
84
- font-size: 14px;
85
- margin-right: 10px;
86
- }
87
-
88
- li.readonly {
89
- color: darkgreen;
90
- }
91
-
92
- li.readonly:before {
93
- content: 'R';
94
- }
95
-
96
- a {
97
- width: 50%;
98
- padding: 0 24px;
99
- line-height: 36px;
100
- margin: auto;
101
- color: #ffffff;
102
- background: #1a73e8;
103
- border-radius: 4px;
104
- display: block;
105
- text-align: center;
106
- text-decoration: none;
107
- font-weight: bold;
108
- font-size: 15px;
109
- }
110
- </style>
1
+ ${css}
111
2
  <div class="box">
112
3
  ${content}
113
4
  </div>
@@ -1,3 +1,4 @@
1
1
  export interface CommonOptions {
2
2
  hideProgress?: boolean;
3
3
  }
4
+ //# sourceMappingURL=common.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/types/common.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC7B,YAAY,CAAC,EAAE,OAAO,CAAA;CACtB"}
@@ -1,2 +1,3 @@
1
1
  export * from './common';
2
2
  export * from './secrets';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC"}
@@ -13,3 +13,4 @@ export interface AuthOptions {
13
13
  temporary?: boolean;
14
14
  scopes?: string[];
15
15
  }
16
+ //# sourceMappingURL=secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/types/secrets.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,OAAO;IACvB,GAAG,EAAE;QACJ,SAAS,EAAE,GAAG,MAAM,6BAA6B,CAAC;QAClD,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,2CAA2C,CAAC;QACtD,SAAS,EAAE,qCAAqC,CAAC;QACjD,2BAA2B,EAAE,4CAA4C,CAAC;QAC1E,aAAa,EAAE,MAAM,CAAC;QACtB,aAAa,EAAE,MAAM,EAAE,CAAC;KACxB,CAAC;CACF;AAED,MAAM,WAAW,WAAW;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB"}
package/jest.config.js CHANGED
@@ -1,12 +1,14 @@
1
1
  module.exports = {
2
- preset : 'ts-jest',
2
+ preset : 'ts-jest',
3
+ transform : {
4
+ '^.+\\.tsx?$' : 'ts-jest',
5
+ },
6
+
3
7
  clearMocks : true,
4
8
 
5
9
  roots : [ '<rootDir>/src' ],
6
10
  testMatch : [ '<rootDir>/src/**/__tests__/*.test.ts' ],
7
- transform : {
8
- '^.+\\.ts$' : 'ts-jest',
9
- },
11
+
10
12
  collectCoverageFrom : [
11
13
  '<rootDir>/src/**/*.ts',
12
14
  '!<rootDir>/src/**/*.d.ts',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anmiles/google-api-wrapper",
3
- "version": "15.2.0",
3
+ "version": "16.0.0",
4
4
  "description": "Provides quick interface for getting google API data",
5
5
  "keywords": [
6
6
  "google",
@@ -16,45 +16,42 @@
16
16
  },
17
17
  "main": "dist/index.js",
18
18
  "scripts": {
19
- "build": "rimraf dist && tsc && copyfiles -u 1 src/templates/* dist/",
20
- "lint": "eslint --ext .js,.ts .",
19
+ "build": "rimraf dist && tsc -p ./tsconfig.build.json && copyfiles -u 1 src/templates/* dist/",
20
+ "lint": "eslint --ext .js,.ts --ignore-path .gitignore .",
21
21
  "lint:fix": "npm run lint -- --fix",
22
22
  "test": "jest --verbose",
23
23
  "test:coverage": "npm test -- --coverage",
24
24
  "test:ci": "npm test -- --ci --coverage",
25
25
  "test:watch": "npm test -- --watch",
26
26
  "test:watch:coverage": "npm test -- --watch --coverage",
27
- "test:report:coverage": "nyc report --nycrc-path ./coverage.config.js -t ./coverage --report-dir ./coverage",
28
- "create": "node ./dist/create.js",
29
- "login": "node ./dist/login.js"
27
+ "test:report:coverage": "nyc report --nycrc-path ./coverage.config.js -t ./coverage --report-dir ./coverage"
30
28
  },
31
29
  "dependencies": {
32
- "@anmiles/google-api-wrapper": "file:.",
33
- "@anmiles/logger": "^3.0.0",
34
- "@anmiles/prototypes": "^2.3.0",
35
- "@anmiles/sleep": "^1.0.3",
36
- "execa": "^5.1.1",
37
- "googleapis": "^126.0.1",
38
- "open": "^8.4.2",
30
+ "@anmiles/logger": "^6.0.0",
31
+ "@anmiles/prototypes": "^7.0.0",
32
+ "@anmiles/sleep": "^3.0.0",
33
+ "googleapis": "^130.0.0",
34
+ "out-url": "^1.1.3",
39
35
  "server-destroy": "^1.0.1"
40
36
  },
41
37
  "devDependencies": {
42
- "@anmiles/eslint-config": "^2.0.0",
43
- "@types/event-emitter": "^0.3.3",
44
- "@types/jest": "^29.5.4",
45
- "@types/server-destroy": "^1.0.1",
46
- "@typescript-eslint/eslint-plugin": "^6.7.0",
47
- "@typescript-eslint/parser": "^6.7.0",
38
+ "@anmiles/eslint-config": "^5.0.0",
39
+ "@anmiles/tsconfig": "^2.0.0",
40
+ "@types/event-emitter": "^0.3.5",
41
+ "@types/jest": "^29.5.11",
42
+ "@types/server-destroy": "^1.0.3",
43
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
44
+ "@typescript-eslint/parser": "^6.19.0",
48
45
  "copyfiles": "^2.4.1",
49
- "eslint": "^8.49.0",
46
+ "eslint": "^8.56.0",
50
47
  "eslint-plugin-align-assignments": "^1.1.2",
51
- "eslint-plugin-import": "^2.28.1",
52
- "eslint-plugin-jest": "^27.2.3",
48
+ "eslint-plugin-import": "^2.29.1",
49
+ "eslint-plugin-jest": "^27.6.3",
53
50
  "event-emitter": "^0.3.5",
54
- "jest": "^29.6.4",
51
+ "jest": "^29.7.0",
55
52
  "nyc": "^15.1.0",
56
- "rimraf": "^5.0.1",
53
+ "rimraf": "^5.0.5",
57
54
  "ts-jest": "^29.1.1",
58
- "typescript": "^5.2.2"
55
+ "typescript": "^5.3.3"
59
56
  }
60
57
  }
@@ -108,7 +108,7 @@ describe('src/lib/api', () => {
108
108
  it('should return instance wrapper for google api', async () => {
109
109
  const instance = await api.getAPI('calendar', profile, { scopes, temporary : true });
110
110
 
111
- expect(instance).toEqual({ apiName : 'calendar', profile, authOptions : { scopes, temporary : true }, api : calendarAPI, auth : googleAuth });
111
+ expect(instance).toEqual({ apiName : 'calendar', profile, authOptions : { scopes, temporary : true }, api : calendarAPI });
112
112
  });
113
113
 
114
114
  it('should warn when creating permanent credentials using non-readonly scopes', async () => {
@@ -228,6 +228,12 @@ describe('src/lib/api', () => {
228
228
 
229
229
  expect(items).toEqual(items);
230
230
  });
231
+
232
+ it('should throw if api was not initialized before getting items', async () => {
233
+ instance = new api.API('calendar', profile);
234
+
235
+ await expect(() => instance.getItems((api) => api.calendarList, args)).rejects.toEqual('API is not initialized. Call `init` before getting items.');
236
+ });
231
237
  });
232
238
  });
233
239
  });
@@ -36,7 +36,8 @@ jest.mock<Partial<typeof paths>>('../paths', () => ({
36
36
  }));
37
37
 
38
38
  const mockTemplates: Record<TemplateName, string> = {
39
- page : 'page content = (${content})',
39
+ page : 'page css = (${css}) content = (${content})',
40
+ css : 'css',
40
41
  auth : 'auth profile = (${profile}) authUrl = (${authUrl}) scopesList = (${scopesList})',
41
42
  scope : 'scope type = (${type}) title = (${title}) name = (${name})',
42
43
  done : 'done profile = (${profile})',
@@ -50,14 +51,14 @@ describe('src/lib/renderer', () => {
50
51
  describe('renderAuth', () => {
51
52
  it('should return auth page', () => {
52
53
  const result = original.renderAuth({ authUrl, profile, scope });
53
- expect(result).toEqual('page content = (auth profile = (username) authUrl = (https://authUrl) scopesList = (scope type = (readonly) title = (Readonly (cannot change or delete your data)) name = (scope1.readonly)\nscope type = () title = (Writable (can change or delete your data)) name = (scope2)))');
54
+ expect(result).toEqual('page css = (css) content = (auth profile = (username) authUrl = (https://authUrl) scopesList = (scope type = (readonly) title = (Readonly (cannot change or delete your data)) name = (scope1.readonly)\nscope type = () title = (Writable (can change or delete your data)) name = (scope2)))');
54
55
  });
55
56
  });
56
57
 
57
58
  describe('renderDone', () => {
58
59
  it('should return done page', () => {
59
60
  const result = original.renderDone({ profile });
60
- expect(result).toEqual('page content = (done profile = (username))');
61
+ expect(result).toEqual('page css = (css) content = (done profile = (username))');
61
62
  });
62
63
  });
63
64
 
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import http from 'http';
3
3
  import path from 'path';
4
- import open from 'open';
4
+ import { open } from 'out-url';
5
5
  import type GoogleApis from 'googleapis';
6
6
  import logger from '@anmiles/logger';
7
7
  import emitter from 'event-emitter';
@@ -54,8 +54,8 @@ jest.mock<Partial<typeof path>>('path', () => ({
54
54
  join : jest.fn().mockImplementation((...args) => args.join('/')),
55
55
  }));
56
56
 
57
- jest.mock('open', () => jest.fn().mockImplementation((url: string) => {
58
- makeRequest(url.replace('http://localhost:6006', ''));
57
+ jest.mock<{ open: typeof open }>('out-url', () => ({
58
+ open : jest.fn().mockImplementation(async (url) => makeRequest(url.replace('http://localhost:6006', ''))),
59
59
  }));
60
60
 
61
61
  jest.mock<Partial<typeof logger>>('@anmiles/logger', () => ({
package/src/lib/api.ts CHANGED
@@ -24,22 +24,17 @@ type CommonResponse<TItem> = {
24
24
  };
25
25
 
26
26
  class API<TKey extends keyof typeof allAPIs> {
27
- api: ReturnType<typeof allAPIs[TKey]>;
28
- private auth: GoogleApis.Common.OAuth2Client;
27
+ api: ReturnType<typeof allAPIs[TKey]> | undefined;
29
28
 
30
- private apiName: TKey;
31
- private profile: string;
32
- private authOptions?: AuthOptions;
33
-
34
- constructor(apiName: TKey, profile: string, authOptions?: AuthOptions) {
35
- this.apiName = apiName;
36
- this.profile = profile;
37
- this.authOptions = authOptions;
38
- }
29
+ constructor(
30
+ private apiName: TKey,
31
+ private profile: string,
32
+ private authOptions?: AuthOptions,
33
+ ) { }
39
34
 
40
35
  async init() {
41
- this.auth = await getAuth(this.profile, this.authOptions);
42
- this.api = allAPIs[this.apiName](this.auth) as ReturnType<typeof allAPIs[TKey]>;
36
+ const auth = await getAuth(this.profile, this.authOptions);
37
+ this.api = allAPIs[this.apiName](auth) as ReturnType<typeof allAPIs[TKey]>;
43
38
  }
44
39
 
45
40
  async getItems<TItem>(selectAPI: (api: ReturnType<typeof allAPIs[TKey]>) => CommonAPI<TItem>, params: any, options?: CommonOptions): Promise<TItem[]> {
@@ -51,9 +46,15 @@ class API<TKey extends keyof typeof allAPIs> {
51
46
  let response: GoogleApis.Common.GaxiosResponse<CommonResponse<TItem>>;
52
47
 
53
48
  try {
49
+ if (!this.api) {
50
+ throw 'API is not initialized. Call `init` before getting items.';
51
+ }
52
+
54
53
  response = await selectAPI(this.api).list({ ...params, pageToken });
55
54
  } catch (ex) {
56
- if ((ex.message === 'invalid_grant' || ex.message === 'Invalid credentials') && !this.authOptions?.temporary) {
55
+ const message = ex instanceof Error ? ex.message : ex as string;
56
+
57
+ if ((message === 'invalid_grant' || message === 'Invalid credentials') && !this.authOptions?.temporary) {
57
58
  warn('Access token stored is invalid, re-creating...');
58
59
  deleteCredentials(this.profile);
59
60
  await this.init();
@@ -5,7 +5,8 @@ import { getTemplateFile } from './paths';
5
5
  export { templates, renderAuth, renderDone };
6
6
 
7
7
  const templates = {
8
- page : [ 'content' ] as const,
8
+ page : [ 'css', 'content' ] as const,
9
+ css : [ ] as const,
9
10
  auth : [ 'profile', 'authUrl', 'scopesList' ],
10
11
  scope : [ 'type', 'title', 'name' ] as const,
11
12
  done : [ 'profile' ] as const,
@@ -22,13 +23,15 @@ function renderAuth({ profile, authUrl, scope }: { profile: string, authUrl: str
22
23
  type : s.endsWith('.readonly') ? 'readonly' : '',
23
24
  })).join('\n');
24
25
 
26
+ const css = render('css', {});
25
27
  const content = render('auth', { profile, authUrl, scopesList });
26
- return render('page', { content });
28
+ return render('page', { css, content });
27
29
  }
28
30
 
29
31
  function renderDone({ profile }: { profile: string }): string {
32
+ const css = render('css', {});
30
33
  const content = render('done', { profile });
31
- return render('page', { content });
34
+ return render('page', { css, content });
32
35
  }
33
36
 
34
37
  // TODO: Use react
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import http from 'http';
3
+ import { open } from 'out-url';
3
4
  import enableDestroy from 'server-destroy';
4
- import open from 'open';
5
5
  import type GoogleApis from 'googleapis';
6
6
  import { warn } from '@anmiles/logger';
7
7
  import type { Secrets, AuthOptions } from '../types';
@@ -115,7 +115,7 @@ async function createCredentials(profile: string, auth: GoogleApis.Auth.OAuth2Cl
115
115
  }
116
116
  });
117
117
 
118
- server.once('listening', () => {
118
+ server.once('listening', async () => {
119
119
  warn('Please check your browser for further actions');
120
120
  open(startURI);
121
121
  });
@@ -3,4 +3,5 @@
3
3
  <ul>
4
4
  ${scopesList}
5
5
  </ul>
6
- <a href="${authUrl}">Continue</a>
6
+ <a id="button" href="${authUrl}">Continue</a>
7
+ <script type="text/javascript">document.addEventListener('DOMContentLoaded', function(){ document.getElementById('button').focus(); });</script>
@@ -0,0 +1,111 @@
1
+ <style type="text/css">
2
+ * {
3
+ box-sizing: border-box;
4
+ }
5
+
6
+ html, body {
7
+ width: 100%;
8
+ height: 100%;
9
+ margin: 0;
10
+ padding: 0;
11
+ }
12
+
13
+ body {
14
+ font-family: Arial, sans-serif;
15
+ font-size: 17px;
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ padding: 30px 0;
20
+ }
21
+
22
+ .box {
23
+ width: 450px;
24
+ min-height: 500px;
25
+ max-height: 100%;
26
+ padding: 82px 40px 28px 40px;
27
+ margin: 1em;
28
+ border: 1px solid #dadce0;
29
+ border-radius: 8px;
30
+ position: relative;
31
+ display: flex;
32
+ flex-direction: column;
33
+ align-items: center;
34
+ }
35
+
36
+ .box:before {
37
+ width: 100%;
38
+ height: 34px;
39
+ border-bottom: 1px solid #dadce0;
40
+ position: absolute;
41
+ top: 0;
42
+ display: block;
43
+ content: '';
44
+ }
45
+
46
+ h1 {
47
+ font-size: 24px;
48
+ line-height: 40px;
49
+ font-weight: normal;
50
+ margin: 0;
51
+ }
52
+
53
+ p {
54
+ line-height: 32px;
55
+ margin: 0;
56
+ }
57
+
58
+ ul {
59
+ width: 100%;
60
+ margin: 18px 0 30px 0;
61
+ padding-left: 0;
62
+ border-top: 1px solid #dadce0;
63
+ list-style-type: none;
64
+ overflow: auto;
65
+ }
66
+
67
+ li {
68
+ line-height: 48px;
69
+ color: brown;
70
+ border-bottom: 1px solid #dadce0;
71
+ display: flex;
72
+ align-items: center;
73
+ }
74
+
75
+ li:before {
76
+ content: 'W';
77
+ width: 28px;
78
+ height: 28px;
79
+ border-radius: 50%;
80
+ border: 2px solid currentColor;
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ font-size: 14px;
85
+ margin-right: 10px;
86
+ }
87
+
88
+ li.readonly {
89
+ color: darkgreen;
90
+ }
91
+
92
+ li.readonly:before {
93
+ content: 'R';
94
+ }
95
+
96
+ a {
97
+ width: 50%;
98
+ padding: 0 24px;
99
+ line-height: 36px;
100
+ margin: auto;
101
+ color: #ffffff;
102
+ background: #1a73e8;
103
+ border-radius: 4px;
104
+ outline: none;
105
+ display: block;
106
+ text-align: center;
107
+ text-decoration: none;
108
+ font-weight: bold;
109
+ font-size: 15px;
110
+ }
111
+ </style>