@micha.bigler/ui-core-micha 2.34.0 → 2.36.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.
- package/WORK_ORDERS.md +17 -3
- package/dev/entries.jsx +79 -0
- package/dev/main.jsx +7 -1
- package/dist/components/MFAComponent.js +7 -6
- package/dist/components/SocialLoginButtons.js +2 -1
- package/dist/components/charts/ChartFrame.js +3 -1
- package/dist/components/charts/exportChart.js +2 -2
- package/dist/i18n/authTranslations.js +24 -0
- package/dist/i18n/sectionNavTranslations.js +20 -0
- package/dist/index.js +3 -1
- package/dist/layout/SectionNav.js +138 -0
- package/dist/pages/AccountPage.js +44 -2
- package/dist/theme/index.js +1 -1
- package/dist/theme/kitSxRegistry.js +8 -1
- package/dist/theme/themeCompleteness.js +218 -14
- package/package.json +1 -1
- package/src/components/MFAComponent.jsx +7 -6
- package/src/components/SocialLoginButtons.jsx +2 -1
- package/src/components/charts/ChartFrame.jsx +7 -1
- package/src/components/charts/exportChart.js +2 -2
- package/src/i18n/authTranslations.ts +24 -0
- package/src/i18n/sectionNavTranslations.ts +20 -0
- package/src/index.js +3 -0
- package/src/layout/SectionNav.jsx +299 -0
- package/src/pages/AccountPage.jsx +55 -14
- package/src/theme/index.js +1 -0
- package/src/theme/kitSxRegistry.js +11 -1
- package/src/theme/themeCompleteness.js +237 -14
- package/tests/AccountPage.test.jsx +193 -0
- package/tests/ChartFrame.test.jsx +5 -0
- package/tests/SectionNav.test.jsx +274 -0
- package/tests/themeCompleteness.test.js +156 -4
- package/work-orders/AUTH-4.md +183 -0
- package/work-orders/I18N-1.md +254 -0
- package/work-orders/SHELL-5.md +345 -0
- package/work-orders/THEME-5.md +341 -0
- package/work-orders/assets/AUTH-4-user-list-card.html +407 -0
|
@@ -2,13 +2,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import { Helmet } from 'react-helmet';
|
|
4
4
|
import { useSearchParams } from 'react-router-dom';
|
|
5
|
-
import {
|
|
5
|
+
import { Box, Typography, Alert, CircularProgress, Paper, Stack, useMediaQuery, } from '@mui/material';
|
|
6
|
+
import { useTheme } from '@mui/material/styles';
|
|
6
7
|
import { useTranslation } from 'react-i18next';
|
|
7
8
|
// Internal context
|
|
8
9
|
import { AuthContext } from '../auth/AuthContext';
|
|
9
10
|
export const ACCOUNT_PAGE_SECTION_PAPER_SX = { p: 2.5 };
|
|
10
11
|
// Falls die Komponenten noch lokal sind:
|
|
11
12
|
import { WidePage } from '../layout/PageLayout';
|
|
13
|
+
import { SectionNav } from '../layout/SectionNav';
|
|
12
14
|
import { ProfileComponent } from '../components/ProfileComponent';
|
|
13
15
|
import { SecurityComponent } from '../components/SecurityComponent';
|
|
14
16
|
import { UserListComponent } from '../components/UserListComponent';
|
|
@@ -30,6 +32,8 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
30
32
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
31
33
|
const [authPolicy, setAuthPolicy] = useState(null);
|
|
32
34
|
const [authPolicyError, setAuthPolicyError] = useState('');
|
|
35
|
+
const theme = useTheme();
|
|
36
|
+
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
|
33
37
|
// 1. URL State Management
|
|
34
38
|
const currentTabRaw = searchParams.get('tab') || 'profile';
|
|
35
39
|
const currentTab = ['invite', 'bulk-invite-csv', 'access'].includes(currentTabRaw)
|
|
@@ -116,6 +120,44 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
116
120
|
});
|
|
117
121
|
return list;
|
|
118
122
|
}, [user, perms, t, isSuperUser, extraTabs, canViewUsers, canViewInvite]);
|
|
123
|
+
const groups = useMemo(() => {
|
|
124
|
+
const groupDefinitions = [
|
|
125
|
+
{
|
|
126
|
+
key: 'my-account',
|
|
127
|
+
label: t('Account.GROUP_MY_ACCOUNT', 'My account'),
|
|
128
|
+
values: new Set(['profile', 'security']),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
key: 'management',
|
|
132
|
+
label: t('Account.GROUP_MANAGEMENT', 'Management'),
|
|
133
|
+
values: new Set(['users', 'invite']),
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
key: 'help',
|
|
137
|
+
label: t('Account.GROUP_HELP', 'Help'),
|
|
138
|
+
values: new Set(['support']),
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
const assignedValues = new Set(groupDefinitions.flatMap((group) => [...group.values]));
|
|
142
|
+
const mappedGroups = groupDefinitions.map((group) => ({
|
|
143
|
+
key: group.key,
|
|
144
|
+
label: group.label,
|
|
145
|
+
items: tabs
|
|
146
|
+
.filter((tab) => group.values.has(tab.value))
|
|
147
|
+
.map((tab) => ({ key: tab.value, label: tab.label })),
|
|
148
|
+
}));
|
|
149
|
+
const extraItems = tabs
|
|
150
|
+
.filter((tab) => !assignedValues.has(tab.value))
|
|
151
|
+
.map((tab) => ({ key: tab.value, label: tab.label }));
|
|
152
|
+
if (extraItems.length > 0) {
|
|
153
|
+
mappedGroups.push({
|
|
154
|
+
key: 'more',
|
|
155
|
+
label: t('Account.GROUP_MORE', 'More'),
|
|
156
|
+
items: extraItems,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return mappedGroups.filter((group) => group.items.length > 0);
|
|
160
|
+
}, [tabs, t]);
|
|
119
161
|
// 4. Loading & Auth Checks
|
|
120
162
|
if (loading) {
|
|
121
163
|
return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', mt: 10 }, children: _jsx(CircularProgress, {}) }));
|
|
@@ -131,5 +173,5 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
131
173
|
const activeExtraTab = builtInTabValues.has(safeTab)
|
|
132
174
|
? null
|
|
133
175
|
: extraTabs.find((tab) => tab.value === safeTab);
|
|
134
|
-
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }),
|
|
176
|
+
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsxs(SectionNav, { mode: isMobile ? 'mobile' : 'desktop', groups: groups, activeKey: safeTab, onSelect: (key) => handleTabChange(null, key), children: [safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser, showNewColumn: userListShowNewColumn, showSuccessfulLoginColumn: userListShowSuccessfulLoginColumn, showRoleColumn: userListShowRoleColumn, onChangeRole: userListOnChangeRole, showDeleteAction: userListShowDeleteAction, canDeleteUser: userListCanDeleteUser, onDeleteUser: userListOnDeleteUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AuthFactorRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canSendInvites && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(UserInviteComponent, {}) })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsxs(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AccessCodeSingleUseToggle, { canEdit: canWriteAuthPolicy, policy: authPolicy, onPolicyChange: setAuthPolicy }) })), canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AllowedEmailDomainsManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain), domains: (authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allowed_email_domains) || [], onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupValidityManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canManageSignupQr && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] })] }));
|
|
135
177
|
}
|
package/dist/theme/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createAppTheme } from './createAppTheme';
|
|
2
|
-
export { assertThemeComplete, assertKitSxDisjoint, calculateContrastRatio, reportKitSxBypasses, reportThemeAdoption, THEME_COMPLETENESS_SURFACES, } from './themeCompleteness';
|
|
2
|
+
export { assertThemeComplete, assertKitSxDisjoint, calculateContrastRatio, reportOffPaletteColours, reportKitSxBypasses, reportThemeAdoption, THEME_COMPLETENESS_SURFACES, } from './themeCompleteness';
|
|
3
3
|
export { KIT_COMPONENT_SX_REGISTRY } from './kitSxRegistry';
|
|
4
4
|
export { formatShortTime, formatShortDate, formatShortMonth, formatShortYear, } from '../components/charts/chartLabels';
|
|
5
5
|
export { yearTickInterval } from '../components/charts/yearTickInterval';
|
|
@@ -17,6 +17,7 @@ import { USER_INVITE_ACTION_SX, USER_INVITE_ALERT_SX } from '../components/UserI
|
|
|
17
17
|
import { USER_LIST_ACTION_SX, USER_LIST_ALERT_SX, USER_LIST_BODY_CELL_SX, USER_LIST_EMPTY_CELL_SX, USER_LIST_HEADER_CELL_SX, } from '../components/UserListComponent';
|
|
18
18
|
import { CHART_FRAME_ALERT_SX, CHART_FRAME_ROOT_SX } from '../components/charts/ChartFrame';
|
|
19
19
|
import { PAGE_LAYOUT_CONTAINER_SX } from '../layout/PageLayout';
|
|
20
|
+
import { SECTION_NAV_DRAWER_PAPER_SX, SECTION_NAV_GROUP_PAPER_SX, } from '../layout/SectionNav';
|
|
20
21
|
import { ATTACHMENT_LIST_ALERT_SX, ATTACHMENT_LIST_LIGHTBOX_ACTION_SX, } from '../messaging/AttachmentList';
|
|
21
22
|
import { COMPOSER_EMOJI_BUTTON_SX } from '../messaging/Composer';
|
|
22
23
|
import { DIRECT_MESSAGE_LAUNCHER_ALERT_SX } from '../messaging/DirectMessageLauncher';
|
|
@@ -88,6 +89,8 @@ const SX_EXPORTS = {
|
|
|
88
89
|
REGISTRATION_METHODS_INFO_SX,
|
|
89
90
|
SECURITY_ALERT_SX,
|
|
90
91
|
SECURITY_DIVIDER_SX,
|
|
92
|
+
SECTION_NAV_DRAWER_PAPER_SX,
|
|
93
|
+
SECTION_NAV_GROUP_PAPER_SX,
|
|
91
94
|
SIGNUP_CONFIRM_PAGE_ALERT_SX,
|
|
92
95
|
SIGN_UP_PAGE_ALERT_SX,
|
|
93
96
|
SUPPORT_RECOVERY_AGENT_BUTTON_SX,
|
|
@@ -103,10 +106,11 @@ const SX_EXPORTS = {
|
|
|
103
106
|
USER_LIST_HEADER_CELL_SX,
|
|
104
107
|
};
|
|
105
108
|
const SX_EXPORT_NAMES = new Map(Object.entries(SX_EXPORTS).map(([name, sx]) => [sx, name]));
|
|
106
|
-
const entry = (component, muiComponent, sx) => ({
|
|
109
|
+
const entry = (component, muiComponent, sx, slots) => ({
|
|
107
110
|
component,
|
|
108
111
|
muiComponent,
|
|
109
112
|
sx,
|
|
113
|
+
slots,
|
|
110
114
|
exportName: SX_EXPORT_NAMES.get(sx),
|
|
111
115
|
});
|
|
112
116
|
/**
|
|
@@ -160,6 +164,9 @@ export const KIT_COMPONENT_SX_REGISTRY = [
|
|
|
160
164
|
entry('ChartFrame.root', 'MuiPaper', CHART_FRAME_ROOT_SX),
|
|
161
165
|
entry('ChartFrame.alert', 'MuiAlert', CHART_FRAME_ALERT_SX),
|
|
162
166
|
entry('PageLayout.container', 'MuiContainer', PAGE_LAYOUT_CONTAINER_SX),
|
|
167
|
+
entry('SectionNav.drawerPaper', 'MuiDrawer', SECTION_NAV_DRAWER_PAPER_SX, ['paper']),
|
|
168
|
+
entry('SectionNav.drawerPaperBase', 'MuiPaper', SECTION_NAV_DRAWER_PAPER_SX),
|
|
169
|
+
entry('SectionNav.groupPaper', 'MuiPaper', SECTION_NAV_GROUP_PAPER_SX),
|
|
163
170
|
entry('AttachmentList.lightboxAction', 'MuiIconButton', ATTACHMENT_LIST_LIGHTBOX_ACTION_SX),
|
|
164
171
|
entry('AttachmentList.alert', 'MuiAlert', ATTACHMENT_LIST_ALERT_SX),
|
|
165
172
|
entry('Composer.emojiAction', 'MuiIconButton', COMPOSER_EMOJI_BUTTON_SX),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createTheme, getContrastRatio as getMuiContrastRatio } from '@mui/material/styles';
|
|
2
|
-
import { STATUS_KEYS, TYPOGRAPHY_VARIANTS } from './tokens';
|
|
2
|
+
import { BASELINE_PALETTE, BASELINE_STATIC, STATUS_KEYS, TYPOGRAPHY_VARIANTS, } from './tokens';
|
|
3
3
|
import { KIT_COMPONENT_SX_REGISTRY } from './kitSxRegistry';
|
|
4
4
|
const MUI_DEFAULT_THEME = createTheme();
|
|
5
5
|
function getPath(value, path) {
|
|
@@ -342,18 +342,28 @@ const SAME_ROOT_STYLE_OVERRIDE_SLOTS = {
|
|
|
342
342
|
MuiButton: ['root', 'contained', 'outlined'],
|
|
343
343
|
MuiChip: ['root', 'outlined'],
|
|
344
344
|
};
|
|
345
|
-
// MUI's sx prop accepts CSS spacing shorthands
|
|
346
|
-
// styleOverrides' plain CSS-in-JS objects do not
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
// expanded to the canonical longhand properties it can affect so the
|
|
350
|
-
// disjointness check compares axes, not raw key spellings; a property
|
|
345
|
+
// MUI's sx prop accepts aliases and CSS spacing shorthands that
|
|
346
|
+
// styleOverrides' plain CSS-in-JS objects do not. Each known alias is
|
|
347
|
+
// expanded to the canonical CSS properties it can affect so the
|
|
348
|
+
// disjointness check compares effects, not raw key spellings; a property
|
|
351
349
|
// outside this table expands to itself.
|
|
352
350
|
const SPACING_SHORTHAND_LONGHANDS = {
|
|
351
|
+
bgcolor: ['backgroundColor'],
|
|
352
|
+
typography: [
|
|
353
|
+
'fontFamily',
|
|
354
|
+
'fontSize',
|
|
355
|
+
'fontStyle',
|
|
356
|
+
'fontWeight',
|
|
357
|
+
'letterSpacing',
|
|
358
|
+
'lineHeight',
|
|
359
|
+
'textTransform',
|
|
360
|
+
],
|
|
353
361
|
p: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
|
|
354
362
|
padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
|
|
355
363
|
px: ['paddingLeft', 'paddingRight'],
|
|
364
|
+
paddingX: ['paddingLeft', 'paddingRight'],
|
|
356
365
|
py: ['paddingTop', 'paddingBottom'],
|
|
366
|
+
paddingY: ['paddingTop', 'paddingBottom'],
|
|
357
367
|
pt: ['paddingTop'],
|
|
358
368
|
pr: ['paddingRight'],
|
|
359
369
|
pb: ['paddingBottom'],
|
|
@@ -361,7 +371,9 @@ const SPACING_SHORTHAND_LONGHANDS = {
|
|
|
361
371
|
m: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
|
|
362
372
|
margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
|
|
363
373
|
mx: ['marginLeft', 'marginRight'],
|
|
374
|
+
marginX: ['marginLeft', 'marginRight'],
|
|
364
375
|
my: ['marginTop', 'marginBottom'],
|
|
376
|
+
marginY: ['marginTop', 'marginBottom'],
|
|
365
377
|
mt: ['marginTop'],
|
|
366
378
|
mr: ['marginRight'],
|
|
367
379
|
mb: ['marginBottom'],
|
|
@@ -383,14 +395,12 @@ function styleOverrideKeys(componentConfig, slots = ['root']) {
|
|
|
383
395
|
}
|
|
384
396
|
/**
|
|
385
397
|
* Reports top-level property collisions between registered kit sx objects and
|
|
386
|
-
* the baseline MUI component they target.
|
|
387
|
-
*
|
|
388
|
-
* see `SPACING_SHORTHAND_LONGHANDS`.
|
|
398
|
+
* the baseline MUI component they target. Known sx aliases are compared by
|
|
399
|
+
* the CSS properties they affect, not by literal key name.
|
|
389
400
|
*
|
|
390
|
-
* This is deliberately a lower bound on shadowing:
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
* disjoint; it is not proof that no component can shadow the baseline.
|
|
401
|
+
* This is deliberately a lower bound on shadowing: unknown normalisation gaps
|
|
402
|
+
* remain. A clean result proves only that the registered top-level key sets
|
|
403
|
+
* are disjoint; it is not proof that no component can shadow the baseline.
|
|
394
404
|
*/
|
|
395
405
|
export function assertKitSxDisjoint(theme, { registry = KIT_COMPONENT_SX_REGISTRY, exemptions = [] } = {}) {
|
|
396
406
|
var _a, _b, _c;
|
|
@@ -553,6 +563,200 @@ export function reportKitSxBypasses(sources = [], { registry } = {}) {
|
|
|
553
563
|
}
|
|
554
564
|
return { findings };
|
|
555
565
|
}
|
|
566
|
+
const BASIC_CSS_COLOUR_NAMES = new Set([
|
|
567
|
+
'aqua',
|
|
568
|
+
'black',
|
|
569
|
+
'blue',
|
|
570
|
+
'fuchsia',
|
|
571
|
+
'gray',
|
|
572
|
+
'green',
|
|
573
|
+
'grey',
|
|
574
|
+
'lime',
|
|
575
|
+
'maroon',
|
|
576
|
+
'navy',
|
|
577
|
+
'olive',
|
|
578
|
+
'purple',
|
|
579
|
+
'red',
|
|
580
|
+
'silver',
|
|
581
|
+
'teal',
|
|
582
|
+
'white',
|
|
583
|
+
'yellow',
|
|
584
|
+
]);
|
|
585
|
+
const MUI_DEFAULT_NUMERIC_RAMP_NAMES = new Set([
|
|
586
|
+
'amber',
|
|
587
|
+
'blue',
|
|
588
|
+
'bluegrey',
|
|
589
|
+
'brown',
|
|
590
|
+
'cyan',
|
|
591
|
+
'deeporange',
|
|
592
|
+
'deeppurple',
|
|
593
|
+
'green',
|
|
594
|
+
'grey',
|
|
595
|
+
'indigo',
|
|
596
|
+
'lightblue',
|
|
597
|
+
'lightgreen',
|
|
598
|
+
'lime',
|
|
599
|
+
'orange',
|
|
600
|
+
'pink',
|
|
601
|
+
'purple',
|
|
602
|
+
'red',
|
|
603
|
+
'teal',
|
|
604
|
+
'yellow',
|
|
605
|
+
]);
|
|
606
|
+
const HEX_COLOUR_PATTERN = /#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})(?![\da-f])/gi;
|
|
607
|
+
const RGB_COLOUR_PATTERN = /rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+(?:\s*,\s*[\d.]+)?\s*\)/gi;
|
|
608
|
+
const NAMED_COLOUR_PATTERN = new RegExp(`\\b(?:${[...BASIC_CSS_COLOUR_NAMES].join('|')})\\b`, 'gi');
|
|
609
|
+
const COLOUR_PROPERTY_PATTERN = /\b(?:background(?:Color)?|bgcolor|border(?:Top|Right|Bottom|Left)?(?:Color)?|boxShadow|caretColor|color|fill|fillStyle|outline(?:Color)?|stroke|strokeStyle|textShadow|WebkitTextFillColor)\s*(?::|=)\s*$/;
|
|
610
|
+
function colourKey(value) {
|
|
611
|
+
const colour = parseColour(value);
|
|
612
|
+
return colour ? `${colour.r},${colour.g},${colour.b},${colour.a}` : null;
|
|
613
|
+
}
|
|
614
|
+
function collectBaselineColourKeys(...roots) {
|
|
615
|
+
const keys = new Set();
|
|
616
|
+
const visit = (value) => {
|
|
617
|
+
if (typeof value === 'string') {
|
|
618
|
+
for (const match of value.matchAll(HEX_COLOUR_PATTERN)) {
|
|
619
|
+
const key = colourKey(match[0]);
|
|
620
|
+
if (key)
|
|
621
|
+
keys.add(key);
|
|
622
|
+
}
|
|
623
|
+
for (const match of value.matchAll(RGB_COLOUR_PATTERN)) {
|
|
624
|
+
const key = colourKey(match[0]);
|
|
625
|
+
if (key)
|
|
626
|
+
keys.add(key);
|
|
627
|
+
}
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (Array.isArray(value)) {
|
|
631
|
+
value.forEach(visit);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (value && typeof value === 'object')
|
|
635
|
+
Object.values(value).forEach(visit);
|
|
636
|
+
};
|
|
637
|
+
roots.forEach(visit);
|
|
638
|
+
return keys;
|
|
639
|
+
}
|
|
640
|
+
function maskComments(source) {
|
|
641
|
+
const characters = [...source];
|
|
642
|
+
let quote = null;
|
|
643
|
+
for (let index = 0; index < characters.length; index += 1) {
|
|
644
|
+
const character = characters[index];
|
|
645
|
+
if (quote) {
|
|
646
|
+
if (character === quote && characters[index - 1] !== '\\')
|
|
647
|
+
quote = null;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (character === '"' || character === "'" || character === '`') {
|
|
651
|
+
quote = character;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (character === '/' && characters[index + 1] === '/') {
|
|
655
|
+
for (; index < characters.length && characters[index] !== '\n'; index += 1) {
|
|
656
|
+
characters[index] = ' ';
|
|
657
|
+
}
|
|
658
|
+
index -= 1;
|
|
659
|
+
}
|
|
660
|
+
else if (character === '/' && characters[index + 1] === '*') {
|
|
661
|
+
characters[index] = ' ';
|
|
662
|
+
characters[index + 1] = ' ';
|
|
663
|
+
index += 2;
|
|
664
|
+
while (index < characters.length
|
|
665
|
+
&& !(characters[index] === '*' && characters[index + 1] === '/')) {
|
|
666
|
+
if (characters[index] !== '\n' && characters[index] !== '\r')
|
|
667
|
+
characters[index] = ' ';
|
|
668
|
+
index += 1;
|
|
669
|
+
}
|
|
670
|
+
if (index < characters.length) {
|
|
671
|
+
characters[index] = ' ';
|
|
672
|
+
characters[index + 1] = ' ';
|
|
673
|
+
index += 1;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return characters.join('');
|
|
678
|
+
}
|
|
679
|
+
function maskQrPrintDocument(source, path) {
|
|
680
|
+
if (!path.replaceAll('\\', '/').endsWith('/QrSignupManager.jsx'))
|
|
681
|
+
return source;
|
|
682
|
+
const call = /\bprintWindow\.document\.write\s*\(\s*`/g.exec(source);
|
|
683
|
+
if (!call)
|
|
684
|
+
return source;
|
|
685
|
+
const start = call.index + call[0].lastIndexOf('`');
|
|
686
|
+
let end = start + 1;
|
|
687
|
+
while (end < source.length) {
|
|
688
|
+
if (source[end] === '`' && source[end - 1] !== '\\')
|
|
689
|
+
break;
|
|
690
|
+
end += 1;
|
|
691
|
+
}
|
|
692
|
+
if (end >= source.length)
|
|
693
|
+
return source;
|
|
694
|
+
// The generated print document is standalone HTML rendered outside the
|
|
695
|
+
// React tree, where no application theme is available. Mask the region as
|
|
696
|
+
// one unit instead of accumulating permanent per-colour exemptions.
|
|
697
|
+
return source.slice(0, start)
|
|
698
|
+
+ source.slice(start, end + 1).replace(/[^\r\n]/g, ' ')
|
|
699
|
+
+ source.slice(end + 1);
|
|
700
|
+
}
|
|
701
|
+
function normalizedSources(sources) {
|
|
702
|
+
return sources.map((entry, index) => typeof entry === 'string'
|
|
703
|
+
? { path: `source-${index + 1}`, source: entry }
|
|
704
|
+
: { path: entry.path || `source-${index + 1}`, source: entry.source || '' });
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Reports colour literals in component source that do not come from the
|
|
708
|
+
* baseline palette, plus report-only uses of MUI's untouched numeric ramps.
|
|
709
|
+
*/
|
|
710
|
+
export function reportOffPaletteColours(sources = [], { palette = BASELINE_PALETTE, baselineStatic = BASELINE_STATIC } = {}) {
|
|
711
|
+
const allowedColours = collectBaselineColourKeys(palette, baselineStatic);
|
|
712
|
+
const findings = [];
|
|
713
|
+
const stringPattern = /(["'`])((?:\\.|(?!\1)[\s\S])*)\1/g;
|
|
714
|
+
for (const { path, source } of normalizedSources(sources)) {
|
|
715
|
+
const normalizedPath = path.replaceAll('\\', '/');
|
|
716
|
+
if (/(^|\/)src\/theme\//.test(normalizedPath))
|
|
717
|
+
continue;
|
|
718
|
+
const scanSource = maskComments(maskQrPrintDocument(source, normalizedPath));
|
|
719
|
+
for (const match of scanSource.matchAll(stringPattern)) {
|
|
720
|
+
const value = match[2].trim();
|
|
721
|
+
const line = source.slice(0, match.index).split(/\r?\n/).length;
|
|
722
|
+
const surface = `${path}:${line}.colour`;
|
|
723
|
+
for (const hex of value.matchAll(HEX_COLOUR_PATTERN)) {
|
|
724
|
+
if (!allowedColours.has(colourKey(hex[0]))) {
|
|
725
|
+
findings.push({
|
|
726
|
+
surface,
|
|
727
|
+
reason: `Off-palette hex colour "${hex[0]}" is not derived from the baseline palette.`,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
for (const rgb of value.matchAll(RGB_COLOUR_PATTERN)) {
|
|
732
|
+
if (!allowedColours.has(colourKey(rgb[0]))) {
|
|
733
|
+
findings.push({
|
|
734
|
+
surface,
|
|
735
|
+
reason: `Off-palette rgb colour "${rgb[0]}" is not derived from the baseline palette.`,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const before = scanSource.slice(Math.max(0, match.index - 40), match.index);
|
|
740
|
+
const isColourProperty = COLOUR_PROPERTY_PATTERN.test(before);
|
|
741
|
+
const numericRamp = value.match(/^([a-z]+)\.(\d+)$/i);
|
|
742
|
+
if (isColourProperty && !numericRamp) {
|
|
743
|
+
for (const namedColour of value.matchAll(NAMED_COLOUR_PATTERN)) {
|
|
744
|
+
findings.push({
|
|
745
|
+
surface,
|
|
746
|
+
reason: `Named CSS colour "${namedColour[0]}" bypasses the baseline palette.`,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (numericRamp && MUI_DEFAULT_NUMERIC_RAMP_NAMES.has(numericRamp[1].toLowerCase())) {
|
|
751
|
+
findings.push({
|
|
752
|
+
surface,
|
|
753
|
+
reason: `Report only: "${value}" uses MUI's untouched numeric palette ramp, not a baseline token.`,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return { findings };
|
|
759
|
+
}
|
|
556
760
|
/** Reports adoption signals as numbers; it never changes completeness. */
|
|
557
761
|
export function reportThemeAdoption(sources = []) {
|
|
558
762
|
const normalized = sources.map((entry, index) => typeof entry === 'string'
|
package/package.json
CHANGED
|
@@ -22,7 +22,7 @@ import { fetchAuthenticators, requestTotpKey, activateTotp, deactivateTotp, fetc
|
|
|
22
22
|
|
|
23
23
|
export const MFA_ALERT_SX = { mb: 2 };
|
|
24
24
|
export const MFA_CARD_SX = { mb: 3 };
|
|
25
|
-
export const MFA_ACTIVE_CARD_SX = { bgcolor: '
|
|
25
|
+
export const MFA_ACTIVE_CARD_SX = { bgcolor: 'success.bg' };
|
|
26
26
|
export const MFA_TEXT_FIELD_SX = { mb: 2 };
|
|
27
27
|
export const MFA_DIVIDER_SX = { my: 3 };
|
|
28
28
|
export const MFA_RECOVERY_BUTTON_SX = { mt: 2 };
|
|
@@ -228,7 +228,7 @@ export function MFAComponent() {
|
|
|
228
228
|
alignItems="center"
|
|
229
229
|
>
|
|
230
230
|
{/* QR CODE */}
|
|
231
|
-
<Box sx={{ p: 2, bgcolor: '
|
|
231
|
+
<Box sx={{ p: 2, bgcolor: 'background.paper', border: '1px solid', borderColor: 'divider' }}>
|
|
232
232
|
<QRCodeSVG value={totpData.key_uri} size={150} />
|
|
233
233
|
</Box>
|
|
234
234
|
|
|
@@ -241,7 +241,7 @@ export function MFAComponent() {
|
|
|
241
241
|
variant="mono"
|
|
242
242
|
sx={{
|
|
243
243
|
fontFamily: 'monospace',
|
|
244
|
-
bgcolor: '
|
|
244
|
+
bgcolor: 'background.subtle',
|
|
245
245
|
p: 1,
|
|
246
246
|
borderRadius: 1,
|
|
247
247
|
display: 'inline-block',
|
|
@@ -300,7 +300,7 @@ export function MFAComponent() {
|
|
|
300
300
|
{t('Auth.MFA_RECOVERY_VIEW_BUTTON')}
|
|
301
301
|
</Button>
|
|
302
302
|
) : (
|
|
303
|
-
<Box sx={{ bgcolor: '
|
|
303
|
+
<Box sx={{ bgcolor: 'background.subtle', p: 2, borderRadius: 1 }}>
|
|
304
304
|
<Alert severity="warning" sx={MFA_ALERT_SX}>
|
|
305
305
|
{t('Auth.MFA_RECOVERY_WARNING')}
|
|
306
306
|
</Alert>
|
|
@@ -310,8 +310,9 @@ export function MFAComponent() {
|
|
|
310
310
|
<Box
|
|
311
311
|
key={code}
|
|
312
312
|
sx={{
|
|
313
|
-
bgcolor: '
|
|
314
|
-
border: '1px solid
|
|
313
|
+
bgcolor: 'background.paper',
|
|
314
|
+
border: '1px solid',
|
|
315
|
+
borderColor: 'divider',
|
|
315
316
|
p: 1,
|
|
316
317
|
borderRadius: 1,
|
|
317
318
|
fontFamily: 'monospace',
|
|
@@ -34,7 +34,8 @@ export function SocialLoginButtons({ onProviderClick, providers }) {
|
|
|
34
34
|
width: 24,
|
|
35
35
|
height: 24,
|
|
36
36
|
borderRadius: '50%',
|
|
37
|
-
border: '1px solid
|
|
37
|
+
border: '1px solid',
|
|
38
|
+
borderColor: 'divider',
|
|
38
39
|
display: 'flex',
|
|
39
40
|
alignItems: 'center',
|
|
40
41
|
justifyContent: 'center',
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Typography,
|
|
10
10
|
} from '@mui/material';
|
|
11
11
|
import { useTranslation } from 'react-i18next';
|
|
12
|
+
import { useTheme } from '@mui/material/styles';
|
|
12
13
|
import { exportChartPng, exportChartSvg } from './exportChart';
|
|
13
14
|
|
|
14
15
|
export const CHART_FRAME_ROOT_SX = { p: 2 };
|
|
@@ -39,6 +40,7 @@ export function ChartFrame({
|
|
|
39
40
|
sx,
|
|
40
41
|
}) {
|
|
41
42
|
const { t } = useTranslation();
|
|
43
|
+
const theme = useTheme();
|
|
42
44
|
const titleId = useId();
|
|
43
45
|
const chartRef = useRef(null);
|
|
44
46
|
const [exportError, setExportError] = useState(false);
|
|
@@ -55,7 +57,11 @@ export function ChartFrame({
|
|
|
55
57
|
const filename = options.filename || 'chart';
|
|
56
58
|
const result = type === 'svg'
|
|
57
59
|
? exportChartSvg(chartRef.current, `${filename}.svg`)
|
|
58
|
-
: await exportChartPng(
|
|
60
|
+
: await exportChartPng(
|
|
61
|
+
chartRef.current,
|
|
62
|
+
`${filename}.png`,
|
|
63
|
+
theme.palette.background.paper,
|
|
64
|
+
);
|
|
59
65
|
const callback = type === 'svg' ? onExportSvg : onExportPng;
|
|
60
66
|
callback?.(result);
|
|
61
67
|
} catch (_error) {
|
|
@@ -29,7 +29,7 @@ export function exportChartSvg(chartContainer, filename = 'chart.svg') {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** Rasterizes the first SVG rendered inside a chart container at two-times scale and downloads it. */
|
|
32
|
-
export function exportChartPng(chartContainer, filename = 'chart.png') {
|
|
32
|
+
export function exportChartPng(chartContainer, filename = 'chart.png', backgroundColour) {
|
|
33
33
|
const source = svgBlob(chartContainer);
|
|
34
34
|
|
|
35
35
|
return new Promise((resolve, reject) => {
|
|
@@ -45,7 +45,7 @@ export function exportChartPng(chartContainer, filename = 'chart.png') {
|
|
|
45
45
|
canvas.width = width * 2;
|
|
46
46
|
canvas.height = height * 2;
|
|
47
47
|
const context = canvas.getContext('2d');
|
|
48
|
-
context.fillStyle =
|
|
48
|
+
context.fillStyle = backgroundColour;
|
|
49
49
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
50
50
|
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
51
51
|
canvas.toBlob((blob) => {
|
|
@@ -1667,6 +1667,30 @@ export const authTranslations = {
|
|
|
1667
1667
|
"en": "Support",
|
|
1668
1668
|
"sw": "Usaidizi"
|
|
1669
1669
|
},
|
|
1670
|
+
"Account.GROUP_MY_ACCOUNT": {
|
|
1671
|
+
"de": "Mein Konto",
|
|
1672
|
+
"fr": "Mon compte",
|
|
1673
|
+
"en": "My account",
|
|
1674
|
+
"sw": "Akaunti yangu"
|
|
1675
|
+
},
|
|
1676
|
+
"Account.GROUP_MANAGEMENT": {
|
|
1677
|
+
"de": "Verwaltung",
|
|
1678
|
+
"fr": "Administration",
|
|
1679
|
+
"en": "Management",
|
|
1680
|
+
"sw": "Usimamizi"
|
|
1681
|
+
},
|
|
1682
|
+
"Account.GROUP_HELP": {
|
|
1683
|
+
"de": "Hilfe",
|
|
1684
|
+
"fr": "Aide",
|
|
1685
|
+
"en": "Help",
|
|
1686
|
+
"sw": "Msaada"
|
|
1687
|
+
},
|
|
1688
|
+
"Account.GROUP_MORE": {
|
|
1689
|
+
"de": "Weitere",
|
|
1690
|
+
"fr": "Autres",
|
|
1691
|
+
"en": "More",
|
|
1692
|
+
"sw": "Zaidi"
|
|
1693
|
+
},
|
|
1670
1694
|
"Account.ACCESS_CODES_HINT": {
|
|
1671
1695
|
"de": "Verwalten Sie Zugangscodes für die Selbstregistrierung.",
|
|
1672
1696
|
"fr": "Gérer les codes d'accès pour l'auto-inscription.",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const sectionNavTranslations = {
|
|
2
|
+
'SectionNav.TITLE': {
|
|
3
|
+
de: 'Bereich wechseln',
|
|
4
|
+
fr: 'Changer de section',
|
|
5
|
+
en: 'Switch section',
|
|
6
|
+
sw: 'Badilisha sehemu',
|
|
7
|
+
},
|
|
8
|
+
'SectionNav.TRIGGER_EYEBROW': {
|
|
9
|
+
de: 'Bereich',
|
|
10
|
+
fr: 'Section',
|
|
11
|
+
en: 'Section',
|
|
12
|
+
sw: 'Sehemu',
|
|
13
|
+
},
|
|
14
|
+
'SectionNav.LAST_OPENED': {
|
|
15
|
+
de: 'Zuletzt geöffnet',
|
|
16
|
+
fr: 'Ouvert en dernier',
|
|
17
|
+
en: 'Last opened',
|
|
18
|
+
sw: 'Iliyofunguliwa mwisho',
|
|
19
|
+
},
|
|
20
|
+
};
|
package/src/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export {
|
|
|
7
7
|
assertKitSxDisjoint,
|
|
8
8
|
calculateContrastRatio,
|
|
9
9
|
KIT_COMPONENT_SX_REGISTRY,
|
|
10
|
+
reportOffPaletteColours,
|
|
10
11
|
reportKitSxBypasses,
|
|
11
12
|
reportThemeAdoption,
|
|
12
13
|
THEME_COMPLETENESS_SURFACES,
|
|
@@ -31,6 +32,7 @@ export * from './utils/authService'; // Service-Funktionen (Passkeys, Social)
|
|
|
31
32
|
|
|
32
33
|
// --- 3. Layouts ---
|
|
33
34
|
export { NarrowPage, WidePage } from './layout/PageLayout';
|
|
35
|
+
export { SectionNav } from './layout/SectionNav';
|
|
34
36
|
|
|
35
37
|
// --- 4. Pages (Vollständige Seiten für Routing) ---
|
|
36
38
|
export { LoginPage } from './pages/LoginPage';
|
|
@@ -77,6 +79,7 @@ export {
|
|
|
77
79
|
// --- 7. Translations ---
|
|
78
80
|
export { authTranslations } from './i18n/authTranslations';
|
|
79
81
|
export { userMenuTranslations } from './i18n/userMenuTranslations';
|
|
82
|
+
export { sectionNavTranslations } from './i18n/sectionNavTranslations';
|
|
80
83
|
|
|
81
84
|
// --- 8. Notifications ---
|
|
82
85
|
export { NotificationSettings } from './notifications/NotificationSettings';
|