@things-factory/auth-ui 7.0.1-rc.0 → 7.0.1-rc.10

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 (62) hide show
  1. package/client/auth-style-sign.ts +2 -1
  2. package/client/components/abstract-sign.ts +30 -72
  3. package/client/components/contact-us.ts +0 -4
  4. package/client/components/delete-user-popup.ts +1 -0
  5. package/client/components/partner-role-editor.ts +0 -4
  6. package/client/components/profile-component.ts +27 -91
  7. package/client/components/role-privilege-editor.ts +0 -3
  8. package/client/components/user-role-editor.ts +0 -4
  9. package/client/entries/auth/activate.ts +4 -5
  10. package/client/entries/auth/checkin.ts +5 -2
  11. package/client/entries/auth/result.ts +5 -3
  12. package/client/entries/oauth2/oauth2-decision-page.ts +0 -4
  13. package/client/pages/app-binding/app-binding.ts +38 -22
  14. package/client/pages/app-binding/app-bindings.ts +20 -1
  15. package/client/pages/appliance/appliance.ts +75 -18
  16. package/client/pages/appliance/home.ts +23 -1
  17. package/client/pages/application/application.ts +93 -40
  18. package/client/pages/application/applications.ts +19 -1
  19. package/dist-client/auth-style-sign.js +2 -1
  20. package/dist-client/auth-style-sign.js.map +1 -1
  21. package/dist-client/components/abstract-sign.d.ts +1 -1
  22. package/dist-client/components/abstract-sign.js +26 -60
  23. package/dist-client/components/abstract-sign.js.map +1 -1
  24. package/dist-client/components/contact-us.js +0 -4
  25. package/dist-client/components/contact-us.js.map +1 -1
  26. package/dist-client/components/delete-user-popup.js +1 -0
  27. package/dist-client/components/delete-user-popup.js.map +1 -1
  28. package/dist-client/components/partner-role-editor.js +0 -4
  29. package/dist-client/components/partner-role-editor.js.map +1 -1
  30. package/dist-client/components/profile-component.d.ts +1 -1
  31. package/dist-client/components/profile-component.js +25 -80
  32. package/dist-client/components/profile-component.js.map +1 -1
  33. package/dist-client/components/role-privilege-editor.js +0 -3
  34. package/dist-client/components/role-privilege-editor.js.map +1 -1
  35. package/dist-client/components/user-role-editor.js +0 -4
  36. package/dist-client/components/user-role-editor.js.map +1 -1
  37. package/dist-client/entries/auth/activate.js +4 -5
  38. package/dist-client/entries/auth/activate.js.map +1 -1
  39. package/dist-client/entries/auth/checkin.js +5 -2
  40. package/dist-client/entries/auth/checkin.js.map +1 -1
  41. package/dist-client/entries/auth/result.js +5 -3
  42. package/dist-client/entries/auth/result.js.map +1 -1
  43. package/dist-client/entries/oauth2/oauth2-decision-page.js +0 -4
  44. package/dist-client/entries/oauth2/oauth2-decision-page.js.map +1 -1
  45. package/dist-client/pages/app-binding/app-binding.js +38 -22
  46. package/dist-client/pages/app-binding/app-binding.js.map +1 -1
  47. package/dist-client/pages/app-binding/app-bindings.d.ts +2 -1
  48. package/dist-client/pages/app-binding/app-bindings.js +19 -1
  49. package/dist-client/pages/app-binding/app-bindings.js.map +1 -1
  50. package/dist-client/pages/appliance/appliance.js +75 -18
  51. package/dist-client/pages/appliance/appliance.js.map +1 -1
  52. package/dist-client/pages/appliance/home.d.ts +2 -1
  53. package/dist-client/pages/appliance/home.js +22 -1
  54. package/dist-client/pages/appliance/home.js.map +1 -1
  55. package/dist-client/pages/application/application.js +93 -40
  56. package/dist-client/pages/application/application.js.map +1 -1
  57. package/dist-client/pages/application/applications.d.ts +2 -1
  58. package/dist-client/pages/application/applications.js +18 -1
  59. package/dist-client/pages/application/applications.js.map +1 -1
  60. package/dist-client/tsconfig.tsbuildinfo +1 -1
  61. package/dist-server/tsconfig.tsbuildinfo +1 -1
  62. package/package.json +4 -4
