@micha.bigler/ui-core-micha 2.33.2 → 2.34.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 (78) hide show
  1. package/WORK_ORDERS.md +2 -2
  2. package/dist/auth/ReauthModal.js +3 -1
  3. package/dist/components/AccessCodeManager.js +16 -14
  4. package/dist/components/AccessCodeSingleUseToggle.js +2 -1
  5. package/dist/components/AllowedEmailDomainsManager.js +3 -1
  6. package/dist/components/AuthFactorRequirementCard.js +2 -1
  7. package/dist/components/BulkInviteCsvTab.js +7 -7
  8. package/dist/components/LoginForm.js +2 -1
  9. package/dist/components/MFAComponent.js +11 -5
  10. package/dist/components/MfaLoginComponent.js +4 -1
  11. package/dist/components/PasskeysComponent.js +4 -2
  12. package/dist/components/QrSignupManager.js +2 -1
  13. package/dist/components/QrSignupValidityManager.js +4 -1
  14. package/dist/components/RegistrationMethodsManager.js +3 -1
  15. package/dist/components/SecurityComponent.js +3 -1
  16. package/dist/components/SupportRecoveryRequestsTab.js +4 -1
  17. package/dist/components/UserInviteComponent.js +8 -8
  18. package/dist/components/UserListComponent.js +8 -4
  19. package/dist/components/charts/ChartFrame.js +5 -2
  20. package/dist/components/charts/TimeSeriesChart.js +7 -4
  21. package/dist/index.js +1 -1
  22. package/dist/layout/PageLayout.js +3 -2
  23. package/dist/messaging/AttachmentList.js +6 -1
  24. package/dist/messaging/Composer.js +2 -1
  25. package/dist/messaging/DirectMessageLauncher.js +2 -1
  26. package/dist/messaging/MessageBubble.js +29 -8
  27. package/dist/messaging/PollCard.js +6 -4
  28. package/dist/messaging/Thread.js +2 -1
  29. package/dist/notifications/NotificationSettings.js +17 -3
  30. package/dist/notifications/PopupSurface.js +2 -1
  31. package/dist/onboarding/steps/BrowserPushStep.js +2 -1
  32. package/dist/pages/AccountPage.js +2 -1
  33. package/dist/pages/LoginPage.js +2 -1
  34. package/dist/pages/SignUpPage.js +2 -1
  35. package/dist/pages/SignupConfirmPage.js +2 -1
  36. package/dist/theme/index.js +2 -1
  37. package/dist/theme/kitSxRegistry.js +187 -0
  38. package/dist/theme/themeCompleteness.js +217 -0
  39. package/package.json +1 -1
  40. package/src/auth/ReauthModal.jsx +5 -2
  41. package/src/components/AccessCodeManager.jsx +22 -20
  42. package/src/components/AccessCodeSingleUseToggle.jsx +4 -2
  43. package/src/components/AllowedEmailDomainsManager.jsx +6 -3
  44. package/src/components/AuthFactorRequirementCard.jsx +4 -2
  45. package/src/components/BulkInviteCsvTab.jsx +11 -10
  46. package/src/components/LoginForm.jsx +5 -3
  47. package/src/components/MFAComponent.jsx +14 -7
  48. package/src/components/MfaLoginComponent.jsx +9 -5
  49. package/src/components/PasskeysComponent.jsx +7 -4
  50. package/src/components/QrSignupManager.jsx +6 -4
  51. package/src/components/QrSignupValidityManager.jsx +8 -4
  52. package/src/components/RegistrationMethodsManager.jsx +7 -4
  53. package/src/components/SecurityComponent.jsx +10 -7
  54. package/src/components/SupportRecoveryRequestsTab.jsx +8 -4
  55. package/src/components/UserInviteComponent.jsx +10 -10
  56. package/src/components/UserListComponent.jsx +12 -7
  57. package/src/components/charts/ChartFrame.jsx +6 -2
  58. package/src/components/charts/TimeSeriesChart.jsx +8 -4
  59. package/src/index.js +3 -0
  60. package/src/layout/PageLayout.jsx +4 -2
  61. package/src/messaging/AttachmentList.jsx +9 -3
  62. package/src/messaging/Composer.jsx +3 -1
  63. package/src/messaging/DirectMessageLauncher.jsx +3 -1
  64. package/src/messaging/MessageBubble.jsx +31 -9
  65. package/src/messaging/PollCard.jsx +8 -6
  66. package/src/messaging/Thread.jsx +3 -1
  67. package/src/notifications/NotificationSettings.jsx +24 -9
  68. package/src/notifications/PopupSurface.jsx +3 -1
  69. package/src/onboarding/steps/BrowserPushStep.jsx +3 -1
  70. package/src/pages/AccountPage.jsx +11 -9
  71. package/src/pages/LoginPage.jsx +6 -4
  72. package/src/pages/SignUpPage.jsx +4 -2
  73. package/src/pages/SignupConfirmPage.jsx +4 -2
  74. package/src/theme/index.js +3 -0
  75. package/src/theme/kitSxRegistry.js +246 -0
  76. package/src/theme/themeCompleteness.js +227 -0
  77. package/tests/themeCompleteness.test.js +199 -0
  78. package/work-orders/THEME-4.md +86 -0
