@micha.bigler/ui-core-micha 3.14.4 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +7 -0
- package/dist/chat/kitSxRegistry.js +9 -0
- package/dist/components/charts/kitSxRegistry.js +6 -0
- package/dist/entries/charts.js +6 -0
- package/dist/entries/chat.js +8 -0
- package/dist/entries/markdown.js +1 -0
- package/dist/index.js +0 -14
- package/dist/pages/AccountPage.js +1 -2
- package/dist/pages/LoginPage.js +1 -2
- package/dist/pages/PasswordChangePage.js +1 -2
- package/dist/pages/PasswordInvitePage.js +1 -2
- package/dist/pages/PasswordResetRequestPage.js +1 -2
- package/dist/pages/SignUpPage.js +1 -2
- package/dist/pages/SignupConfirmPage.js +1 -2
- package/dist/theme/kitSxEntry.js +11 -0
- package/dist/theme/kitSxRegistry.js +2 -15
- package/package.json +32 -7
- package/.github/workflows/ci.yml +0 -35
- package/.github/workflows/publish.yml +0 -126
- package/DESIGN.md +0 -60
- package/WORK_ORDERS.md +0 -150
- package/dev/entries.jsx +0 -721
- package/dev/fixtures.js +0 -26
- package/dev/main.jsx +0 -75
- package/dev/mockTransport.jsx +0 -128
- package/docs/CHART-LAYOUT.md +0 -157
- package/docs/DARK-TOKENS.md +0 -140
- package/docs/TYPE-SCALE.md +0 -111
- package/docs/messaging-deviations.md +0 -118
- package/index.html +0 -12
- package/renovate.json +0 -10
- package/scripts/chart-api-census.mjs +0 -415
- package/src/auth/AuthContext.jsx +0 -158
- package/src/auth/ReauthModal.jsx +0 -82
- package/src/auth/UserMenu.jsx +0 -121
- package/src/auth/apiClient.jsx +0 -232
- package/src/auth/authApi.jsx +0 -539
- package/src/auth/authConfig.jsx +0 -34
- package/src/auth/reauth.js +0 -47
- package/src/chat/ChatComposer.jsx +0 -36
- package/src/chat/ChatList.jsx +0 -87
- package/src/chat/ChatThread.jsx +0 -128
- package/src/chat/ChatWorkspace.jsx +0 -244
- package/src/chat/MarkdownReply.jsx +0 -22
- package/src/chat/ModelPicker.jsx +0 -85
- package/src/chat/api.js +0 -18
- package/src/chat/useChatSocket.js +0 -32
- package/src/chat/wire.js +0 -7
- package/src/components/AccessCodeManager.jsx +0 -288
- package/src/components/AccessCodeSingleUseToggle.jsx +0 -92
- package/src/components/AllowedEmailDomainsManager.jsx +0 -92
- package/src/components/AuthFactorRequirementCard.jsx +0 -95
- package/src/components/BulkInviteCsvTab.jsx +0 -198
- package/src/components/EmptyState.jsx +0 -38
- package/src/components/LoginForm.jsx +0 -159
- package/src/components/MFAComponent.jsx +0 -350
- package/src/components/MfaLoginComponent.jsx +0 -252
- package/src/components/MobileBottomNav.jsx +0 -82
- package/src/components/PasskeysComponent.jsx +0 -252
- package/src/components/PasswordChangeForm.jsx +0 -87
- package/src/components/PasswordResetRequestForm.jsx +0 -43
- package/src/components/PasswordSetForm.jsx +0 -97
- package/src/components/ProfileComponent.jsx +0 -348
- package/src/components/QrSignupManager.jsx +0 -404
- package/src/components/QrSignupValidityManager.jsx +0 -106
- package/src/components/RegistrationMethodsManager.jsx +0 -150
- package/src/components/SecurityComponent.jsx +0 -141
- package/src/components/SocialLoginButtons.jsx +0 -83
- package/src/components/SoftChip.jsx +0 -93
- package/src/components/StatTile.jsx +0 -39
- package/src/components/SupportRecoveryRequestsTab.jsx +0 -482
- package/src/components/TurnstileWidget.jsx +0 -99
- package/src/components/UserInviteComponent.jsx +0 -76
- package/src/components/UserListComponent.jsx +0 -618
- package/src/components/charts/BarChart.jsx +0 -102
- package/src/components/charts/ChartFrame.jsx +0 -214
- package/src/components/charts/LineChart.jsx +0 -126
- package/src/components/charts/ScatterChart.jsx +0 -299
- package/src/components/charts/TimeSeriesChart.jsx +0 -300
- package/src/components/charts/chartDefaults.js +0 -644
- package/src/components/charts/chartLabels.js +0 -36
- package/src/components/charts/exportChart.js +0 -258
- package/src/components/charts/formatters.js +0 -39
- package/src/components/charts/palette.js +0 -33
- package/src/components/charts/yearTickInterval.js +0 -7
- package/src/i18n/authTranslations.ts +0 -2651
- package/src/i18n/chartsTranslations.ts +0 -91
- package/src/i18n/chatTranslations.ts +0 -55
- package/src/i18n/messagingTranslations.ts +0 -120
- package/src/i18n/notificationsTranslations.ts +0 -32
- package/src/i18n/onboardingTranslations.ts +0 -29
- package/src/i18n/sectionNavTranslations.ts +0 -20
- package/src/i18n/uiCoreTranslations.js +0 -29
- package/src/i18n/userMenuTranslations.ts +0 -5
- package/src/index.js +0 -158
- package/src/layout/PageLayout.jsx +0 -36
- package/src/layout/SectionNav.jsx +0 -320
- package/src/messaging/AttachmentList.jsx +0 -145
- package/src/messaging/Composer.jsx +0 -197
- package/src/messaging/ConversationLaunchers.jsx +0 -51
- package/src/messaging/ConversationList.jsx +0 -109
- package/src/messaging/DirectMessageLauncher.jsx +0 -112
- package/src/messaging/MessageBubble.jsx +0 -183
- package/src/messaging/MessagingProvider.jsx +0 -0
- package/src/messaging/MessagingScopeConfig.jsx +0 -24
- package/src/messaging/PollCard.jsx +0 -163
- package/src/messaging/ReactionBar.jsx +0 -43
- package/src/messaging/ReadTicks.jsx +0 -82
- package/src/messaging/Thread.jsx +0 -195
- package/src/messaging/api.js +0 -48
- package/src/messaging/composerImageCompression.js +0 -35
- package/src/messaging/linkifyText.jsx +0 -37
- package/src/notifications/NotificationBell.jsx +0 -84
- package/src/notifications/NotificationSettings.jsx +0 -330
- package/src/notifications/NotificationsProvider.jsx +0 -224
- package/src/notifications/PopupSurface.jsx +0 -105
- package/src/notifications/api.js +0 -55
- package/src/notifications/feedApi.js +0 -18
- package/src/notifications/realtime.jsx +0 -138
- package/src/notifications/serviceWorker/sw.js +0 -50
- package/src/onboarding/OnboardingProvider.jsx +0 -326
- package/src/onboarding/OnboardingWizard.jsx +0 -61
- package/src/onboarding/WizardDialogShell.jsx +0 -51
- package/src/onboarding/api.js +0 -13
- package/src/onboarding/stepSelection.js +0 -16
- package/src/onboarding/steps/BrowserPushStep.jsx +0 -121
- package/src/onboarding/steps/CompleteNameStep.jsx +0 -46
- package/src/onboarding/steps/CookieConsentStep.jsx +0 -133
- package/src/onboarding/steps/PwaInstallStep.jsx +0 -47
- package/src/pages/AccountPage.jsx +0 -394
- package/src/pages/LoginPage.jsx +0 -343
- package/src/pages/PasswordChangePage.jsx +0 -62
- package/src/pages/PasswordInvitePage.jsx +0 -177
- package/src/pages/PasswordResetRequestPage.jsx +0 -68
- package/src/pages/SignUpPage.jsx +0 -268
- package/src/pages/SignupConfirmPage.jsx +0 -155
- package/src/testing/checkKitIntegration.js +0 -233
- package/src/theme/createAppTheme.js +0 -423
- package/src/theme/fonts.js +0 -3
- package/src/theme/index.js +0 -21
- package/src/theme/kitSxRegistry.js +0 -285
- package/src/theme/themeCompleteness.js +0 -1045
- package/src/theme/tokens.js +0 -478
- package/src/utils/auth-errors.js +0 -76
- package/src/utils/authService.js +0 -223
- package/src/utils/resolveErrorText.js +0 -28
- package/src/utils/webauthn.js +0 -51
- package/tests/AccountPage.profileStatements.test.jsx +0 -162
- package/tests/AccountPage.test.jsx +0 -200
- package/tests/AuthContext.test.jsx +0 -129
- package/tests/BarChart.test.jsx +0 -112
- package/tests/BrowserPushStep.test.jsx +0 -110
- package/tests/ChartFrame.test.jsx +0 -336
- package/tests/ConversationList.test.jsx +0 -144
- package/tests/CookieConsentStep.test.jsx +0 -69
- package/tests/EmptyState.test.jsx +0 -62
- package/tests/LineChart.test.jsx +0 -80
- package/tests/MessagingProvider.test.jsx +0 -50
- package/tests/MobileBottomNav.test.jsx +0 -319
- package/tests/NotificationBell.test.jsx +0 -60
- package/tests/NotificationSettings.test.jsx +0 -276
- package/tests/NotificationsProvider.test.jsx +0 -240
- package/tests/OnboardingProvider.test.jsx +0 -294
- package/tests/OnboardingWizard.test.jsx +0 -143
- package/tests/PasswordChangeForm.test.jsx +0 -70
- package/tests/PasswordInvitePage.test.jsx +0 -142
- package/tests/PasswordSetForm.test.jsx +0 -70
- package/tests/PopupSurface.test.jsx +0 -142
- package/tests/ProfileComponent.refresh.test.jsx +0 -194
- package/tests/QrSignupManager.registrationContext.test.jsx +0 -106
- package/tests/QrSignupManagerInfoBox.test.jsx +0 -49
- package/tests/ScatterChart.test.jsx +0 -270
- package/tests/SectionNav.test.jsx +0 -363
- package/tests/SignUpPage.test.jsx +0 -208
- package/tests/SoftChip.test.jsx +0 -105
- package/tests/StatTile.test.jsx +0 -57
- package/tests/Thread.test.jsx +0 -151
- package/tests/TimeSeriesChart.test.jsx +0 -367
- package/tests/UserListComponent.test.jsx +0 -192
- package/tests/UserMenu.test.jsx +0 -171
- package/tests/apiClientLanguage.test.js +0 -33
- package/tests/apiClientPublicPaths.test.js +0 -178
- package/tests/authApiLogin.test.js +0 -42
- package/tests/authApiRegistration.test.js +0 -31
- package/tests/authErrorCodeRendering.test.jsx +0 -77
- package/tests/authErrors.test.js +0 -74
- package/tests/authTranslations.test.js +0 -74
- package/tests/chart-api-census.test.js +0 -366
- package/tests/chartAxisTickBand.test.jsx +0 -95
- package/tests/chartDefaults.test.js +0 -902
- package/tests/chartLabels.test.js +0 -50
- package/tests/chartsPalette.test.js +0 -99
- package/tests/chatApi.test.js +0 -48
- package/tests/chatDriverNames.test.jsx +0 -71
- package/tests/chatExports.test.js +0 -3
- package/tests/chatList.test.jsx +0 -84
- package/tests/chatPicker.test.jsx +0 -82
- package/tests/chatSocket.test.jsx +0 -26
- package/tests/chatThread.test.jsx +0 -116
- package/tests/chatTranslations.test.js +0 -47
- package/tests/chatWorkspace.test.jsx +0 -234
- package/tests/checkKitIntegration.test.jsx +0 -219
- package/tests/createAppTheme.test.js +0 -132
- package/tests/createAppThemeDark.test.js +0 -127
- package/tests/createAppThemeLightGuard.test.js +0 -40
- package/tests/createAppThemePageContrast.test.js +0 -108
- package/tests/createAppThemeTypography.test.js +0 -53
- package/tests/errorCallSiteGuard.test.js +0 -32
- package/tests/exportChart.test.js +0 -313
- package/tests/feedApi.test.js +0 -46
- package/tests/fixtures/light-theme-baseline.json +0 -2234
- package/tests/i18nAggregate.test.js +0 -120
- package/tests/i18nGermanVariant.test.js +0 -65
- package/tests/loginPageErrorResolution.test.jsx +0 -102
- package/tests/messagingApi.test.js +0 -32
- package/tests/messagingChunk6.test.jsx +0 -116
- package/tests/messagingCompactBubble.test.jsx +0 -64
- package/tests/messagingComposer.test.jsx +0 -136
- package/tests/messagingComposerChunk5.test.jsx +0 -113
- package/tests/messagingContractConformance.test.js +0 -69
- package/tests/messagingDirectMessageLauncher.test.jsx +0 -151
- package/tests/messagingExports.test.js +0 -21
- package/tests/messagingInteractions.test.jsx +0 -136
- package/tests/messagingLastMessagePreview.test.jsx +0 -93
- package/tests/messagingLinkify.test.jsx +0 -51
- package/tests/messagingMessageActions.test.jsx +0 -163
- package/tests/messagingMsg6.test.jsx +0 -105
- package/tests/messagingMsg6f.test.jsx +0 -155
- package/tests/messagingMsg6g.test.jsx +0 -166
- package/tests/messagingMsg6h.test.jsx +0 -136
- package/tests/messagingMsg6i.test.jsx +0 -129
- package/tests/messagingPollRendering.test.jsx +0 -142
- package/tests/messagingReactionMerge.test.jsx +0 -77
- package/tests/messagingReadTicksTooltip.test.jsx +0 -36
- package/tests/messagingThreadReceipts.test.jsx +0 -178
- package/tests/messagingThreadingQuoting.test.jsx +0 -87
- package/tests/messagingUnreadLifecycle.test.jsx +0 -78
- package/tests/notificationsApi.test.js +0 -41
- package/tests/notificationsExports.test.js +0 -34
- package/tests/onboardingDescriptors.test.js +0 -132
- package/tests/packageTreeShaking.test.js +0 -126
- package/tests/realtime.test.jsx +0 -219
- package/tests/resolveErrorText.test.js +0 -72
- package/tests/stepSelection.test.js +0 -53
- package/tests/themeCompleteness.test.js +0 -626
- package/tests/timeSeriesLegend.test.js +0 -99
- package/tests/userScreenTranslations.test.jsx +0 -218
- package/tsconfig.build.json +0 -18
- package/vite.config.mjs +0 -6
- package/work-orders/AUTH-1.md +0 -74
- package/work-orders/AUTH-2.md +0 -74
- package/work-orders/AUTH-4.md +0 -183
- package/work-orders/AUTH-5.md +0 -154
- package/work-orders/AUTH-7.md +0 -181
- package/work-orders/AUTH-8.md +0 -184
- package/work-orders/CHART-1.md +0 -285
- package/work-orders/CHART-2.md +0 -222
- package/work-orders/CHART-3.md +0 -163
- package/work-orders/CHART-4.md +0 -203
- package/work-orders/CHART-5.md +0 -226
- package/work-orders/CHART-6.md +0 -428
- package/work-orders/CHART-7.md +0 -175
- package/work-orders/DX-1.md +0 -169
- package/work-orders/DX-2.md +0 -188
- package/work-orders/DX-3.md +0 -195
- package/work-orders/I18N-1.md +0 -254
- package/work-orders/I18N-3.md +0 -207
- package/work-orders/MSG-3.md +0 -375
- package/work-orders/MSG-3b.md +0 -1002
- package/work-orders/MSG-3c.md +0 -378
- package/work-orders/MSG-3d.md +0 -268
- package/work-orders/MSG-3e.md +0 -164
- package/work-orders/MSG-3f.md +0 -391
- package/work-orders/MSG-6e.md +0 -308
- package/work-orders/MSG-6f.md +0 -214
- package/work-orders/MSG-6g.md +0 -244
- package/work-orders/MSG-6h.md +0 -238
- package/work-orders/MSG-6i.md +0 -271
- package/work-orders/MSG-6j.md +0 -117
- package/work-orders/NOTIF-12.md +0 -214
- package/work-orders/NOTIF-13.md +0 -142
- package/work-orders/NOTIF-14.md +0 -151
- package/work-orders/PRIM-1.md +0 -177
- package/work-orders/SHELL-1.md +0 -255
- package/work-orders/SHELL-2.md +0 -197
- package/work-orders/SHELL-3.md +0 -359
- package/work-orders/SHELL-4.md +0 -351
- package/work-orders/SHELL-5.md +0 -345
- package/work-orders/SHELL-6.md +0 -210
- package/work-orders/THEME-1.md +0 -587
- package/work-orders/THEME-10.md +0 -303
- package/work-orders/THEME-11.md +0 -208
- package/work-orders/THEME-2.md +0 -339
- package/work-orders/THEME-3.md +0 -210
- package/work-orders/THEME-4.md +0 -271
- package/work-orders/THEME-5.md +0 -341
- package/work-orders/THEME-6.md +0 -247
- package/work-orders/THEME-7.md +0 -82
- package/work-orders/THEME-8.md +0 -211
- package/work-orders/THEME-9.md +0 -194
- package/work-orders/UCM-AUTH-10.md +0 -490
- package/work-orders/UCM-AUTH-11.md +0 -185
- package/work-orders/UCM-AUTH-12.md +0 -98
- package/work-orders/UCM-AUTH-13.md +0 -47
- package/work-orders/UCM-AUTH-9.md +0 -207
- package/work-orders/UCM-CHART-10.md +0 -150
- package/work-orders/UCM-CHART-11.md +0 -132
- package/work-orders/UCM-CHART-12.md +0 -348
- package/work-orders/UCM-CHART-13.md +0 -122
- package/work-orders/UCM-CHART-14.md +0 -198
- package/work-orders/UCM-CHART-15.md +0 -187
- package/work-orders/UCM-CHART-16.md +0 -169
- package/work-orders/UCM-CHART-17.md +0 -195
- package/work-orders/UCM-CHART-18.md +0 -200
- package/work-orders/UCM-CHART-8.md +0 -147
- package/work-orders/UCM-CHART-9.md +0 -265
- package/work-orders/UCM-CHAT-1.md +0 -190
- package/work-orders/UCM-CHAT-2.md +0 -75
- package/work-orders/UCM-CI-1.md +0 -65
- package/work-orders/UCM-DEP-1.md +0 -63
- package/work-orders/UCM-I18N-4.md +0 -226
- package/work-orders/UCM-I18N-5.md +0 -211
- package/work-orders/UCM-MSG-20.md +0 -192
- package/work-orders/UCM-PRIM-2.md +0 -114
- package/work-orders/UCM-SHELL-7.md +0 -134
- package/work-orders/UCM-SHELL-8.md +0 -125
- package/work-orders/UCM-TEST-1.md +0 -304
- package/work-orders/UCM-THEME-13.md +0 -69
- package/work-orders/UCM-THEME-14.md +0 -312
- package/work-orders/UCM-THEME-15.md +0 -356
- package/work-orders/UCM-THEME-16.md +0 -80
- package/work-orders/UCM-THEME-17.md +0 -111
- package/work-orders/UCM-THEME-18.md +0 -184
- package/work-orders/assets/AUTH-4-user-list-card.html +0 -407
- package/work-orders/assets/PRIM-1-dashboard-primitives.html +0 -624
- package/work-orders/assets/SHELL-2-account-navigation.html +0 -1159
- package/work-orders/assets/THEME-1-baseline-reference-sheet.html +0 -1352
- package/work-orders/assets/UCM-CHAT-1-ai-chat.html +0 -447
- package/work-orders/assets/UCM-THEME-13-dark-prototype.html +0 -390
- package/work-orders/assets/UCM-THEME-17-type-scale.html +0 -391
|
@@ -1,1045 +0,0 @@
|
|
|
1
|
-
import { createTheme, getContrastRatio as getMuiContrastRatio } from '@mui/material/styles';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
BASELINE_PALETTE,
|
|
5
|
-
BASELINE_STATIC,
|
|
6
|
-
STATUS_KEYS,
|
|
7
|
-
TYPOGRAPHY_VARIANTS,
|
|
8
|
-
} from './tokens';
|
|
9
|
-
import { KIT_COMPONENT_SX_REGISTRY } from './kitSxRegistry';
|
|
10
|
-
|
|
11
|
-
const MUI_DEFAULT_THEME = createTheme();
|
|
12
|
-
|
|
13
|
-
function getPath(value, path) {
|
|
14
|
-
return path.split('.').reduce((current, key) => current?.[key], value);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function sameValue(left, right) {
|
|
18
|
-
if (left === right) return true;
|
|
19
|
-
if (left === undefined || right === undefined) return false;
|
|
20
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const pathSurface = (surface, path = surface) => ({
|
|
24
|
-
surface,
|
|
25
|
-
get: (theme) => getPath(theme, path),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const keyedSurface = (surface, get) => ({ surface, get });
|
|
29
|
-
|
|
30
|
-
const STATUS_SURFACES = STATUS_KEYS.flatMap((status) =>
|
|
31
|
-
['text', 'fill', 'fillText', 'bg'].map((token) =>
|
|
32
|
-
pathSurface(`palette.${status}.${token}`),
|
|
33
|
-
),
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
const TYPOGRAPHY_SURFACES = TYPOGRAPHY_VARIANTS.flatMap((variant) =>
|
|
37
|
-
['fontSize', 'fontWeight', 'lineHeight', 'letterSpacing'].map((token) =>
|
|
38
|
-
pathSurface(`typography.${variant}.${token}`),
|
|
39
|
-
),
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
const componentPath = (component, path) =>
|
|
43
|
-
pathSurface(`components.${component}.${path}`);
|
|
44
|
-
|
|
45
|
-
const componentKeyLeaf = (component, path, key, leaf) => keyedSurface(
|
|
46
|
-
`components.${component}.${path}.${key}.${leaf}`,
|
|
47
|
-
(theme) => getPath(theme, `components.${component}.${path}`)?.[key]?.[leaf],
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
const COMPONENT_SURFACES = [
|
|
51
|
-
keyedSurface(
|
|
52
|
-
'components.MuiCssBaseline.reduced-motion.transitionDuration',
|
|
53
|
-
(theme) => theme.components?.MuiCssBaseline?.styleOverrides
|
|
54
|
-
?.['@media (prefers-reduced-motion: reduce)']
|
|
55
|
-
?.['*, *::before, *::after']?.transitionDuration,
|
|
56
|
-
),
|
|
57
|
-
componentPath('MuiButton', 'defaultProps.disableElevation'),
|
|
58
|
-
componentPath('MuiButton', 'styleOverrides.root.minHeight'),
|
|
59
|
-
componentPath('MuiButton', 'styleOverrides.root.borderRadius'),
|
|
60
|
-
componentPath('MuiButton', 'styleOverrides.root.textTransform'),
|
|
61
|
-
componentKeyLeaf('MuiButton', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
|
|
62
|
-
componentKeyLeaf('MuiButton', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
|
|
63
|
-
componentPath('MuiButton', 'styleOverrides.contained.boxShadow'),
|
|
64
|
-
componentPath('MuiButton', 'styleOverrides.outlined.borderColor'),
|
|
65
|
-
componentPath('MuiTableCell', 'styleOverrides.root.padding'),
|
|
66
|
-
componentPath('MuiIconButton', 'styleOverrides.root.minHeight'),
|
|
67
|
-
componentPath('MuiIconButton', 'styleOverrides.root.minWidth'),
|
|
68
|
-
componentKeyLeaf('MuiIconButton', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
|
|
69
|
-
componentKeyLeaf('MuiIconButton', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
|
|
70
|
-
componentPath('MuiDivider', 'styleOverrides.root.borderColor'),
|
|
71
|
-
componentPath('MuiTooltip', 'styleOverrides.tooltip.boxShadow'),
|
|
72
|
-
componentPath('MuiTooltip', 'styleOverrides.tooltip.transitionDuration'),
|
|
73
|
-
componentPath('MuiTooltip', 'defaultProps.slotProps.transition.timeout.enter'),
|
|
74
|
-
componentPath('MuiTooltip', 'defaultProps.slotProps.transition.timeout.exit'),
|
|
75
|
-
componentPath('MuiSelect', 'defaultProps.variant'),
|
|
76
|
-
componentPath('MuiSelect', 'styleOverrides.root.minHeight'),
|
|
77
|
-
componentKeyLeaf('MuiSelect', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
|
|
78
|
-
componentPath('MuiCheckbox', 'styleOverrides.root.color'),
|
|
79
|
-
componentKeyLeaf('MuiCheckbox', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
|
|
80
|
-
componentKeyLeaf('MuiCheckbox', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
|
|
81
|
-
componentPath('MuiFormControlLabel', 'styleOverrides.root.minHeight'),
|
|
82
|
-
componentKeyLeaf('MuiFormControlLabel', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
|
|
83
|
-
componentKeyLeaf('MuiFormControlLabel', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
|
|
84
|
-
componentPath('MuiFormControlLabel', 'styleOverrides.label.fontSize'),
|
|
85
|
-
componentPath('MuiContainer', 'styleOverrides.root.paddingLeft'),
|
|
86
|
-
componentPath('MuiContainer', 'styleOverrides.root.paddingRight'),
|
|
87
|
-
componentKeyLeaf('MuiContainer', 'styleOverrides.root', '@media (min-width:1200px)', 'paddingLeft'),
|
|
88
|
-
componentKeyLeaf('MuiContainer', 'styleOverrides.root', '@media (min-width:1200px)', 'paddingRight'),
|
|
89
|
-
componentPath('MuiPaper', 'defaultProps.elevation'),
|
|
90
|
-
componentPath('MuiPaper', 'defaultProps.variant'),
|
|
91
|
-
componentPath('MuiPaper', 'styleOverrides.root.borderRadius'),
|
|
92
|
-
componentPath('MuiPaper', 'styleOverrides.root.boxShadow'),
|
|
93
|
-
componentPath('MuiCard', 'defaultProps.elevation'),
|
|
94
|
-
componentPath('MuiCard', 'defaultProps.variant'),
|
|
95
|
-
componentPath('MuiCard', 'styleOverrides.root.borderRadius'),
|
|
96
|
-
componentPath('MuiCard', 'styleOverrides.root.boxShadow'),
|
|
97
|
-
componentPath('MuiChip', 'styleOverrides.root.height'),
|
|
98
|
-
componentPath('MuiChip', 'styleOverrides.root.borderRadius'),
|
|
99
|
-
componentPath('MuiChip', 'styleOverrides.root.fontSize'),
|
|
100
|
-
keyedSurface(
|
|
101
|
-
'components.MuiChip.styleOverrides.root.coarse-hit-area.inset',
|
|
102
|
-
(theme) => theme.components?.MuiChip?.styleOverrides?.root
|
|
103
|
-
?.['@media (any-pointer: coarse)']?.['&::after']?.inset,
|
|
104
|
-
),
|
|
105
|
-
componentPath('MuiBottomNavigation', 'styleOverrides.root.boxShadow'),
|
|
106
|
-
componentPath('MuiBottomNavigation', 'styleOverrides.root.backgroundColor'),
|
|
107
|
-
componentPath('MuiBottomNavigationAction', 'styleOverrides.root.padding'),
|
|
108
|
-
componentPath('MuiBottomNavigationAction', 'styleOverrides.root.gap'),
|
|
109
|
-
componentKeyLeaf('MuiBottomNavigationAction', 'styleOverrides.root', '& .MuiSvgIcon-root', 'width'),
|
|
110
|
-
componentKeyLeaf('MuiBottomNavigationAction', 'styleOverrides.root', '& .MuiSvgIcon-root', 'height'),
|
|
111
|
-
componentPath('MuiBottomNavigationAction', 'styleOverrides.root.color'),
|
|
112
|
-
componentPath('MuiBottomNavigationAction', 'styleOverrides.label.fontSize'),
|
|
113
|
-
componentPath('MuiBottomNavigationAction', 'styleOverrides.label.fontWeight'),
|
|
114
|
-
componentPath('MuiBottomNavigationAction', 'styleOverrides.label.lineHeight'),
|
|
115
|
-
componentPath('MuiBottomNavigationAction', 'styleOverrides.label.maxWidth'),
|
|
116
|
-
componentPath('MuiBottomNavigationAction', 'styleOverrides.label.whiteSpace'),
|
|
117
|
-
componentPath('MuiBottomNavigationAction', 'styleOverrides.label.overflow'),
|
|
118
|
-
componentPath('MuiBottomNavigationAction', 'styleOverrides.label.textOverflow'),
|
|
119
|
-
componentKeyLeaf('MuiBottomNavigationAction', 'styleOverrides.label', '&.Mui-selected', 'fontSize'),
|
|
120
|
-
componentKeyLeaf('MuiBottomNavigationAction', 'styleOverrides.root', '&.Mui-selected', 'color'),
|
|
121
|
-
componentPath('MuiTextField', 'defaultProps.variant'),
|
|
122
|
-
componentPath('MuiTextField', 'styleOverrides.root.minHeight'),
|
|
123
|
-
componentKeyLeaf('MuiTextField', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
|
|
124
|
-
componentKeyLeaf('MuiTextField', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
|
|
125
|
-
componentPath('MuiOutlinedInput', 'styleOverrides.root.minHeight'),
|
|
126
|
-
componentPath('MuiOutlinedInput', 'styleOverrides.root.borderRadius'),
|
|
127
|
-
keyedSurface(
|
|
128
|
-
'components.MuiOutlinedInput.variants.non-multiline-height',
|
|
129
|
-
(theme) => theme.components?.MuiOutlinedInput?.variants
|
|
130
|
-
?.find(({ props }) => props?.multiline === false)?.style?.height,
|
|
131
|
-
),
|
|
132
|
-
componentKeyLeaf('MuiOutlinedInput', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
|
|
133
|
-
componentKeyLeaf('MuiOutlinedInput', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
|
|
134
|
-
componentKeyLeaf('MuiOutlinedInput', 'styleOverrides.root', '& .MuiOutlinedInput-notchedOutline', 'borderColor'),
|
|
135
|
-
componentKeyLeaf('MuiOutlinedInput', 'styleOverrides.root', '&:hover .MuiOutlinedInput-notchedOutline', 'borderColor'),
|
|
136
|
-
componentKeyLeaf('MuiOutlinedInput', 'styleOverrides.root', '&.Mui-focused .MuiOutlinedInput-notchedOutline', 'borderColor'),
|
|
137
|
-
componentKeyLeaf('MuiOutlinedInput', 'styleOverrides.root', '&.Mui-error .MuiOutlinedInput-notchedOutline', 'borderColor'),
|
|
138
|
-
componentPath('MuiDialog', 'defaultProps.transitionDuration.enter'),
|
|
139
|
-
componentPath('MuiDialog', 'defaultProps.transitionDuration.exit'),
|
|
140
|
-
componentPath('MuiDialog', 'styleOverrides.paper.borderRadius'),
|
|
141
|
-
componentPath('MuiDialog', 'styleOverrides.paper.boxShadow'),
|
|
142
|
-
componentPath('MuiDrawer', 'styleOverrides.paper.boxShadow'),
|
|
143
|
-
componentPath('MuiMenu', 'styleOverrides.paper.boxShadow'),
|
|
144
|
-
...['success', 'warning', 'error', 'info'].flatMap((status) => {
|
|
145
|
-
const slot = `standard${status[0].toUpperCase()}${status.slice(1)}`;
|
|
146
|
-
return [
|
|
147
|
-
componentPath('MuiAlert', `styleOverrides.${slot}.color`),
|
|
148
|
-
componentPath('MuiAlert', `styleOverrides.${slot}.backgroundColor`),
|
|
149
|
-
componentKeyLeaf('MuiAlert', `styleOverrides.${slot}`, '& .MuiAlert-icon', 'color'),
|
|
150
|
-
];
|
|
151
|
-
}),
|
|
152
|
-
...['MuiOutlinedInput', 'MuiFilledInput', 'MuiInput'].flatMap((component) => [
|
|
153
|
-
componentKeyLeaf(component, 'styleOverrides.input', '&:-webkit-autofill', 'WebkitBoxShadow'),
|
|
154
|
-
componentKeyLeaf(component, 'styleOverrides.input', '&:-webkit-autofill', 'WebkitTextFillColor'),
|
|
155
|
-
componentKeyLeaf(component, 'styleOverrides.input', '&:autofill', 'boxShadow'),
|
|
156
|
-
componentKeyLeaf(component, 'styleOverrides.input', '&:autofill', 'WebkitTextFillColor'),
|
|
157
|
-
]),
|
|
158
|
-
];
|
|
159
|
-
|
|
160
|
-
/** The versioned surface inventory used by every adopting application. */
|
|
161
|
-
export const THEME_COMPLETENESS_SURFACES = [
|
|
162
|
-
pathSurface('palette.primary.main'),
|
|
163
|
-
...STATUS_SURFACES,
|
|
164
|
-
pathSurface('palette.ink.primary'),
|
|
165
|
-
pathSurface('palette.ink.secondary'),
|
|
166
|
-
pathSurface('palette.ink.muted'),
|
|
167
|
-
pathSurface('palette.background.default'),
|
|
168
|
-
pathSurface('palette.background.paper'),
|
|
169
|
-
pathSurface('palette.background.subtle'),
|
|
170
|
-
pathSurface('palette.divider'),
|
|
171
|
-
pathSurface('palette.controlBorder.main'),
|
|
172
|
-
pathSurface('palette.controlBorder.hover'),
|
|
173
|
-
pathSurface('palette.controlBorder.focus'),
|
|
174
|
-
pathSurface('palette.controlBorder.error'),
|
|
175
|
-
pathSurface('palette.dataSeries.categorical'),
|
|
176
|
-
pathSurface('radius.control'),
|
|
177
|
-
pathSurface('radius.card'),
|
|
178
|
-
pathSurface('typography.fontFamily'),
|
|
179
|
-
...TYPOGRAPHY_SURFACES,
|
|
180
|
-
pathSurface('shape.borderRadius'),
|
|
181
|
-
{
|
|
182
|
-
surface: 'spacing.unit',
|
|
183
|
-
get: (theme) => (typeof theme.spacing === 'function' ? theme.spacing(1) : undefined),
|
|
184
|
-
},
|
|
185
|
-
pathSurface('breakpoints.values'),
|
|
186
|
-
pathSurface('shadow.rest'),
|
|
187
|
-
pathSurface('shadow.overlay'),
|
|
188
|
-
pathSurface('motion.duration.fast'),
|
|
189
|
-
pathSurface('motion.duration.base'),
|
|
190
|
-
pathSurface('motion.duration.overlayEnter'),
|
|
191
|
-
pathSurface('motion.duration.overlayExit'),
|
|
192
|
-
pathSurface('motion.duration.chart'),
|
|
193
|
-
pathSurface('motion.easing.enter'),
|
|
194
|
-
pathSurface('motion.easing.exit'),
|
|
195
|
-
pathSurface('motion.easing.state'),
|
|
196
|
-
pathSurface('motion.reduced'),
|
|
197
|
-
pathSurface('fontLoading.family'),
|
|
198
|
-
pathSurface('fontLoading.weights'),
|
|
199
|
-
...COMPONENT_SURFACES,
|
|
200
|
-
];
|
|
201
|
-
|
|
202
|
-
function parseColour(colour) {
|
|
203
|
-
if (typeof colour !== 'string') return null;
|
|
204
|
-
const value = colour.trim();
|
|
205
|
-
if (value.startsWith('#')) {
|
|
206
|
-
const hex = value.slice(1);
|
|
207
|
-
const expanded = hex.length === 3
|
|
208
|
-
? hex.split('').map((part) => part + part).join('')
|
|
209
|
-
: hex;
|
|
210
|
-
if (!/^[\da-f]{6}([\da-f]{2})?$/i.test(expanded)) return null;
|
|
211
|
-
return {
|
|
212
|
-
r: parseInt(expanded.slice(0, 2), 16),
|
|
213
|
-
g: parseInt(expanded.slice(2, 4), 16),
|
|
214
|
-
b: parseInt(expanded.slice(4, 6), 16),
|
|
215
|
-
a: expanded.length === 8 ? parseInt(expanded.slice(6, 8), 16) / 255 : 1,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const match = value.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i);
|
|
220
|
-
if (!match) return null;
|
|
221
|
-
return {
|
|
222
|
-
r: Number(match[1]),
|
|
223
|
-
g: Number(match[2]),
|
|
224
|
-
b: Number(match[3]),
|
|
225
|
-
a: match[4] === undefined ? 1 : Number(match[4]),
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function composite(foreground, background) {
|
|
230
|
-
const alpha = foreground.a + background.a * (1 - foreground.a);
|
|
231
|
-
if (alpha === 0) return { r: 0, g: 0, b: 0, a: 0 };
|
|
232
|
-
return {
|
|
233
|
-
r: (foreground.r * foreground.a + background.r * background.a * (1 - foreground.a)) / alpha,
|
|
234
|
-
g: (foreground.g * foreground.a + background.g * background.a * (1 - foreground.a)) / alpha,
|
|
235
|
-
b: (foreground.b * foreground.a + background.b * background.a * (1 - foreground.a)) / alpha,
|
|
236
|
-
a: alpha,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function luminance(colour) {
|
|
241
|
-
const channels = [colour.r, colour.g, colour.b].map((channel) => {
|
|
242
|
-
const value = channel / 255;
|
|
243
|
-
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
244
|
-
});
|
|
245
|
-
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** Calculates WCAG contrast, including alpha compositing over the background. */
|
|
249
|
-
export function calculateContrastRatio(foreground, background) {
|
|
250
|
-
const foregroundColour = parseColour(foreground);
|
|
251
|
-
const backgroundColour = parseColour(background);
|
|
252
|
-
if (!foregroundColour || !backgroundColour) {
|
|
253
|
-
try {
|
|
254
|
-
const ratio = getMuiContrastRatio(foreground, background);
|
|
255
|
-
return Number.isFinite(ratio) ? ratio : Number.NaN;
|
|
256
|
-
} catch {
|
|
257
|
-
return Number.NaN;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const opaqueBackground = backgroundColour.a < 1
|
|
262
|
-
? composite(backgroundColour, { r: 255, g: 255, b: 255, a: 1 })
|
|
263
|
-
: backgroundColour;
|
|
264
|
-
const opaqueForeground = foregroundColour.a < 1
|
|
265
|
-
? composite(foregroundColour, opaqueBackground)
|
|
266
|
-
: foregroundColour;
|
|
267
|
-
const light = Math.max(luminance(opaqueForeground), luminance(opaqueBackground));
|
|
268
|
-
const dark = Math.min(luminance(opaqueForeground), luminance(opaqueBackground));
|
|
269
|
-
return (light + 0.05) / (dark + 0.05);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function contrastFindings(theme) {
|
|
273
|
-
const findings = [];
|
|
274
|
-
for (const status of STATUS_KEYS) {
|
|
275
|
-
const tokens = theme.palette?.[status];
|
|
276
|
-
if (!tokens) continue;
|
|
277
|
-
const textRatio = calculateContrastRatio(tokens.text, tokens.bg);
|
|
278
|
-
if (!Number.isFinite(textRatio) || textRatio < 4.5) {
|
|
279
|
-
findings.push({
|
|
280
|
-
surface: `contrast.${status}.text-on-bg`,
|
|
281
|
-
reason: Number.isFinite(textRatio)
|
|
282
|
-
? `Expected at least 4.5:1; received ${textRatio.toFixed(2)}:1.`
|
|
283
|
-
: 'Contrast could not be calculated from the declared colours.',
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
const fillRatio = calculateContrastRatio(tokens.fillText, tokens.fill);
|
|
287
|
-
if (!Number.isFinite(fillRatio) || fillRatio < 4.5) {
|
|
288
|
-
findings.push({
|
|
289
|
-
surface: `contrast.${status}.fillText-on-fill`,
|
|
290
|
-
reason: Number.isFinite(fillRatio)
|
|
291
|
-
? `Expected at least 4.5:1; received ${fillRatio.toFixed(2)}:1.`
|
|
292
|
-
: 'Contrast could not be calculated from the declared colours.',
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const dark = theme.palette?.mode === 'dark';
|
|
298
|
-
const backgrounds = dark
|
|
299
|
-
? {
|
|
300
|
-
paper: theme.palette?.background?.paper,
|
|
301
|
-
page: theme.palette?.background?.default,
|
|
302
|
-
}
|
|
303
|
-
: {
|
|
304
|
-
white: '#FFFFFF',
|
|
305
|
-
page: theme.palette?.background?.default,
|
|
306
|
-
};
|
|
307
|
-
for (const status of STATUS_KEYS) {
|
|
308
|
-
const main = theme.palette?.[status]?.main;
|
|
309
|
-
if (!main) continue;
|
|
310
|
-
for (const [surface, background] of Object.entries(backgrounds)) {
|
|
311
|
-
const ratio = calculateContrastRatio(main, background);
|
|
312
|
-
if (!Number.isFinite(ratio) || ratio < 4.5) {
|
|
313
|
-
findings.push({
|
|
314
|
-
surface: `contrast.${status}.main-on-${surface}`,
|
|
315
|
-
reason: Number.isFinite(ratio)
|
|
316
|
-
? `Expected at least 4.5:1; received ${ratio.toFixed(2)}:1.`
|
|
317
|
-
: 'Contrast could not be calculated from the declared colours.',
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
for (const state of ['main', 'hover', 'focus', 'error']) {
|
|
324
|
-
const colour = theme.palette?.controlBorder?.[state];
|
|
325
|
-
if (!colour) continue;
|
|
326
|
-
for (const [surface, background] of Object.entries(backgrounds)) {
|
|
327
|
-
const ratio = calculateContrastRatio(colour, background);
|
|
328
|
-
if (!Number.isFinite(ratio) || ratio < 3) {
|
|
329
|
-
findings.push({
|
|
330
|
-
surface: `contrast.controlBorder.${state}-on-${surface}`,
|
|
331
|
-
reason: Number.isFinite(ratio)
|
|
332
|
-
? `Expected at least 3:1; received ${ratio.toFixed(2)}:1.`
|
|
333
|
-
: 'Contrast could not be calculated from the declared colours.',
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return findings;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Returns completeness findings. Adopting applications make this a hard CI
|
|
343
|
-
* assertion in their own suite; non-adopters may call it for reporting only.
|
|
344
|
-
*/
|
|
345
|
-
export function assertThemeComplete(theme, { exemptions = [] } = {}) {
|
|
346
|
-
const declaredExemptions = [
|
|
347
|
-
...(theme?.themeCompleteness?.exemptions || []),
|
|
348
|
-
...exemptions,
|
|
349
|
-
];
|
|
350
|
-
const validExemptions = new Set();
|
|
351
|
-
const findings = [];
|
|
352
|
-
|
|
353
|
-
for (const exemption of declaredExemptions) {
|
|
354
|
-
if (!exemption?.surface || !exemption?.reason?.trim()) {
|
|
355
|
-
findings.push({
|
|
356
|
-
surface: `exemption.${exemption?.surface || 'unknown'}`,
|
|
357
|
-
reason: 'A completeness exemption must name a surface and include a reason.',
|
|
358
|
-
});
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
validExemptions.add(exemption.surface);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
for (const descriptor of THEME_COMPLETENESS_SURFACES) {
|
|
365
|
-
const value = descriptor.get(theme);
|
|
366
|
-
const defaultValue = descriptor.get(MUI_DEFAULT_THEME);
|
|
367
|
-
if ((value === undefined || sameValue(value, defaultValue)) && !validExemptions.has(descriptor.surface)) {
|
|
368
|
-
findings.push({
|
|
369
|
-
surface: descriptor.surface,
|
|
370
|
-
reason: value === undefined
|
|
371
|
-
? 'The surface is not deliberately defined.'
|
|
372
|
-
: 'The surface still equals MUI\'s untouched default.',
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
findings.push(...contrastFindings(theme));
|
|
378
|
-
return { findings };
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const SAME_ROOT_STYLE_OVERRIDE_SLOTS = {
|
|
382
|
-
MuiAlert: ['standardSuccess', 'standardWarning', 'standardError', 'standardInfo'],
|
|
383
|
-
MuiButton: ['root', 'contained', 'outlined'],
|
|
384
|
-
MuiChip: ['root', 'outlined'],
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
// MUI's sx prop accepts aliases and CSS spacing shorthands that
|
|
388
|
-
// styleOverrides' plain CSS-in-JS objects do not. Each known alias is
|
|
389
|
-
// expanded to the canonical CSS properties it can affect so the
|
|
390
|
-
// disjointness check compares effects, not raw key spellings; a property
|
|
391
|
-
// outside this table expands to itself.
|
|
392
|
-
const SPACING_SHORTHAND_LONGHANDS = {
|
|
393
|
-
bgcolor: ['backgroundColor'],
|
|
394
|
-
typography: [
|
|
395
|
-
'fontFamily',
|
|
396
|
-
'fontSize',
|
|
397
|
-
'fontStyle',
|
|
398
|
-
'fontWeight',
|
|
399
|
-
'letterSpacing',
|
|
400
|
-
'lineHeight',
|
|
401
|
-
'textTransform',
|
|
402
|
-
],
|
|
403
|
-
p: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
|
|
404
|
-
padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
|
|
405
|
-
px: ['paddingLeft', 'paddingRight'],
|
|
406
|
-
paddingX: ['paddingLeft', 'paddingRight'],
|
|
407
|
-
py: ['paddingTop', 'paddingBottom'],
|
|
408
|
-
paddingY: ['paddingTop', 'paddingBottom'],
|
|
409
|
-
pt: ['paddingTop'],
|
|
410
|
-
pr: ['paddingRight'],
|
|
411
|
-
pb: ['paddingBottom'],
|
|
412
|
-
pl: ['paddingLeft'],
|
|
413
|
-
m: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
|
|
414
|
-
margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
|
|
415
|
-
mx: ['marginLeft', 'marginRight'],
|
|
416
|
-
marginX: ['marginLeft', 'marginRight'],
|
|
417
|
-
my: ['marginTop', 'marginBottom'],
|
|
418
|
-
marginY: ['marginTop', 'marginBottom'],
|
|
419
|
-
mt: ['marginTop'],
|
|
420
|
-
mr: ['marginRight'],
|
|
421
|
-
mb: ['marginBottom'],
|
|
422
|
-
ml: ['marginLeft'],
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
function expandShorthandProperty(property) {
|
|
426
|
-
return SPACING_SHORTHAND_LONGHANDS[property] || [property];
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function styleOverrideKeys(componentConfig, slots = ['root']) {
|
|
430
|
-
const keys = new Set();
|
|
431
|
-
for (const slot of slots) {
|
|
432
|
-
const slotStyles = componentConfig?.styleOverrides?.[slot];
|
|
433
|
-
if (!slotStyles || typeof slotStyles !== 'object' || Array.isArray(slotStyles)) continue;
|
|
434
|
-
Object.keys(slotStyles).forEach((key) => expandShorthandProperty(key).forEach((longhand) => keys.add(longhand)));
|
|
435
|
-
}
|
|
436
|
-
return keys;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Reports top-level property collisions between registered kit sx objects and
|
|
441
|
-
* the baseline MUI component they target. Known sx aliases are compared by
|
|
442
|
-
* the CSS properties they affect, not by literal key name.
|
|
443
|
-
*
|
|
444
|
-
* This is deliberately a lower bound on shadowing: unknown normalisation gaps
|
|
445
|
-
* remain. A clean result proves only that the registered top-level key sets
|
|
446
|
-
* are disjoint; it is not proof that no component can shadow the baseline.
|
|
447
|
-
*/
|
|
448
|
-
export function assertKitSxDisjoint(
|
|
449
|
-
theme,
|
|
450
|
-
{ registry = KIT_COMPONENT_SX_REGISTRY, exemptions = [] } = {},
|
|
451
|
-
) {
|
|
452
|
-
const declaredExemptions = [
|
|
453
|
-
...(theme?.themeCompleteness?.exemptions || []),
|
|
454
|
-
...exemptions,
|
|
455
|
-
];
|
|
456
|
-
const validExemptions = new Set();
|
|
457
|
-
const findings = [];
|
|
458
|
-
|
|
459
|
-
for (const exemption of declaredExemptions) {
|
|
460
|
-
if (!exemption?.surface || !exemption?.reason?.trim()) {
|
|
461
|
-
findings.push({
|
|
462
|
-
surface: `exemption.${exemption?.surface || 'unknown'}`,
|
|
463
|
-
reason: 'A completeness exemption must name a surface and include a reason.',
|
|
464
|
-
});
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
validExemptions.add(exemption.surface);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
for (const entry of registry) {
|
|
471
|
-
const baselineKeys = styleOverrideKeys(
|
|
472
|
-
theme?.components?.[entry.muiComponent],
|
|
473
|
-
entry.slots || SAME_ROOT_STYLE_OVERRIDE_SLOTS[entry.muiComponent],
|
|
474
|
-
);
|
|
475
|
-
for (const property of Object.keys(entry.sx || {})) {
|
|
476
|
-
const surface = `${entry.component}.${entry.muiComponent}.${property}`;
|
|
477
|
-
const collides = expandShorthandProperty(property).some((longhand) => baselineKeys.has(longhand));
|
|
478
|
-
if (collides && !validExemptions.has(surface)) {
|
|
479
|
-
findings.push({
|
|
480
|
-
surface,
|
|
481
|
-
reason: `${entry.component} sx shadows ${entry.muiComponent} styleOverrides property "${property}".`,
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
return { findings };
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const BASELINE_STYLED_MUI_COMPONENTS = [
|
|
491
|
-
'Alert',
|
|
492
|
-
'BottomNavigation',
|
|
493
|
-
'BottomNavigationAction',
|
|
494
|
-
'Button',
|
|
495
|
-
'Card',
|
|
496
|
-
'Checkbox',
|
|
497
|
-
'Chip',
|
|
498
|
-
'Container',
|
|
499
|
-
'CssBaseline',
|
|
500
|
-
'Dialog',
|
|
501
|
-
'Divider',
|
|
502
|
-
'Drawer',
|
|
503
|
-
'FilledInput',
|
|
504
|
-
'FormControlLabel',
|
|
505
|
-
'IconButton',
|
|
506
|
-
'Input',
|
|
507
|
-
'Menu',
|
|
508
|
-
'OutlinedInput',
|
|
509
|
-
'Paper',
|
|
510
|
-
'Select',
|
|
511
|
-
'TableCell',
|
|
512
|
-
'TextField',
|
|
513
|
-
'Tooltip',
|
|
514
|
-
];
|
|
515
|
-
|
|
516
|
-
function jsxOpeningTag(source, start) {
|
|
517
|
-
let braces = 0;
|
|
518
|
-
let quote = null;
|
|
519
|
-
for (let index = start; index < source.length; index += 1) {
|
|
520
|
-
const character = source[index];
|
|
521
|
-
if (quote) {
|
|
522
|
-
if (character === quote && source[index - 1] !== '\\') quote = null;
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
if (character === '"' || character === "'" || character === '`') {
|
|
526
|
-
quote = character;
|
|
527
|
-
} else if (character === '{') {
|
|
528
|
-
braces += 1;
|
|
529
|
-
} else if (character === '}') {
|
|
530
|
-
braces = Math.max(0, braces - 1);
|
|
531
|
-
} else if (character === '>' && braces === 0) {
|
|
532
|
-
return source.slice(start, index + 1);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
return source.slice(start);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
function topLevelSxValue(tag) {
|
|
539
|
-
let braces = 0;
|
|
540
|
-
let quote = null;
|
|
541
|
-
for (let index = 0; index < tag.length; index += 1) {
|
|
542
|
-
const character = tag[index];
|
|
543
|
-
if (quote) {
|
|
544
|
-
if (character === quote && tag[index - 1] !== '\\') quote = null;
|
|
545
|
-
continue;
|
|
546
|
-
}
|
|
547
|
-
if (character === '"' || character === "'" || character === '`') {
|
|
548
|
-
quote = character;
|
|
549
|
-
continue;
|
|
550
|
-
}
|
|
551
|
-
if (braces === 0 && (index === 0 || /\s/.test(tag[index - 1]))) {
|
|
552
|
-
const attribute = tag.slice(index).match(/^sx\b\s*=\s*/)?.[0];
|
|
553
|
-
if (attribute) return tag.slice(index + attribute.length);
|
|
554
|
-
}
|
|
555
|
-
if (character === '{') braces += 1;
|
|
556
|
-
if (character === '}') braces = Math.max(0, braces - 1);
|
|
557
|
-
}
|
|
558
|
-
return '';
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* Reports baseline-styled MUI tags that bypass the exported-object convention.
|
|
563
|
-
* Like assertKitSxDisjoint, this is a documented lower bound: conditional,
|
|
564
|
-
* array, callback, and otherwise nested sx expressions are not inspected.
|
|
565
|
-
*/
|
|
566
|
-
export function reportKitSxBypasses(sources = [], { registry } = {}) {
|
|
567
|
-
const normalized = sources.map((entry, index) =>
|
|
568
|
-
typeof entry === 'string'
|
|
569
|
-
? { path: `source-${index + 1}`, source: entry }
|
|
570
|
-
: { path: entry.path || `source-${index + 1}`, source: entry.source || '' },
|
|
571
|
-
);
|
|
572
|
-
const findings = [];
|
|
573
|
-
const registeredTargets = registry
|
|
574
|
-
? registry.reduce((targets, { exportName, muiComponent }) => {
|
|
575
|
-
if (!exportName || !muiComponent) return targets;
|
|
576
|
-
if (!targets.has(exportName)) targets.set(exportName, new Set());
|
|
577
|
-
targets.get(exportName).add(muiComponent);
|
|
578
|
-
return targets;
|
|
579
|
-
}, new Map())
|
|
580
|
-
: null;
|
|
581
|
-
const componentPattern = BASELINE_STYLED_MUI_COMPONENTS.join('|');
|
|
582
|
-
const tagPattern = new RegExp(`<(${componentPattern})\\b`, 'g');
|
|
583
|
-
|
|
584
|
-
for (const { path, source } of normalized) {
|
|
585
|
-
const exportedObjects = new Set(
|
|
586
|
-
[...source.matchAll(/\bexport\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*\{/g)]
|
|
587
|
-
.map((match) => match[1]),
|
|
588
|
-
);
|
|
589
|
-
|
|
590
|
-
for (const match of source.matchAll(tagPattern)) {
|
|
591
|
-
const tag = jsxOpeningTag(source, match.index);
|
|
592
|
-
const sxValue = topLevelSxValue(tag);
|
|
593
|
-
const inline = /^\{\s*\{/.test(sxValue);
|
|
594
|
-
const identifier = sxValue.match(/^\{\s*([A-Za-z_$][\w$]*)\s*\}/)?.[1];
|
|
595
|
-
const exported = identifier && exportedObjects.has(identifier);
|
|
596
|
-
const muiComponent = `Mui${match[1]}`;
|
|
597
|
-
const unregistered = exported
|
|
598
|
-
&& registeredTargets
|
|
599
|
-
&& !registeredTargets.get(identifier)?.has(muiComponent);
|
|
600
|
-
if (!inline && (!identifier || (exported && !unregistered))) continue;
|
|
601
|
-
|
|
602
|
-
const line = source.slice(0, match.index).split(/\r?\n/).length;
|
|
603
|
-
findings.push({
|
|
604
|
-
surface: `${path}:${line}.Mui${match[1]}.sx`,
|
|
605
|
-
reason: inline
|
|
606
|
-
? `${match[1]} uses an inline sx object instead of a top-level exported object.`
|
|
607
|
-
: unregistered
|
|
608
|
-
? `${match[1]} uses exported sx object "${identifier}" without a ${muiComponent} registry entry.`
|
|
609
|
-
: `${match[1]} uses non-exported sx object "${identifier}".`,
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
return { findings };
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const BASIC_CSS_COLOUR_NAMES = new Set([
|
|
618
|
-
'aqua',
|
|
619
|
-
'black',
|
|
620
|
-
'blue',
|
|
621
|
-
'fuchsia',
|
|
622
|
-
'gray',
|
|
623
|
-
'green',
|
|
624
|
-
'grey',
|
|
625
|
-
'lime',
|
|
626
|
-
'maroon',
|
|
627
|
-
'navy',
|
|
628
|
-
'olive',
|
|
629
|
-
'purple',
|
|
630
|
-
'red',
|
|
631
|
-
'silver',
|
|
632
|
-
'teal',
|
|
633
|
-
'white',
|
|
634
|
-
'yellow',
|
|
635
|
-
]);
|
|
636
|
-
|
|
637
|
-
const MUI_DEFAULT_NUMERIC_RAMP_NAMES = new Set([
|
|
638
|
-
'amber',
|
|
639
|
-
'blue',
|
|
640
|
-
'bluegrey',
|
|
641
|
-
'brown',
|
|
642
|
-
'cyan',
|
|
643
|
-
'deeporange',
|
|
644
|
-
'deeppurple',
|
|
645
|
-
'green',
|
|
646
|
-
'grey',
|
|
647
|
-
'indigo',
|
|
648
|
-
'lightblue',
|
|
649
|
-
'lightgreen',
|
|
650
|
-
'lime',
|
|
651
|
-
'orange',
|
|
652
|
-
'pink',
|
|
653
|
-
'purple',
|
|
654
|
-
'red',
|
|
655
|
-
'teal',
|
|
656
|
-
'yellow',
|
|
657
|
-
]);
|
|
658
|
-
|
|
659
|
-
const HEX_COLOUR_PATTERN = /#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})(?![\da-f])/gi;
|
|
660
|
-
const RGB_COLOUR_PATTERN = /rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+(?:\s*,\s*[\d.]+)?\s*\)/gi;
|
|
661
|
-
const NAMED_COLOUR_PATTERN = new RegExp(
|
|
662
|
-
`\\b(?:${[...BASIC_CSS_COLOUR_NAMES].join('|')})\\b`,
|
|
663
|
-
'gi',
|
|
664
|
-
);
|
|
665
|
-
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*$/;
|
|
666
|
-
|
|
667
|
-
function colourKey(value) {
|
|
668
|
-
const colour = parseColour(value);
|
|
669
|
-
return colour ? `${colour.r},${colour.g},${colour.b},${colour.a}` : null;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
function collectBaselineColourKeys(...roots) {
|
|
673
|
-
const keys = new Set();
|
|
674
|
-
const visit = (value) => {
|
|
675
|
-
if (typeof value === 'string') {
|
|
676
|
-
for (const match of value.matchAll(HEX_COLOUR_PATTERN)) {
|
|
677
|
-
const key = colourKey(match[0]);
|
|
678
|
-
if (key) keys.add(key);
|
|
679
|
-
}
|
|
680
|
-
for (const match of value.matchAll(RGB_COLOUR_PATTERN)) {
|
|
681
|
-
const key = colourKey(match[0]);
|
|
682
|
-
if (key) keys.add(key);
|
|
683
|
-
}
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
if (Array.isArray(value)) {
|
|
687
|
-
value.forEach(visit);
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
if (value && typeof value === 'object') Object.values(value).forEach(visit);
|
|
691
|
-
};
|
|
692
|
-
roots.forEach(visit);
|
|
693
|
-
return keys;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function maskComments(source) {
|
|
697
|
-
const characters = [...source];
|
|
698
|
-
let quote = null;
|
|
699
|
-
for (let index = 0; index < characters.length; index += 1) {
|
|
700
|
-
const character = characters[index];
|
|
701
|
-
if (quote) {
|
|
702
|
-
if (character === quote && characters[index - 1] !== '\\') quote = null;
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
if (character === '"' || character === "'" || character === '`') {
|
|
706
|
-
quote = character;
|
|
707
|
-
continue;
|
|
708
|
-
}
|
|
709
|
-
if (character === '/' && characters[index + 1] === '/') {
|
|
710
|
-
for (; index < characters.length && characters[index] !== '\n'; index += 1) {
|
|
711
|
-
characters[index] = ' ';
|
|
712
|
-
}
|
|
713
|
-
index -= 1;
|
|
714
|
-
} else if (character === '/' && characters[index + 1] === '*') {
|
|
715
|
-
characters[index] = ' ';
|
|
716
|
-
characters[index + 1] = ' ';
|
|
717
|
-
index += 2;
|
|
718
|
-
while (index < characters.length
|
|
719
|
-
&& !(characters[index] === '*' && characters[index + 1] === '/')) {
|
|
720
|
-
if (characters[index] !== '\n' && characters[index] !== '\r') characters[index] = ' ';
|
|
721
|
-
index += 1;
|
|
722
|
-
}
|
|
723
|
-
if (index < characters.length) {
|
|
724
|
-
characters[index] = ' ';
|
|
725
|
-
characters[index + 1] = ' ';
|
|
726
|
-
index += 1;
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
return characters.join('');
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function maskQrPrintDocument(source, path) {
|
|
734
|
-
if (!path.replaceAll('\\', '/').endsWith('/QrSignupManager.jsx')) return source;
|
|
735
|
-
const call = /\bprintWindow\.document\.write\s*\(\s*`/g.exec(source);
|
|
736
|
-
if (!call) return source;
|
|
737
|
-
const start = call.index + call[0].lastIndexOf('`');
|
|
738
|
-
let end = start + 1;
|
|
739
|
-
while (end < source.length) {
|
|
740
|
-
if (source[end] === '`' && source[end - 1] !== '\\') break;
|
|
741
|
-
end += 1;
|
|
742
|
-
}
|
|
743
|
-
if (end >= source.length) return source;
|
|
744
|
-
|
|
745
|
-
// The generated print document is standalone HTML rendered outside the
|
|
746
|
-
// React tree, where no application theme is available. Mask the region as
|
|
747
|
-
// one unit instead of accumulating permanent per-colour exemptions.
|
|
748
|
-
return source.slice(0, start)
|
|
749
|
-
+ source.slice(start, end + 1).replace(/[^\r\n]/g, ' ')
|
|
750
|
-
+ source.slice(end + 1);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function normalizedSources(sources) {
|
|
754
|
-
return sources.map((entry, index) =>
|
|
755
|
-
typeof entry === 'string'
|
|
756
|
-
? { path: `source-${index + 1}`, source: entry }
|
|
757
|
-
: { path: entry.path || `source-${index + 1}`, source: entry.source || '' },
|
|
758
|
-
);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
/**
|
|
762
|
-
* Reports colour literals in component source that do not come from the
|
|
763
|
-
* baseline palette, plus report-only uses of MUI's untouched numeric ramps.
|
|
764
|
-
*/
|
|
765
|
-
export function reportOffPaletteColours(
|
|
766
|
-
sources = [],
|
|
767
|
-
{ palette = BASELINE_PALETTE, baselineStatic = BASELINE_STATIC } = {},
|
|
768
|
-
) {
|
|
769
|
-
const allowedColours = collectBaselineColourKeys(palette, baselineStatic);
|
|
770
|
-
const findings = [];
|
|
771
|
-
const stringPattern = /(["'`])((?:\\.|(?!\1)[\s\S])*)\1/g;
|
|
772
|
-
|
|
773
|
-
for (const { path, source } of normalizedSources(sources)) {
|
|
774
|
-
const normalizedPath = path.replaceAll('\\', '/');
|
|
775
|
-
if (/(^|\/)src\/theme\//.test(normalizedPath)) continue;
|
|
776
|
-
const scanSource = maskComments(maskQrPrintDocument(source, normalizedPath));
|
|
777
|
-
|
|
778
|
-
for (const match of scanSource.matchAll(stringPattern)) {
|
|
779
|
-
const value = match[2].trim();
|
|
780
|
-
const line = source.slice(0, match.index).split(/\r?\n/).length;
|
|
781
|
-
const surface = `${path}:${line}.colour`;
|
|
782
|
-
|
|
783
|
-
for (const hex of value.matchAll(HEX_COLOUR_PATTERN)) {
|
|
784
|
-
if (!allowedColours.has(colourKey(hex[0]))) {
|
|
785
|
-
findings.push({
|
|
786
|
-
surface,
|
|
787
|
-
reason: `Off-palette hex colour "${hex[0]}" is not derived from the baseline palette.`,
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
for (const rgb of value.matchAll(RGB_COLOUR_PATTERN)) {
|
|
793
|
-
if (!allowedColours.has(colourKey(rgb[0]))) {
|
|
794
|
-
findings.push({
|
|
795
|
-
surface,
|
|
796
|
-
reason: `Off-palette rgb colour "${rgb[0]}" is not derived from the baseline palette.`,
|
|
797
|
-
});
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const before = scanSource.slice(Math.max(0, match.index - 40), match.index);
|
|
802
|
-
const isColourProperty = COLOUR_PROPERTY_PATTERN.test(before);
|
|
803
|
-
const numericRamp = value.match(/^([a-z]+)\.(\d+)$/i);
|
|
804
|
-
if (isColourProperty && !numericRamp) {
|
|
805
|
-
for (const namedColour of value.matchAll(NAMED_COLOUR_PATTERN)) {
|
|
806
|
-
findings.push({
|
|
807
|
-
surface,
|
|
808
|
-
reason: `Named CSS colour "${namedColour[0]}" bypasses the baseline palette.`,
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
if (numericRamp && MUI_DEFAULT_NUMERIC_RAMP_NAMES.has(numericRamp[1].toLowerCase())) {
|
|
814
|
-
findings.push({
|
|
815
|
-
surface,
|
|
816
|
-
reason: `Report only: "${value}" uses MUI's untouched numeric palette ramp, not a baseline token.`,
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
return { findings };
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
function balancedBraceRegion(text, start) {
|
|
826
|
-
let depth = 0;
|
|
827
|
-
let quote = null;
|
|
828
|
-
for (let index = start; index < text.length; index += 1) {
|
|
829
|
-
const character = text[index];
|
|
830
|
-
if (quote) {
|
|
831
|
-
if (character === quote && text[index - 1] !== '\\') quote = null;
|
|
832
|
-
continue;
|
|
833
|
-
}
|
|
834
|
-
if (character === '"' || character === "'" || character === '`') {
|
|
835
|
-
quote = character;
|
|
836
|
-
continue;
|
|
837
|
-
}
|
|
838
|
-
if (character === '{') depth += 1;
|
|
839
|
-
else if (character === '}') {
|
|
840
|
-
depth -= 1;
|
|
841
|
-
if (depth === 0) return text.slice(start, index + 1);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
return text.slice(start);
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
function topLevelObjectEntries(objectText) {
|
|
848
|
-
const inner = objectText.slice(1, -1);
|
|
849
|
-
const entries = [];
|
|
850
|
-
let depth = 0;
|
|
851
|
-
let quote = null;
|
|
852
|
-
let entryStart = 0;
|
|
853
|
-
const push = (raw) => {
|
|
854
|
-
const trimmed = raw.trim();
|
|
855
|
-
if (trimmed) entries.push(trimmed);
|
|
856
|
-
};
|
|
857
|
-
for (let index = 0; index < inner.length; index += 1) {
|
|
858
|
-
const character = inner[index];
|
|
859
|
-
if (quote) {
|
|
860
|
-
if (character === quote && inner[index - 1] !== '\\') quote = null;
|
|
861
|
-
continue;
|
|
862
|
-
}
|
|
863
|
-
if (character === '"' || character === "'" || character === '`') {
|
|
864
|
-
quote = character;
|
|
865
|
-
continue;
|
|
866
|
-
}
|
|
867
|
-
if (character === '{' || character === '[' || character === '(') depth += 1;
|
|
868
|
-
else if (character === '}' || character === ']' || character === ')') depth -= 1;
|
|
869
|
-
else if (character === ',' && depth === 0) {
|
|
870
|
-
push(inner.slice(entryStart, index));
|
|
871
|
-
entryStart = index + 1;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
push(inner.slice(entryStart));
|
|
875
|
-
return entries;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function splitSxEntry(entry) {
|
|
879
|
-
let depth = 0;
|
|
880
|
-
let quote = null;
|
|
881
|
-
for (let index = 0; index < entry.length; index += 1) {
|
|
882
|
-
const character = entry[index];
|
|
883
|
-
if (quote) {
|
|
884
|
-
if (character === quote && entry[index - 1] !== '\\') quote = null;
|
|
885
|
-
continue;
|
|
886
|
-
}
|
|
887
|
-
if (character === '"' || character === "'" || character === '`') {
|
|
888
|
-
quote = character;
|
|
889
|
-
continue;
|
|
890
|
-
}
|
|
891
|
-
if (character === '{' || character === '[' || character === '(') depth += 1;
|
|
892
|
-
else if (character === '}' || character === ']' || character === ')') depth -= 1;
|
|
893
|
-
else if (character === ':' && depth === 0) {
|
|
894
|
-
return { rawKey: entry.slice(0, index).trim(), rawValue: entry.slice(index + 1).trim() };
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
return null;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
const SIMPLE_IDENTIFIER_PATTERN = /^[A-Za-z_$][\w$]*$/;
|
|
901
|
-
const QUOTED_STRING_PATTERN = /^(['"])((?:\\.|(?!\1)[\s\S])*)\1$/;
|
|
902
|
-
const NUMERIC_LITERAL_PATTERN = /^-?\d+(?:\.\d+)?$/;
|
|
903
|
-
|
|
904
|
-
/**
|
|
905
|
-
* Reads a top-level `sx={{ ... }}` object literal on a baseline-styled tag
|
|
906
|
-
* and returns its simple `{ property, value }` entries: identifier keys only
|
|
907
|
-
* (a quoted key is a nested selector like `'&:hover'` and is skipped, not a
|
|
908
|
-
* property), values that are a plain quoted string or bare number only. A
|
|
909
|
-
* template literal, identifier reference, spread, or function call is out of
|
|
910
|
-
* scope by design -- this is a lower bound, not a JS evaluator.
|
|
911
|
-
*/
|
|
912
|
-
function literalSxEntries(sxValueText) {
|
|
913
|
-
if (!/^\{\s*\{/.test(sxValueText)) return [];
|
|
914
|
-
const objectText = balancedBraceRegion(sxValueText, sxValueText.indexOf('{', 1));
|
|
915
|
-
const entries = [];
|
|
916
|
-
for (const rawEntry of topLevelObjectEntries(objectText)) {
|
|
917
|
-
const split = splitSxEntry(rawEntry);
|
|
918
|
-
if (!split) continue;
|
|
919
|
-
const { rawKey, rawValue } = split;
|
|
920
|
-
if (!SIMPLE_IDENTIFIER_PATTERN.test(rawKey)) continue;
|
|
921
|
-
|
|
922
|
-
const quoted = rawValue.match(QUOTED_STRING_PATTERN);
|
|
923
|
-
if (quoted) {
|
|
924
|
-
entries.push({ property: rawKey, value: quoted[2] });
|
|
925
|
-
continue;
|
|
926
|
-
}
|
|
927
|
-
if (NUMERIC_LITERAL_PATTERN.test(rawValue)) {
|
|
928
|
-
entries.push({ property: rawKey, value: Number(rawValue) });
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
return entries;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
// MUI's sx system multiplies a bare NUMBER by a theme scale factor for these
|
|
935
|
-
// properties (`theme.shape.borderRadius` for borderRadius, `theme.spacing()`
|
|
936
|
-
// for every spacing alias) before it ever reaches the DOM -- a raw
|
|
937
|
-
// `styleOverrides.root` value is already-resolved CSS, unscaled. Comparing
|
|
938
|
-
// the two numbers directly would be a false positive (e.g. `borderRadius: 8`
|
|
939
|
-
// in sx renders as `theme.shape.borderRadius * 8`, not `8`), and false
|
|
940
|
-
// positives are the failure mode this whole check exists to avoid. Numeric
|
|
941
|
-
// literals on these properties are therefore skipped, not compared -- a
|
|
942
|
-
// false negative (this check's accepted lower bound) is the safe direction
|
|
943
|
-
// to err in, a false positive is not.
|
|
944
|
-
const MUI_NUMERIC_SCALE_PROPERTIES = new Set([
|
|
945
|
-
'borderRadius',
|
|
946
|
-
...Object.keys(SPACING_SHORTHAND_LONGHANDS).filter((key) => key !== 'bgcolor' && key !== 'typography'),
|
|
947
|
-
]);
|
|
948
|
-
|
|
949
|
-
/**
|
|
950
|
-
* Resolves what an sx value would actually render as, for comparison against
|
|
951
|
-
* a theme styleOverride value already resolved to a real CSS value. A string
|
|
952
|
-
* sx value is tried as a `theme.palette.<value>` token path first (MUI's own
|
|
953
|
-
* resolution for palette-aware sx properties, e.g. `borderColor: 'divider'`
|
|
954
|
-
* -> `theme.palette.divider`); if that path is undefined, the raw string is
|
|
955
|
-
* used as-is (a plain CSS keyword like `'none'` is not a token path).
|
|
956
|
-
*/
|
|
957
|
-
function resolveSxValue(theme, value) {
|
|
958
|
-
if (typeof value !== 'string') return value;
|
|
959
|
-
const paletteValue = getPath(theme, `palette.${value}`);
|
|
960
|
-
return paletteValue !== undefined ? paletteValue : value;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
/**
|
|
964
|
-
* Reports an app's own `sx` value that already equals what its theme
|
|
965
|
-
* resolves for that MUI component and property -- not "you overrode the
|
|
966
|
-
* baseline" (in an app, that is the intended mechanism), but "you re-stated
|
|
967
|
-
* a value you already had", which silently diverges the next time the token
|
|
968
|
-
* changes. Attribution is by JSX tag name against the same
|
|
969
|
-
* `BASELINE_STYLED_MUI_COMPONENTS` list `reportKitSxBypasses` uses, checked
|
|
970
|
-
* against `theme.components.Mui<Tag>.styleOverrides.root.<property>` only
|
|
971
|
-
* (no other slot). Report-only; there is no exemption arm yet -- each
|
|
972
|
-
* adopting app makes this a hard assertion in its own follow-up WO once it
|
|
973
|
-
* has acted on the findings.
|
|
974
|
-
*
|
|
975
|
-
* This is a lower bound, and the bound is open, not enumerated: a `styled()`
|
|
976
|
-
* component, a component rendered through `component={...}` (so its JSX tag
|
|
977
|
-
* name is not the MUI component it becomes), an aliased import
|
|
978
|
-
* (`import { Paper as Surface }`), array/callback/conditional `sx`, and a
|
|
979
|
-
* template-literal or identifier-reference value are all unattributed or
|
|
980
|
-
* unresolved by this method. A numeric `sx` value on a property MUI scales
|
|
981
|
-
* by a theme factor (`borderRadius`, every spacing alias) is deliberately
|
|
982
|
-
* SKIPPED rather than compared -- see `MUI_NUMERIC_SCALE_PROPERTIES` -- since
|
|
983
|
-
* comparing the raw numbers would be a false positive, not a missed finding.
|
|
984
|
-
* A clean result means "nothing found by this method", not "no redundant
|
|
985
|
-
* value exists".
|
|
986
|
-
*/
|
|
987
|
-
export function reportRedundantThemeValues(sources = [], { theme } = {}) {
|
|
988
|
-
const findings = [];
|
|
989
|
-
if (!theme) return { findings };
|
|
990
|
-
const componentPattern = BASELINE_STYLED_MUI_COMPONENTS.join('|');
|
|
991
|
-
const tagPattern = new RegExp(`<(${componentPattern})\\b`, 'g');
|
|
992
|
-
|
|
993
|
-
for (const { path, source } of normalizedSources(sources)) {
|
|
994
|
-
const scanSource = maskComments(maskQrPrintDocument(source, path.replaceAll('\\', '/')));
|
|
995
|
-
|
|
996
|
-
for (const match of scanSource.matchAll(tagPattern)) {
|
|
997
|
-
const tagName = match[1];
|
|
998
|
-
const tag = jsxOpeningTag(scanSource, match.index);
|
|
999
|
-
const sxValueText = topLevelSxValue(tag);
|
|
1000
|
-
const line = source.slice(0, match.index).split(/\r?\n/).length;
|
|
1001
|
-
|
|
1002
|
-
for (const { property, value } of literalSxEntries(sxValueText)) {
|
|
1003
|
-
if (typeof value === 'number' && MUI_NUMERIC_SCALE_PROPERTIES.has(property)) continue;
|
|
1004
|
-
const [canonicalProperty] = expandShorthandProperty(property);
|
|
1005
|
-
const lookupProperty = expandShorthandProperty(property).length === 1
|
|
1006
|
-
? canonicalProperty
|
|
1007
|
-
: property;
|
|
1008
|
-
const themeValue = getPath(
|
|
1009
|
-
theme,
|
|
1010
|
-
`components.Mui${tagName}.styleOverrides.root.${lookupProperty}`,
|
|
1011
|
-
);
|
|
1012
|
-
if (themeValue === undefined) continue;
|
|
1013
|
-
if (sameValue(resolveSxValue(theme, value), themeValue)) {
|
|
1014
|
-
findings.push({
|
|
1015
|
-
surface: `${path}:${line}.Mui${tagName}.${property}`,
|
|
1016
|
-
reason: `Mui${tagName}'s sx sets "${property}" to a value the theme already resolves to the same thing.`,
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
return { findings };
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
/** Reports adoption signals as numbers; it never changes completeness. */
|
|
1027
|
-
export function reportThemeAdoption(sources = []) {
|
|
1028
|
-
const normalized = sources.map((entry, index) =>
|
|
1029
|
-
typeof entry === 'string'
|
|
1030
|
-
? { path: `source-${index + 1}`, source: entry }
|
|
1031
|
-
: { path: entry.path || `source-${index + 1}`, source: entry.source || '' },
|
|
1032
|
-
);
|
|
1033
|
-
const themeBypassPaths = normalized
|
|
1034
|
-
.filter(({ source }) => /\bcreateTheme\s*\(/.test(source) && !/\bcreateAppTheme\s*\(/.test(source))
|
|
1035
|
-
.map(({ path }) => path);
|
|
1036
|
-
const inlineHexCount = normalized.reduce(
|
|
1037
|
-
(count, { source }) => count + (source.match(/#[\da-f]{3,8}\b/gi)?.length || 0),
|
|
1038
|
-
0,
|
|
1039
|
-
);
|
|
1040
|
-
return {
|
|
1041
|
-
inlineHexCount,
|
|
1042
|
-
themeBypassPathCount: themeBypassPaths.length,
|
|
1043
|
-
themeBypassPaths,
|
|
1044
|
-
};
|
|
1045
|
-
}
|