@@ -3,7 +3,8 @@ import { css } from 'lit'
3
3
  export const AUTH_STYLE_SIGN = css`
4
4
  :host {
5
5
  display: flex;
6
- background-color: var(--auth-background, var(--md-sys-color-primary));
6
+ background-color: var(--md-sys-color-primary);
7
+ color: var(--md-sys-color-on-primary);
7
8
  }
8
9
 
9
10
  :host *:focus {
@@ -1,8 +1,8 @@
1
- import { html, nothing } from 'lit'
2
- import base64url from 'base64url'
3
-
4
1
  import '@operato/i18n/ox-i18n.js'
5
2
 
3
+ import { html, nothing } from 'lit'
4
+ import { startAuthentication } from '@simplewebauthn/browser'
5
+
6
6
  import { i18next } from '@operato/i18n'
7
7
  import { notify } from '@operato/layout'
8
8
 
@@ -85,84 +85,42 @@ export abstract class AbstractSign extends AbstractAuthPage {
85
85
  <ox-i18n msgid="field.${this.pageName}"> </ox-i18n>
86
86
  </md-elevated-button>
87
87
  ${isAvailableWebauthn
88
- ? html` <md-icon class="fingerprint" raised @click=${e => this.signinWithAuthn()}> fingerprint </md-icon>`
88
+ ? html` <md-icon class="fingerprint" raised @click=${e => this.authenticateUser()}> fingerprint </md-icon>`
89
89
  : nothing}
90
90
  </div>
91
91
  `
92
92
  }
93
93
 