@@ -9,6 +9,9 @@ import {
9
9
  import { useTranslation } from 'react-i18next';
10
10
  import { updateAuthPolicy } from '../auth/authApi';
11
11
 
12
+ export const ALLOWED_EMAIL_DOMAINS_ALERT_SX = { mb: 2 };
13
+ export const ALLOWED_EMAIL_DOMAINS_BUTTON_SX = { mt: 2 };
14
+
12
15
  export function AllowedEmailDomainsManager({
13
16
  domains = [],
14
17
  enabled = false,
@@ -63,8 +66,8 @@ export function AllowedEmailDomainsManager({
63
66
  )}
64
67
  </Typography>
65
68
 
66
- {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
67
- {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
69
+ {error && <Alert severity="error" sx={ALLOWED_EMAIL_DOMAINS_ALERT_SX}>{error}</Alert>}
70
+ {success && <Alert severity="success" sx={ALLOWED_EMAIL_DOMAINS_ALERT_SX}>{success}</Alert>}
68
71
 
69
72
  <TextField
70
73
  label={t('Auth.ALLOWED_EMAIL_DOMAINS_LABEL', 'Allowed email domains')}
@@ -80,7 +83,7 @@ export function AllowedEmailDomainsManager({
80
83
  disabled={busy || !canEdit}
81
84
  />
82
85
 
83
- <Button variant="contained" sx={{ mt: 2 }} onClick={handleSave} disabled={busy || !canEdit}>
86
+ <Button variant="contained" sx={ALLOWED_EMAIL_DOMAINS_BUTTON_SX} onClick={handleSave} disabled={busy || !canEdit}>
84
87
  {t('Common.SAVE', 'Save')}
85
88
  </Button>
86
89
  </Box>
@@ -11,6 +11,8 @@ import {
11
11
  import { useTranslation } from 'react-i18next';
12
12
  import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
13
13
 
14
+ export const AUTH_FACTOR_REQUIREMENT_ALERT_SX = { mb: 2 };
15
+
14
16
  export function AuthFactorRequirementCard({ canEdit = true, policy = null }) {
15
17
  const { t } = useTranslation();
16
18
  const [value, setValue] = useState('1');
@@ -71,8 +73,8 @@ export function AuthFactorRequirementCard({ canEdit = true, policy = null }) {
71
73
  'Define the minimum number of authentication factors required for sign-in.',
72
74
  )}
73
75
  </Typography>
74
- {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
75
- {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
76
+ {error && <Alert severity="error" sx={AUTH_FACTOR_REQUIREMENT_ALERT_SX}>{error}</Alert>}
77
+ {success && <Alert severity="success" sx={AUTH_FACTOR_REQUIREMENT_ALERT_SX}>{success}</Alert>}
76
78
  <FormControl>
77
79
  <RadioGroup value={value} onChange={handleChange}>
78
80
  <FormControlLabel
@@ -16,6 +16,13 @@ import {
16
16
  import { useTranslation } from 'react-i18next';
17
17
  import { sendAdminInvite } from '../auth/authApi';
18
18
 
19
+ export const BULK_INVITE_ACTION_SX = {
20
+ minWidth: 120,
21
+ height: 40,
22
+ whiteSpace: 'nowrap',
23
+ };
24
+ export const BULK_INVITE_ALERT_SX = { mb: 2 };
25
+
19
26
  function parseEmailsFromCsv(text) {
20
27
  if (!text) return [];
21
28
 
@@ -45,12 +52,6 @@ export function BulkInviteCsvTab({
45
52
  onCompleted,
46
53
  }) {
47
54
  const { t } = useTranslation();
48
- const actionButtonSx = {
49
- minWidth: 120,
50
- height: 40,
51
- textTransform: 'none',
52
- whiteSpace: 'nowrap',
53
- };
54
55
  const [emails, setEmails] = useState([]);
55
56
  const [results, setResults] = useState({});
56
57
  const [busy, setBusy] = useState(false);
@@ -134,15 +135,15 @@ export function BulkInviteCsvTab({
134
135
  )}
135
136
  </Typography>
136
137
 
137
- {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
138
- {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
138
+ {error && <Alert severity="error" sx={BULK_INVITE_ALERT_SX}>{error}</Alert>}
139
+ {success && <Alert severity="success" sx={BULK_INVITE_ALERT_SX}>{success}</Alert>}
139
140
 
140
141
  <Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mb: 2, flexWrap: 'wrap' }}>
141
- <Button variant="outlined" size="small" component="label" disabled={busy} sx={actionButtonSx}>
142
+ <Button variant="outlined" size="small" component="label" disabled={busy} sx={BULK_INVITE_ACTION_SX}>
142
143
  {t('Account.BULK_INVITE_UPLOAD', 'Upload CSV')}
143
144
  <input type="file" accept=".csv,text/csv" hidden onChange={handleFile} />
144
145
  </Button>
145
- <Button variant="contained" size="small" sx={actionButtonSx} onClick={handleInviteAll} disabled={busy || emails.length === 0}>
146
+ <Button variant="contained" size="small" sx={BULK_INVITE_ACTION_SX} onClick={handleInviteAll} disabled={busy || emails.length === 0}>
146
147
  {t('Account.BULK_INVITE_SEND', 'Send Invites')}
147
148
  </Button>
148
149
  <Typography variant="body2">
@@ -9,6 +9,8 @@ import {
9
9
  import { useTranslation } from 'react-i18next';
10
10
  import { SocialLoginButtons } from './SocialLoginButtons';
11
11
 
12
+ export const LOGIN_FORM_DIVIDER_SX = { my: 2 };
13
+
12
14
  export function LoginForm({
13
15
  onSubmit,
14
16
  onForgotPassword,
@@ -62,7 +64,7 @@ export function LoginForm({
62
64
  {t('Auth.LOGIN_USE_PASSKEY_BUTTON')}
63
65
  </Button>
64
66
 
65
- <Divider sx={{ my: 2 }}>
67
+ <Divider sx={LOGIN_FORM_DIVIDER_SX}>
66
68
  {t('Auth.LOGIN_OR')}
67
69
  </Divider>
68
70
  </>
@@ -109,7 +111,7 @@ export function LoginForm({
109
111
  {/* Other ways to sign in */}
110
112
  {onSocialLogin && (
111
113
  <Box>
112
- <Divider sx={{ my: 2 }}>
114
+ <Divider sx={LOGIN_FORM_DIVIDER_SX}>
113
115
  {t('Auth.LOGIN_OR')}
114
116
  </Divider>
115
117
  <SocialLoginButtons
@@ -121,7 +123,7 @@ export function LoginForm({
121
123
  {/* Account actions */}
122
124
  {(onSignUp || onForgotPassword) && (
123
125
  <Box>
124
- <Divider sx={{ my: 2 }}>
126
+ <Divider sx={LOGIN_FORM_DIVIDER_SX}>
125
127
  {t('Auth.LOGIN_OR')}
126
128
  </Divider>
127
129
 
@@ -20,6 +20,13 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
20
20
  import { useTranslation } from 'react-i18next';
21
21
  import { fetchAuthenticators, requestTotpKey, activateTotp, deactivateTotp, fetchRecoveryCodes, generateRecoveryCodes } from '../auth/authApi';
22
22
 
23
+ export const MFA_ALERT_SX = { mb: 2 };
24
+ export const MFA_CARD_SX = { mb: 3 };
25
+ export const MFA_ACTIVE_CARD_SX = { bgcolor: '#f0fdf4' };
26
+ export const MFA_TEXT_FIELD_SX = { mb: 2 };
27
+ export const MFA_DIVIDER_SX = { my: 3 };
28
+ export const MFA_RECOVERY_BUTTON_SX = { mt: 2 };
29
+
23
30
  export function MFAComponent() {
24
31
  const { t } = useTranslation();
25
32
 
@@ -165,14 +172,14 @@ export function MFAComponent() {
165
172
  </Typography>
166
173
 
167
174
  {errorKey && (
168
- <Alert severity="error" sx={{ mb: 2 }}>
175
+ <Alert severity="error" sx={MFA_ALERT_SX}>
169
176
  {t(errorKey)}
170
177
  </Alert>
171
178
  )}
172
179
 
173
180
  {/* --- CASE 1: TOTP IS ACTIVE --- */}
174
181
  {totpAuthenticator ? (
175
- <Card variant="outlined" sx={{ mb: 3, bgcolor: '#f0fdf4' }}>
182
+ <Card variant="outlined" sx={[MFA_CARD_SX, MFA_ACTIVE_CARD_SX]}>
176
183
  <CardContent
177
184
  sx={{
178
185
  display: 'flex',
@@ -209,7 +216,7 @@ export function MFAComponent() {
209
216
 
210
217
  {/* --- WIZARD: SETUP --- */}
211
218
  {isSettingUp && totpData && (
212
- <Card variant="outlined" sx={{ mb: 3 }}>
219
+ <Card variant="outlined" sx={MFA_CARD_SX}>
213
220
  <CardContent>
214
221
  <Typography variant="subtitle1" gutterBottom>
215
222
  {t('Auth.MFA_TOTP_QR_TITLE')}
@@ -251,7 +258,7 @@ export function MFAComponent() {
251
258
  onChange={(e) => setVerifyCode(e.target.value)}
252
259
  fullWidth
253
260
  required
254
- sx={{ mb: 2 }}
261
+ sx={MFA_TEXT_FIELD_SX}
255
262
  autoComplete="off"
256
263
  />
257
264
  <Stack direction="row" spacing={1}>
@@ -278,7 +285,7 @@ export function MFAComponent() {
278
285
  </Card>
279
286
  )}
280
287
 
281
- <Divider sx={{ my: 3 }} />
288
+ <Divider sx={MFA_DIVIDER_SX} />
282
289
 
283
290
  {/* --- RECOVERY CODES --- */}
284
291
  <Typography variant="h6" gutterBottom>
@@ -294,7 +301,7 @@ export function MFAComponent() {
294
301
  </Button>
295
302
  ) : (
296
303
  <Box sx={{ bgcolor: '#f5f5f5', p: 2, borderRadius: 1 }}>
297
- <Alert severity="warning" sx={{ mb: 2 }}>
304
+ <Alert severity="warning" sx={MFA_ALERT_SX}>
298
305
  {t('Auth.MFA_RECOVERY_WARNING')}
299
306
  </Alert>
300
307
  <Stack direction="row" flexWrap="wrap" gap={2}>
@@ -326,7 +333,7 @@ export function MFAComponent() {
326
333
  ))}
327
334
  </Stack>
328
335
  <Button
329
- sx={{ mt: 2 }}
336
+ sx={MFA_RECOVERY_BUTTON_SX}
330
337
  size="small"
331
338
  onClick={handleGenerateNewRecoveryCodes}
332
339
  >
@@ -16,6 +16,10 @@ import { useTranslation } from 'react-i18next';
16
16
  import { authenticateWithMFA, fetchCurrentUser, requestMfaSupportHelp } from '../auth/authApi';
17
17
  import { loginWithPasskey } from '../utils/authService';
18
18
 
19
+ export const MFA_LOGIN_ALERT_SX = { mb: 2 };
20
+ export const MFA_LOGIN_DIVIDER_SX = { my: 2 };
21
+ export const MFA_LOGIN_TEXT_FIELD_SX = { mb: 2 };
22
+
19
23
  export function MfaLoginComponent({ availableTypes, identifier, onSuccess, onCancel }) {
20
24
  const { t } = useTranslation();
21
25
  const [code, setCode] = useState('');
@@ -113,13 +117,13 @@ export function MfaLoginComponent({ availableTypes, identifier, onSuccess, onCan
113
117
  </Typography>
114
118
 
115
119
  {errorKey && (
116
- <Alert severity="error" sx={{ mb: 2 }}>
120
+ <Alert severity="error" sx={MFA_LOGIN_ALERT_SX}>
117
121
  {t(errorKey)}
118
122
  </Alert>
119
123
  )}
120
124
 
121
125
  {infoKey && (
122
- <Alert severity="info" sx={{ mb: 2 }}>
126
+ <Alert severity="info" sx={MFA_LOGIN_ALERT_SX}>
123
127
  {t(infoKey)}
124
128
  </Alert>
125
129
  )}
@@ -136,7 +140,7 @@ export function MfaLoginComponent({ availableTypes, identifier, onSuccess, onCan
136
140
  </Button>
137
141
  )}
138
142
 
139
- <Divider sx={{ my: 2 }}>
143
+ <Divider sx={MFA_LOGIN_DIVIDER_SX}>
140
144
  {t('Auth.LOGIN_OR')}
141
145
  </Divider>
142
146
 
@@ -152,7 +156,7 @@ export function MfaLoginComponent({ availableTypes, identifier, onSuccess, onCan
152
156
  fullWidth
153
157
  disabled={submitting || helpRequested}
154
158
  autoComplete="one-time-code"
155
- sx={{ mb: 2 }}
159
+ sx={MFA_LOGIN_TEXT_FIELD_SX}
156
160
  />
157
161
  <Button
158
162
  type="submit"
@@ -165,7 +169,7 @@ export function MfaLoginComponent({ availableTypes, identifier, onSuccess, onCan
165
169
  </Box>
166
170
  )}
167
171
 
168
- <Divider sx={{ my: 2 }}>
172
+ <Divider sx={MFA_LOGIN_DIVIDER_SX}>
169
173
  {t('Auth.LOGIN_OR')}
170
174
  </Divider>
171
175
 
@@ -22,6 +22,9 @@ import { fetchPasskeys, deletePasskey } from '../auth/authApi';
22
22
  import { registerPasskey } from '../utils/authService';
23
23
  import { FEATURES } from '../auth/authConfig';
24
24
 
25
+ export const PASSKEYS_ALERT_SX = { mb: 2 };
26
+ export const PASSKEYS_DIVIDER_SX = { mb: 2 };
27
+
25
28
  export function PasskeysComponent() {
26
29
  const { t } = useTranslation();
27
30
 
@@ -131,18 +134,18 @@ export function PasskeysComponent() {
131
134
  </Typography>
132
135
 
133
136
  {!passkeysSupported && (
134
- <Alert severity="info" sx={{ mb: 2 }}>
137
+ <Alert severity="info" sx={PASSKEYS_ALERT_SX}>
135
138
  {t('Auth.PASSKEY_BROWSER_NOT_SUPPORTED')}
136
139
  </Alert>
137
140
  )}
138
141
 
139
142
  {messageKey && (
140
- <Alert severity="success" sx={{ mb: 2 }}>
143
+ <Alert severity="success" sx={PASSKEYS_ALERT_SX}>
141
144
  {t(messageKey)}
142
145
  </Alert>
143
146
  )}
144
147
  {errorKey && (
145
- <Alert severity="error" sx={{ mb: 2 }}>
148
+ <Alert severity="error" sx={PASSKEYS_ALERT_SX}>
146
149
  {t(errorKey)}
147
150
  </Alert>
148
151
  )}
@@ -176,7 +179,7 @@ export function PasskeysComponent() {
176
179
  </Box>
177
180
  )}
178
181
 
179
- <Divider sx={{ mb: 2 }} />
182
+ <Divider sx={PASSKEYS_DIVIDER_SX} />
180
183
 
181
184
  {/* List of existing passkeys */}
182
185
  {loading ? (
@@ -11,6 +11,8 @@ import { useTranslation } from 'react-i18next';
11
11
  import { QRCodeSVG } from 'qrcode.react';
12
12
  import { createSignupQr } from '../auth/authApi';
13
13
 
14
+ export const QR_SIGNUP_MANAGER_ALERT_SX = { mb: 2 };
15
+
14
16
  const DEFAULT_EXPIRY_DAYS = 90;
15
17
  const DEFAULT_MAX_REDEMPTIONS = 1;
16
18
 
@@ -297,15 +299,15 @@ export function QrSignupManager({
297
299
  />
298
300
  </Box>
299
301
 
300
- {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
301
- {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
302
+ {error && <Alert severity="error" sx={QR_SIGNUP_MANAGER_ALERT_SX}>{error}</Alert>}
303
+ {success && <Alert severity="success" sx={QR_SIGNUP_MANAGER_ALERT_SX}>{success}</Alert>}
302
304
  {copyState === 'copied' && (
303
- <Alert severity="success" sx={{ mb: 2 }}>
305
+ <Alert severity="success" sx={QR_SIGNUP_MANAGER_ALERT_SX}>
304
306
  {t('Auth.SIGNUP_QR_LINK_COPIED', 'Signup link copied.')}
305
307
  </Alert>
306
308
  )}
307
309
  {copyState === 'error' && (
308
- <Alert severity="warning" sx={{ mb: 2 }}>
310
+ <Alert severity="warning" sx={QR_SIGNUP_MANAGER_ALERT_SX}>
309
311
  {t('Auth.SIGNUP_QR_COPY_UNAVAILABLE', 'Copying the link is not available in this browser.')}
310
312
  </Alert>
311
313
  )}
@@ -10,6 +10,10 @@ import {
10
10
  import { useTranslation } from 'react-i18next';
11
11
  import { updateAuthPolicy } from '../auth/authApi';
12
12
 
13
+ export const QR_SIGNUP_VALIDITY_ALERT_SX = { mb: 2 };
14
+ export const QR_SIGNUP_VALIDITY_FIELD_SX = { flex: 1 };
15
+ export const QR_SIGNUP_VALIDITY_BUTTON_SX = { minWidth: 120, mt: { sm: '8px' } };
16
+
13
17
  const DEFAULT_EXPIRY_DAYS = 90;
14
18
 
15
19
  function clampExpiryDays(value) {
@@ -71,8 +75,8 @@ export function QrSignupValidityManager({
71
75
  )}
72
76
  </Typography>
73
77
 
74
- {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
75
- {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
78
+ {error && <Alert severity="error" sx={QR_SIGNUP_VALIDITY_ALERT_SX}>{error}</Alert>}
79
+ {success && <Alert severity="success" sx={QR_SIGNUP_VALIDITY_ALERT_SX}>{success}</Alert>}
76
80
 
77
81
  <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems={{ sm: 'flex-start' }}>
78
82
  <TextField
@@ -85,13 +89,13 @@ export function QrSignupValidityManager({
85
89
  value={currentExpiryDays}
86
90
  onChange={(event) => setCurrentExpiryDays(event.target.value)}
87
91
  disabled={busy || !canEdit}
88
- sx={{ flex: 1 }}
92
+ sx={QR_SIGNUP_VALIDITY_FIELD_SX}
89
93
  />
90
94
  <Button
91
95
  variant="contained"
92
96
  onClick={handleSave}
93
97
  disabled={busy || !canEdit}
94
- sx={{ minWidth: 120, mt: { sm: '8px' } }}
98
+ sx={QR_SIGNUP_VALIDITY_BUTTON_SX}
95
99
  >
96
100
  {t('Common.SAVE', 'Save')}
97
101
  </Button>
@@ -11,6 +11,9 @@ import {
11
11
  import { useTranslation } from 'react-i18next';
12
12
  import { updateAuthPolicy } from '../auth/authApi';
13
13
 
14
+ export const REGISTRATION_METHODS_ALERT_SX = { mb: 2 };
15
+ export const REGISTRATION_METHODS_INFO_SX = { mt: 2 };
16
+
14
17
  const EMPTY_POLICY = {
15
18
  allow_admin_invite: true,
16
19
  allow_self_signup_access_code: false,
@@ -76,9 +79,9 @@ export function RegistrationMethodsManager({
76
79
  <Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
77
80
  {t('Auth.REGISTRATION_METHODS_HINT', 'Choose which signup and invite flows are active for this app.')}
78
81
  </Typography>
79
- {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
80
- {saveError && <Alert severity="error" sx={{ mb: 2 }}>{saveError}</Alert>}
81
- {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
82
+ {error && <Alert severity="error" sx={REGISTRATION_METHODS_ALERT_SX}>{error}</Alert>}
83
+ {saveError && <Alert severity="error" sx={REGISTRATION_METHODS_ALERT_SX}>{saveError}</Alert>}
84
+ {success && <Alert severity="success" sx={REGISTRATION_METHODS_ALERT_SX}>{success}</Alert>}
82
85
 
83
86
  <Stack spacing={1}>
84
87
  <FormControlLabel
@@ -134,7 +137,7 @@ export function RegistrationMethodsManager({
134
137
  </Stack>
135
138
 
136
139
  {policyState.allow_self_signup_email_domain && !(policyState.allowed_email_domains || []).length && (
137
- <Alert severity="info" sx={{ mt: 2 }}>
140
+ <Alert severity="info" sx={REGISTRATION_METHODS_INFO_SX}>
138
141
  {t(
139
142
  'Auth.EMAIL_DOMAIN_CURRENTLY_BLOCKED_HINT',
140
143
  'Email-domain signup is enabled, but it stays blocked until at least one allowed domain is saved.',
@@ -15,6 +15,9 @@ import { changePassword } from '../auth/authApi';
15
15
  import { startSocialLogin } from '../utils/authService';
16
16
  import { AuthContext } from '../auth/AuthContext';
17
17
 
18
+ export const SECURITY_ALERT_SX = { mb: 2 };
19
+ export const SECURITY_DIVIDER_SX = { my: 3 };
20
+
18
21
  export function SecurityComponent({
19
22
  fromRecovery = false,
20
23
  fromWeakLogin = false,
@@ -75,24 +78,24 @@ export function SecurityComponent({
75
78
  return (
76
79
  <Box>
77
80
  {fromRecovery && (
78
- <Alert severity="warning" sx={{ mb: 2 }}>
81
+ <Alert severity="warning" sx={SECURITY_ALERT_SX}>
79
82
  {t('Security.RECOVERY_LOGIN_WARNING')}
80
83
  </Alert>
81
84
  )}
82
85
 
83
86
  {fromWeakLogin && (
84
- <Alert severity="warning" sx={{ mb: 2 }}>
87
+ <Alert severity="warning" sx={SECURITY_ALERT_SX}>
85
88
  {t('Security.WEAK_LOGIN_WARNING')}
86
89
  </Alert>
87
90
  )}
88
91
 
89
92
  {messageKey && (
90
- <Alert severity="success" sx={{ mb: 2 }}>
93
+ <Alert severity="success" sx={SECURITY_ALERT_SX}>
91
94
  {t(messageKey)}
92
95
  </Alert>
93
96
  )}
94
97
  {errorKey && (
95
- <Alert severity="error" sx={{ mb: 2 }}>
98
+ <Alert severity="error" sx={SECURITY_ALERT_SX}>
96
99
  {t(errorKey)}
97
100
  </Alert>
98
101
  )}
@@ -103,7 +106,7 @@ export function SecurityComponent({
103
106
  {t('Auth.LOGIN_PASSWORD_LABEL')}
104
107
  </Typography>
105
108
  <PasswordChangeForm onSubmit={handlePasswordChange} />
106
- {needsDividerAfter('password') && <Divider sx={{ my: 3 }} />}
109
+ {needsDividerAfter('password') && <Divider sx={SECURITY_DIVIDER_SX} />}
107
110
  </>
108
111
  )}
109
112
 
@@ -119,14 +122,14 @@ export function SecurityComponent({
119
122
  onProviderClick={handleSocialClick}
120
123
  providers={socialProviders}
121
124
  />
122
- {needsDividerAfter('social') && <Divider sx={{ my: 3 }} />}
125
+ {needsDividerAfter('social') && <Divider sx={SECURITY_DIVIDER_SX} />}
123
126
  </>
124
127
  )}
125
128
 
126
129
  {passkeysEnabled && (
127
130
  <>
128
131
  <PasskeysComponent />
129
- {needsDividerAfter('passkeys') && <Divider sx={{ my: 3 }} />}
132
+ {needsDividerAfter('passkeys') && <Divider sx={SECURITY_DIVIDER_SX} />}
130
133
  </>
131
134
  )}
132
135
 
@@ -33,6 +33,10 @@ import {
33
33
  updateUserSupportStatus,
34
34
  } from '../auth/authApi';
35
35
 
36
+ export const SUPPORT_RECOVERY_AGENT_PAPER_SX = { p: 2, mb: 3 };
37
+ export const SUPPORT_RECOVERY_ALERT_SX = { mb: 2 };
38
+ export const SUPPORT_RECOVERY_AGENT_BUTTON_SX = { minWidth: 160 };
39
+
36
40
  export function SupportRecoveryRequestsTab() {
37
41
  const { t, i18n } = useTranslation();
38
42
  const { user } = useContext(AuthContext);
@@ -198,7 +202,7 @@ export function SupportRecoveryRequestsTab() {
198
202
  return (
199
203
  <Box>
200
204
  {canManageAgents && (
201
- <Paper variant="outlined" sx={{ p: 2, mb: 3, borderRadius: 2 }}>
205
+ <Paper variant="outlined" sx={SUPPORT_RECOVERY_AGENT_PAPER_SX}>
202
206
  <Typography variant="h6" gutterBottom>
203
207
  {t('Support.AGENTS_TITLE', 'Support agents')}
204
208
  </Typography>
@@ -211,7 +215,7 @@ export function SupportRecoveryRequestsTab() {
211
215
  </Typography>
212
216
 
213
217
  {agentErrorKey && (
214
- <Alert severity="error" sx={{ mb: 2 }}>
218
+ <Alert severity="error" sx={SUPPORT_RECOVERY_ALERT_SX}>
215
219
  {t(agentErrorKey)}
216
220
  </Alert>
217
221
  )}
@@ -242,7 +246,7 @@ export function SupportRecoveryRequestsTab() {
242
246
  variant="contained"
243
247
  onClick={handleAddAgent}
244
248
  disabled={!selectedAgentCandidate || agentLoading || Boolean(agentActionUserId)}
245
- sx={{ minWidth: 160 }}
249
+ sx={SUPPORT_RECOVERY_AGENT_BUTTON_SX}
246
250
  >
247
251
  {t('Support.AGENTS_ADD', 'Add as agent')}
248
252
  </Button>
@@ -305,7 +309,7 @@ export function SupportRecoveryRequestsTab() {
305
309
  </Typography>
306
310
 
307
311
  {errorKey && (
308
- <Alert severity="error" sx={{ mb: 2 }}>
312
+ <Alert severity="error" sx={SUPPORT_RECOVERY_ALERT_SX}>
309
313
  {t(errorKey)}
310
314
  </Alert>
311
315
  )}
@@ -3,19 +3,19 @@ import { Box, TextField, Button, Typography, Alert, CircularProgress } from '@mu
3
3
  import { sendAdminInvite } from '../auth/authApi';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
+ export const USER_INVITE_ACTION_SX = {
7
+ minWidth: 120,
8
+ height: 40,
9
+ whiteSpace: 'nowrap',
10
+ };
11
+ export const USER_INVITE_ALERT_SX = { mb: 2 };
12
+
6
13
  export function UserInviteComponent() { // FIX: Removed apiUrl prop
7
14
  const { t } = useTranslation();
8
15
  const [inviteEmail, setInviteEmail] = useState('');
9
16
  const [message, setMessage] = useState('');
10
17
  const [error, setError] = useState('');
11
18
  const [loading, setLoading] = useState(false);
12
- const actionButtonSx = {
13
- minWidth: 120,
14
- height: 40,
15
- textTransform: 'none',
16
- whiteSpace: 'nowrap',
17
- };
18
-
19
19
  const inviteUser = async () => {
20
20
  setMessage('');
21
21
  setError('');
@@ -43,8 +43,8 @@ export function UserInviteComponent() { // FIX: Removed apiUrl prop
43
43
  {t('Auth.INVITE_TITLE', 'Invite a new user')}
44
44
  </Typography>
45
45
 
46
- {message && <Alert severity="success" sx={{ mb: 2 }}>{message}</Alert>}
47
- {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
46
+ {message && <Alert severity="success" sx={USER_INVITE_ALERT_SX}>{message}</Alert>}
47
+ {error && <Alert severity="error" sx={USER_INVITE_ALERT_SX}>{error}</Alert>}
48
48
 
49
49
  <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
50
50
  <TextField
@@ -65,7 +65,7 @@ export function UserInviteComponent() { // FIX: Removed apiUrl prop
65
65
  size="small"
66
66
  onClick={inviteUser}
67
67
  disabled={loading || !inviteEmail}
68
- sx={actionButtonSx}
68
+ sx={USER_INVITE_ACTION_SX}
69
69
  >
70
70
  {loading ? <CircularProgress size={24} color="inherit" /> : t('Auth.INVITE_BUTTON', 'Invite')}
71
71
  </Button>
@@ -27,6 +27,12 @@ import { fetchUsersList, deleteUser, updateUserRole } from '../auth/authApi';
27
27
 
28
28
  const DEFAULT_ROLES = ['none', 'student', 'teacher', 'admin'];
29
29
 
30
+ export const USER_LIST_ACTION_SX = { minWidth: 90 };
31
+ export const USER_LIST_ALERT_SX = { mb: 2 };
32
+ export const USER_LIST_HEADER_CELL_SX = { fontWeight: 600, whiteSpace: 'nowrap' };
33
+ export const USER_LIST_EMPTY_CELL_SX = { py: 4, color: 'text.secondary' };
34
+ export const USER_LIST_BODY_CELL_SX = { verticalAlign: 'top', py: 1 };
35
+
30
36
  export function UserListComponent({
31
37
  roles = DEFAULT_ROLES,
32
38
  currentUser,
@@ -54,7 +60,6 @@ export function UserListComponent({
54
60
  const [page, setPage] = useState(0);
55
61
  const [pageSize, setPageSize] = useState(25);
56
62
  const controlSx = { minWidth: 140 };
57
- const actionButtonSx = { textTransform: 'none', minWidth: 90 };
58
63
 
59
64
  const loadUsers = useCallback(async () => {
60
65
  setLoading(true);
@@ -419,7 +424,7 @@ export function UserListComponent({
419
424
  variant="outlined"
420
425
  onClick={() => runRowAction(action, row)}
421
426
  disabled={isBusy || isDisabled}
422
- sx={actionButtonSx}
427
+ sx={USER_LIST_ACTION_SX}
423
428
  >
424
429
  {typeof action.label === 'function'
425
430
  ? action.label({ user: row, t, currentUser, canEdit: canEdit(row) })
@@ -438,7 +443,7 @@ export function UserListComponent({
438
443
  startIcon={<DeleteIcon />}
439
444
  onClick={() => handleDelete(row.id)}
440
445
  disabled={!canDelete(row)}
441
- sx={actionButtonSx}
446
+ sx={USER_LIST_ACTION_SX}
442
447
  >
443
448
  {t('Common.DELETE', 'Delete')}
444
449
  </Button>
@@ -508,7 +513,7 @@ export function UserListComponent({
508
513
  />
509
514
  </Box>
510
515
 
511
- {error && <Alert severity="error" sx={{ mb: 2 }}>{t(error)}</Alert>}
516
+ {error && <Alert severity="error" sx={USER_LIST_ALERT_SX}>{t(error)}</Alert>}
512
517
 
513
518
  {loading && (
514
519
  <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
@@ -527,7 +532,7 @@ export function UserListComponent({
527
532
  key={col.field}
528
533
  align={col.headerAlign || col.align || 'left'}
529
534
  sortDirection={sortField === col.field ? sortDir : false}
530
- sx={{ minWidth: col.minWidth, fontWeight: 600, whiteSpace: 'nowrap' }}
535
+ sx={[USER_LIST_HEADER_CELL_SX, { minWidth: col.minWidth }]}
531
536
  >
532
537
  {col.sortable !== false ? (
533
538
  <TableSortLabel
@@ -550,7 +555,7 @@ export function UserListComponent({
550
555
  <TableCell
551
556
  colSpan={columns.length}
552
557
  align="center"
553
- sx={{ py: 4, color: 'text.secondary' }}
558
+ sx={USER_LIST_EMPTY_CELL_SX}
554
559
  >
555
560
  {t('UserList.NO_USERS', 'No users found.')}
556
561
  </TableCell>
@@ -562,7 +567,7 @@ export function UserListComponent({
562
567
  <TableCell
563
568
  key={col.field}
564
569
  align={col.align || 'left'}
565
- sx={{ verticalAlign: 'top', py: 1 }}
570
+ sx={USER_LIST_BODY_CELL_SX}
566
571
  >
567
572
  {col.renderCell(row)}
568
573
  </TableCell>