@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.
Files changed (37) hide show
  1. package/WORK_ORDERS.md +17 -3
  2. package/dev/entries.jsx +79 -0
  3. package/dev/main.jsx +7 -1
  4. package/dist/components/MFAComponent.js +7 -6
  5. package/dist/components/SocialLoginButtons.js +2 -1
  6. package/dist/components/charts/ChartFrame.js +3 -1
  7. package/dist/components/charts/exportChart.js +2 -2
  8. package/dist/i18n/authTranslations.js +24 -0
  9. package/dist/i18n/sectionNavTranslations.js +20 -0
  10. package/dist/index.js +3 -1
  11. package/dist/layout/SectionNav.js +138 -0
  12. package/dist/pages/AccountPage.js +44 -2
  13. package/dist/theme/index.js +1 -1
  14. package/dist/theme/kitSxRegistry.js +8 -1
  15. package/dist/theme/themeCompleteness.js +218 -14
  16. package/package.json +1 -1
  17. package/src/components/MFAComponent.jsx +7 -6
  18. package/src/components/SocialLoginButtons.jsx +2 -1
  19. package/src/components/charts/ChartFrame.jsx +7 -1
  20. package/src/components/charts/exportChart.js +2 -2
  21. package/src/i18n/authTranslations.ts +24 -0
  22. package/src/i18n/sectionNavTranslations.ts +20 -0
  23. package/src/index.js +3 -0
  24. package/src/layout/SectionNav.jsx +299 -0
  25. package/src/pages/AccountPage.jsx +55 -14
  26. package/src/theme/index.js +1 -0
  27. package/src/theme/kitSxRegistry.js +11 -1
  28. package/src/theme/themeCompleteness.js +237 -14
  29. package/tests/AccountPage.test.jsx +193 -0
  30. package/tests/ChartFrame.test.jsx +5 -0
  31. package/tests/SectionNav.test.jsx +274 -0
  32. package/tests/themeCompleteness.test.js +156 -4
  33. package/work-orders/AUTH-4.md +183 -0
  34. package/work-orders/I18N-1.md +254 -0
  35. package/work-orders/SHELL-5.md +345 -0
  36. package/work-orders/THEME-5.md +341 -0
  37. 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 { Tabs, Tab, Box, Typography, Alert, CircularProgress, Paper, Stack, } from '@mui/material';
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] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), 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 }) }))] }));
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
  }
@@ -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 (`py`, `px`, `pt`, ...) that
346
- // styleOverrides' plain CSS-in-JS objects do not -- a baseline `padding`
347
- // default and a component's own `sx={{ py: 1 }}` collide on the vertical
348
- // axis even though the two key strings never match literally. Each key is
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. Spacing shorthands (`py`/`px`/...)
387
- * are compared by the longhand axis they affect, not by literal key name --
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: conditional sx, nested sx,
391
- * callback results, and nested selector properties are not inspected. A clean
392
- * result therefore proves only that the registered top-level key sets are
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "2.34.0",
3
+ "version": "2.36.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "repository": {
@@ -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: '#f0fdf4' };
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: 'white', border: '1px solid #eee' }}>
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: '#eee',
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: '#f5f5f5', p: 2, borderRadius: 1 }}>
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: 'white',
314
- border: '1px solid #ddd',
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 rgba(0,0,0,0.2)',
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(chartRef.current, `${filename}.png`);
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 = 'white';
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';