94
- async signinWithAuthn() {
95
- const response = await fetch('/auth/signin-webauthn/challenge', {
96
- method: 'POST',
97
- headers: {
98
- 'Content-Type': 'application/json',
99
- Accept: 'application/json'
100
- },
101
- credentials: 'include'
102
- })
103
-
104
- if (!response.ok) {
105
- notify({
106
- level: 'error',
107
- message: await response.text()
108
- })
109
-
110
- return
111
- }
112
-
113
- const options = await response.json()
114
- const credential = (await navigator.credentials.get({
115
- publicKey: {
116
- challenge: Buffer.from(options.challenge, 'base64')
117
- }
118
- })) as PublicKeyCredential
119
-
120
- if (!credential) {
121
- notify({
122
- level: 'error',
123
- message: 'can not get user credential'
124
- })
125
-
126
- return
127
- }
128
-
129
- const assertionResponse = {
130
- id: credential.id,
131
- response: {
132
- clientDataJSON: base64url.encode(Buffer.from(credential.response.clientDataJSON)),
133
- authenticatorData: base64url.encode((credential.response as any).authenticatorData),
134
- signature: base64url.encode((credential.response as any).signature),
135
- userHandle: (credential.response as any).userHandle
136
- ? base64url.encode((credential.response as any).userHandle)
137
- : null
94
+ async authenticateUser() {
95
+ try {
96
+ const options = await fetch('/auth/signin-webauthn/challenge').then(res => res.json())
97
+ const assertionResp = await startAuthentication(options)
98
+ const verification = await fetch('/auth/signin-webauthn', {
99
+ method: 'POST',
100
+ headers: {
101
+ 'Content-Type': 'application/json'
102
+ },
103
+ body: JSON.stringify(assertionResp)
104
+ }).then(res => res.json())
105
+
106
+ if (verification.verified) {
107
+ const { redirectURL } = verification
108
+
109
+ if (redirectURL) {
110
+ window.location.href = redirectURL
111
+ }
112
+ } else {
113
+ notify({
114
+ level: 'error',
115
+ message: verification.message
116
+ })
138
117
  }
139
- } as AssertionResponse
140
-
141
- if (credential.authenticatorAttachment) {
142
- assertionResponse.authenticatorAttachment = credential.authenticatorAttachment as AuthenticatorAttachment
143
- }
144
-
145
- const signinResponse = await fetch('/auth/signin-webauthn', {
146
- method: 'POST',
147
- headers: {
148
- 'Content-Type': 'application/json',
149
- Accept: 'application/json'
150
- },
151
- body: JSON.stringify(assertionResponse),
152
- credentials: 'include'
153
- })
154
-
155
- if (!signinResponse.ok) {
118
+
119
+ } catch (error) {
156
120
  notify({
157
121
  level: 'error',
158
- message: await signinResponse.text()
122
+ message: i18next.t('error.authn verification failed')
159
123
  })
160
- } else {
161
- const { redirectURL } = await signinResponse.json()
162
-
163
- if (redirectURL) {
164
- window.location.href = redirectURL
165
- }
166
124
  }
167
125
  }
168
126
  }
@@ -16,10 +16,6 @@ export class ContactUs extends localize(i18next)(LitElement) {
16
16
  static get styles() {
17
17
  return [
18
18
  css`
19
- :host {
20
- --md-theme-primary: var(--md-sys-color-primary);
21
- }
22
-
23
19
  * {
24
20
  box-sizing: border-box;
25
21
  }
@@ -14,6 +14,7 @@ export class DeleteUserPopup extends localize(i18next)(LitElement) {
14
14
  :host {
15
15
  display: flex;
16
16
  flex-direction: column;
17
+ color: var(--popup-content-color);
17
18
  background-color: var(--popup-content-background-color);
18
19
  padding: var(--popup-content-padding);
19
20
  }
@@ -30,10 +30,6 @@ class PartnerRoleEditor extends localize(i18next)(LitElement) {
30
30
  md-elevated-button {
31
31
  margin-right: var(--padding-narrow);
32
32
  }
33
-
34
- [danger] {
35
- --md-theme-primary: var(--md-danger-button-primary-color);
36
- }
37
33
  `
38
34
  ]
39
35
 
@@ -7,6 +7,7 @@ import './my-login-history'
7
7
  import base64url from 'base64url'
8
8
  import { css, html, LitElement, nothing } from 'lit'
9
9
  import { customElement, property, query, state } from 'lit/decorators.js'
10
+ import { startRegistration } from '@simplewebauthn/browser'
10
11
 
11
12
  import { i18next, localize } from '@operato/i18n'
12
13
  import { notify, openPopup } from '@operato/layout'
@@ -86,10 +87,6 @@ export class ProfileComponent extends localize(i18next)(LitElement) {
86
87
  margin: var(--change-password-field-margin);
87
88
  }
88
89
 
89
- [danger] {
90
- --md-theme-primary: var(--md-danger-button-primary-color);
91
- }
92
-
93
90
  footer {
94
91
  padding: 20px;
95
92
  text-align: center;
@@ -168,7 +165,7 @@ export class ProfileComponent extends localize(i18next)(LitElement) {
168
165
 
169
166
  ${isAvailableWebauthn
170
167
  ? html`
171
- <md-text-button @click=${() => this.registerWebAuthn()}
168
+ <md-text-button @click=${() => this.registerUser()}
172
169
  >${i18next.t('button.security-key registration')}</md-text-button
173
170
  >
174
171
  `
@@ -248,96 +245,35 @@ export class ProfileComponent extends localize(i18next)(LitElement) {
248
245
  })
249
246
  }
250
247
 
251
- async registerWebAuthn() {
252
- const challenge = await fetch('/auth/register-webauthn/challenge', {
253
- method: 'POST',
254
- headers: {
255
- 'Content-Type': 'application/json',
256
- Accept: 'application/json'
257
- },
258
- body: JSON.stringify({ id: this.userId }),
259
- credentials: 'include'
260
- })
261
-
262
- if (!challenge.ok) {
263
- notify({
264
- level: 'error',
265
- message: await challenge.text()
266
- })
267
-
268
- return
269
- }
270
-
271
- const options = await challenge.json()
272
-
273
- const credential = (await navigator.credentials.create({
274
- publicKey: {
275
- rp: {
276
- name: this.applicationMeta.title
277
- },
278
- user: {
279
- id: Uint8Array.from(base64url.toBuffer(options.user.id)).buffer,
280
- name: options.user.name,
281
- displayName: options.user.displayName
248
+ async registerUser() {
249
+ try {
250
+ const options = await fetch('/auth/register-webauthn/challenge').then(res => res.json())
251
+ const attResp = await startRegistration(options)
252
+ const verification = await fetch('/auth/verify-registration', {
253
+ method: 'POST',
254
+ headers: {
255
+ 'Content-Type': 'application/json'
282
256
  },
283
- challenge: Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0)),
284
- pubKeyCredParams: [
285
- {
286
- type: 'public-key',
287
- alg: -7 // ES256
288
- },
289
- {
290
- type: 'public-key',
291
- alg: -257 // RS256
292
- }
293
- ],
294
- authenticatorSelection: {
295
- userVerification: 'preferred'
296
- }
257
+ body: JSON.stringify(attResp)
258
+ }).then(res => res.json())
259
+
260
+ if (verification.verified) {
261
+ notify({
262
+ level: 'info',
263
+ message: i18next.t('text.user credential registered successfully')
264
+ })
265
+ } else {
266
+ console.error(await verification.text())
267
+
268
+ notify({
269
+ level: 'error',
270
+ message: i18next.t('error.user credential registeration not allowed')
271
+ })
297
272
  }
298
- })) as PublicKeyCredential
299
-
300
- if (!credential) {
273
+ } catch (error) {
301
274
  notify({
302
275
  level: 'error',
303
- message: 'can not get user credential'
304
- })
305
-
306
- return
307
- }
308
-
309
- const response = credential.response as AuthenticatorAttestationResponse
310
-
311
- var body = {
312
- response: {
313
- clientDataJSON: Buffer.from(response.clientDataJSON).toString('base64'),
314
- attestationObject: Buffer.from(response.attestationObject).toString('base64')
315
- } as any
316
- }
317
-
318
- if (response.getTransports) {
319
- body.response.transports = response.getTransports()
320
- }
321
-
322
- const signinResponse = await fetch('/auth/signin-webauthn', {
323
- method: 'POST',
324
- headers: {
325
- 'Content-Type': 'application/json',
326
- Accept: 'application/json'
327
- },
328
- body: JSON.stringify(body),
329
- credentials: 'include'
330
- })
331
-
332
- if (!signinResponse.ok) {
333
- notify({
334
- level: 'error',
335
- message: await signinResponse.text()
336
- })
337
- } else {
338
- notify({
339
- level: 'info',
340
- message: i18next.t('text.user credential registered successfully')
276
+ message: i18next.t('error.user credential registeration failed')
341
277
  })
342
278
  }
343
279
  }
@@ -56,9 +56,6 @@ class RolePrivilegeEditor extends localize(i18next)(LitElement) {
56
56
  background-color: var(--md-sys-color-surface-variant);
57
57
  }
58
58
 
59
- [danger] {
60
- --md-theme-primary: var(--md-danger-button-primary-color);
61
- }
62
59
  md-outlined-button {
63
60
  background-color: var(--md-sys-color-surface-variant);
64
61
  }
@@ -71,10 +71,6 @@ class UserRoleEditor extends connect(store)(LitElement) {
71
71
  margin: 5px;
72
72
  }
73
73
 
74
- [danger] {
75
- --md-theme-primary: var(--md-danger-button-primary-color);
76
- }
77
-
78
74
  md-outlined-button {
79
75
  background-color: var(--md-sys-color-surface-variant);
80
76
  }
@@ -20,7 +20,8 @@ export class AuthActivate extends localize(i18next)(LitElement) {
20
20
  width: 100vw;
21
21
  height: 100vh;
22
22
  height: 100dvh;
23
- background-color: var(--auth-background, var(--md-sys-color-primary));
23
+ background-color: var(--md-sys-color-primary);
24
+ color: var(--md-sys-color-on-primary);
24
25
  }
25
26
 
26
27
  .wrap {
@@ -69,10 +70,8 @@ export class AuthActivate extends localize(i18next)(LitElement) {
69
70
  }
70
71
 
71
72
  md-elevated-button {
72
- --md-theme-primary: var(--md-sys-color-on-primary);
73
- --md-theme-on-primary: var(--md-sys-color-primary);
74
- --md-button-horizontal-padding: var(--spacing-medium);
75
- --md-button-ink-color: var(--md-sys-color-primary);
73
+ --md-elevated-button-horizontal-padding: var(--spacing-medium);
74
+ --md-elevated-button-ink-color: var(--md-sys-color-primary);
76
75
  }
77
76
 
78
77
  contact-us {
@@ -21,12 +21,14 @@ export class AuthCheckIn extends localize(i18next)(LitElement) {
21
21
  display: flex;
22
22
  flex-direction: column;
23
23
  margin: auto;
24
- background-color: var(--auth-background, var(--md-sys-color-primary));
24
+ background-color: var(--md-sys-color-primary);
25
+ color: var(--md-sys-color-on-primary);
25
26
  height: 100vh;
26
27
  height: 100dvh;
27
28
  }
28
29
  .header {
29
30
  background-color: var(--md-sys-color-primary);
31
+ color: var(--md-sys-color-on-primary);
30
32
  height: var(--checkin-header-height);
31
33
  }
32
34
  .content {
@@ -38,6 +40,7 @@ export class AuthCheckIn extends localize(i18next)(LitElement) {
38
40
  margin: var(--margin-wide) 0;
39
41
  padding: 0;
40
42
  background-color: var(--md-sys-color-surface);
43
+ color: var(--md-sys-color-on-surface);
41
44
  border-radius: var(--border-radius);
42
45
  border: var(--border-dim-color);
43
46
  list-style: none;
@@ -47,13 +50,13 @@ export class AuthCheckIn extends localize(i18next)(LitElement) {
47
50
  margin-bottom: -1px;
48
51
  padding: var(--padding-default) var(--padding-wide);
49
52
  font-size: 18px;
50
- color: var(--md-sys-color-secondary);
51
53
  text-align: left;
52
54
 
53
55
  cursor: pointer;
54
56
  }
55
57
  li:hover {
56
58
  background-color: var(--md-sys-color-primary-container);
59
+ color: var(--md-sys-color-on-primary-container);
57
60
  }
58
61
  li span {
59
62
  display: block;
@@ -18,7 +18,8 @@ export class AuthResult extends localize(i18next)(LitElement) {
18
18
  width: 100vw;
19
19
  height: 100vh;
20
20
  height: 100dvh;
21
- background-color: var(--auth-background, var(--md-sys-color-primary));
21
+ background-color: var(--md-sys-color-primary);
22
+ color: var(--md-sys-color-on-primary);
22
23
  }
23
24
  .wrap {
24
25
  display: block;
@@ -70,16 +71,17 @@ export class AuthResult extends localize(i18next)(LitElement) {
70
71
  font: bold 14px var(--theme-font);
71
72
  color: var(--md-sys-color-primary);
72
73
  }
74
+
73
75
  #button-area {
74
76
  border-top: 1px dashed rgba(0, 0, 0, 0.1);
75
77
  padding-top: 10px;
76
78
  }
79
+
77
80
  md-elevated-button {
78
- --md-theme-primary: var(--auth-button-background-color);
79
- --md-theme-on-primary: var(--md-sys-color-primary);
80
81
  --md-button-horizontal-padding: var(--padding-default);
81
82
  --md-button-ink-color: var(--md-sys-color-primary);
82
83
  }
84
+
83
85
  .lottie-container {
84
86
  width: 100%;
85
87
  height: 300px;
@@ -61,10 +61,6 @@ class OAuth2DecisionPage extends LitElement {
61
61
  md-elevated-button {
62
62
  margin-right: var(--padding-narrow);
63
63
  }
64
-
65
- [danger] {
66
- --md-theme-primary: var(--md-danger-button-primary-color);
67
- }
68
64
  `
69
65
  ]
70
66
 
@@ -25,16 +25,19 @@ class AppBinding extends connect(store)(PageView) {
25
25
  background-color: var(--md-sys-color-background);
26
26
  padding: var(--padding-wide);
27
27
  }
28
+
28
29
  h2 {
29
30
  margin: var(--title-margin);
30
31
  font: var(--title-font);
31
32
  color: var(--title-text-color);
32
33
  }
34
+
33
35
  [page-description] {
34
36
  margin: var(--page-description-margin);
35
37
  font: var(--page-description-font);
36
38
  color: var(--page-description-color);
37
39
  }
40
+
38
41
  [icon] {
39
42
  position: absolute;
40
43
  top: 10px;
@@ -42,68 +45,77 @@ class AppBinding extends connect(store)(PageView) {
42
45
 
43
46
  max-width: 80px;
44
47
  }
48
+
45
49
  [icon] img {
46
50
  max-width: 100%;
47
51
  max-height: 100%;
48
52
  }
49
53
 
50
- label {
51
- font: var(--label-font);
52
- color: var(--label-color, var(--md-sys-color-on-surface));
53
- text-transform: var(--label-text-transform);
54
- }
55
- input {
56
- border: var(--border-dim-color);
57
- border-radius: var(--border-radius);
58
- margin: var(--input-margin);
59
- padding: var(--input-padding);
60
- font: var(--input-font);
61
-
62
- flex: 1;
63
- }
64
- select:focus,
65
- input:focus,
66
- button {
67
- outline: none;
68
- }
69
- form {
70
- max-width: var(--content-container-max-width);
71
- }
72
54
  [fieldset-container] {
73
55
  background-color: var(--md-sys-color-surface);
74
56
  margin: var(--margin-wide) 0 var(--margin-default) 0;
75
57
  padding: var(--padding-default);
76
58
  border-radius: var(--border-radius);
77
59
  box-shadow: var(--box-shadow);
60
+
61
+ label {
62
+ font: var(--label-font);
63
+ color: var(--label-color, var(--md-sys-color-on-surface));
64
+ text-transform: var(--label-text-transform);
65
+ }
66
+ input {
67
+ border: var(--border-dim-color);
68
+ border-radius: var(--border-radius);
69
+ margin: var(--input-margin);
70
+ padding: var(--input-padding);
71
+ font: var(--input-font);
72
+
73
+ flex: 1;
74
+ }
75
+ select:focus,
76
+ input:focus,
77
+ button {
78
+ outline: none;
79
+ }
80
+ form {
81
+ max-width: var(--content-container-max-width);
82
+ }
78
83
  }
84
+
79
85
  [fieldset-container] fieldset {
80
86
  margin: 0;
81
87
  margin-top: -15px;
82
88
  }
89
+
83
90
  fieldset {
84
91
  border-radius: var(--border-radius);
85
92
  border: var(--border-dim-color);
86
93
  margin: var(--fieldset-margin);
87
94
  padding: var(--fieldset-padding);
88
95
  }
96
+
89
97
  legend {
90
98
  padding: var(--legend-padding);
91
99
  font-weight: bold;
92
100
  color: var(--legend-color);
93
101
  }
102
+
94
103
  [field-2column] {
95
104
  display: grid;
96
105
  grid-template-columns: 1fr 1fr;
97
106
  grid-gap: 15px;
98
107
  }
108
+
99
109
  [field] {
100
110
  display: flex;
101
111
  flex-direction: column;
102
112
  position: relative;
103
113
  }
114
+
104
115
  [grid-span] {
105
116
  grid-column: span 2;
106
117
  }
118
+
107
119
  button,
108
120
  [button-in-field] {
109
121
  background-color: var(--button-background-color);
@@ -118,10 +130,12 @@ class AppBinding extends connect(store)(PageView) {
118
130
  float: right;
119
131
  text-decoration: none;
120
132
  }
133
+
121
134
  button:hover {
122
135
  border: var(--button-activ-border);
123
136
  box-shadow: var(--button-active-box-shadow);
124
137
  }
138
+
125
139
  [button-in-field] {
126
140
  border-radius: 0 var(--button-border-radius) var(--button-border-radius) 0;
127
141
  position: absolute;
@@ -129,10 +143,12 @@ class AppBinding extends connect(store)(PageView) {
129
143
  right: 0;
130
144
  max-height: 36px;
131
145
  }
146
+
132
147
  [input-hint] {
133
148
  font: var(--input-hint-font);
134
149
  color: var(--input-hint-color);
135
150
  }
151
+
136
152
  @media screen and (max-width: 480px) {
137
153
  [field] {
138
154
  grid-column: span 2;
@@ -1,3 +1,6 @@
1
+ import '@material/web/button/elevated-button.js'
2
+ import '@material/web/button/outlined-button.js'
3
+
1
4
  import gql from 'graphql-tag'
2
5
  import { css, html } from 'lit'
3
6
  import { customElement, property } from 'lit/decorators.js'
@@ -16,15 +19,23 @@ class AppBindings extends connect(store)(PageView) {
16
19
 
17
20
  overflow: auto;
18
21
  }
22
+
19
23
  md-elevated-button {
20
- margin-right: 0;
24
+ text-transform: capitalize;
25
+ }
26
+
27
+ md-outlined-button {
21
28
  float: right;
29
+ margin-top: var(--margin-default);
30
+ text-transform: capitalize;
22
31
  }
32
+
23
33
  h2 {
24
34
  margin: var(--title-margin);
25
35
  font: var(--title-font);
26
36
  color: var(--title-text-color);
27
37
  }
38
+
28
39
  [page-description] {
29
40
  margin: var(--page-description-margin);
30
41
  font: var(--page-description-font);
@@ -36,15 +47,19 @@ class AppBindings extends connect(store)(PageView) {
36
47
  margin: var(--margin-wide) 0;
37
48
  border-collapse: collapse;
38
49
  }
50
+
39
51
  tr {
40
52
  background-color: var(--tr-background-color);
41
53
  }
54
+
42
55
  tr:nth-child(odd) {
43
56
  background-color: var(--tr-background-odd-color);
44
57
  }
58
+
45
59
  tr:hover {
46
60
  background-color: var(--tr-background-hover-color);
47
61
  }
62
+
48
63
  th {
49
64
  border-top: var(--th-border-top);
50
65
  border-bottom: var(--td-border-bottom);
@@ -55,12 +70,14 @@ class AppBindings extends connect(store)(PageView) {
55
70
  text-transform: var(--th-text-transform);
56
71
  text-align: left;
57
72
  }
73
+
58
74
  td {
59
75
  padding: var(--td-padding);
60
76
  border-bottom: var(--td-border-bottom);
61
77
  font: var(--td-font);
62
78
  color: var(--td-color);
63
79
  }
80
+
64
81
  td a {
65
82
  color: var(--md-sys-color-primary);
66
83
  font: bold 16px var(--theme-font);
@@ -68,9 +85,11 @@ class AppBindings extends connect(store)(PageView) {
68
85
  display: block;
69
86
  text-decoration: none;
70
87
  }
88
+
71
89
  .text-align-center {
72
90
  text-align: center;
73
91
  }
92
+
74
93
  .text-align-right {
75
94
  text-align: right;
76
95
  }