@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.
Files changed (341) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +7 -0
  3. package/dist/chat/kitSxRegistry.js +9 -0
  4. package/dist/components/charts/kitSxRegistry.js +6 -0
  5. package/dist/entries/charts.js +6 -0
  6. package/dist/entries/chat.js +8 -0
  7. package/dist/entries/markdown.js +1 -0
  8. package/dist/index.js +0 -14
  9. package/dist/pages/AccountPage.js +1 -2
  10. package/dist/pages/LoginPage.js +1 -2
  11. package/dist/pages/PasswordChangePage.js +1 -2
  12. package/dist/pages/PasswordInvitePage.js +1 -2
  13. package/dist/pages/PasswordResetRequestPage.js +1 -2
  14. package/dist/pages/SignUpPage.js +1 -2
  15. package/dist/pages/SignupConfirmPage.js +1 -2
  16. package/dist/theme/kitSxEntry.js +11 -0
  17. package/dist/theme/kitSxRegistry.js +2 -15
  18. package/package.json +32 -7
  19. package/.github/workflows/ci.yml +0 -35
  20. package/.github/workflows/publish.yml +0 -126
  21. package/DESIGN.md +0 -60
  22. package/WORK_ORDERS.md +0 -150
  23. package/dev/entries.jsx +0 -721
  24. package/dev/fixtures.js +0 -26
  25. package/dev/main.jsx +0 -75
  26. package/dev/mockTransport.jsx +0 -128
  27. package/docs/CHART-LAYOUT.md +0 -157
  28. package/docs/DARK-TOKENS.md +0 -140
  29. package/docs/TYPE-SCALE.md +0 -111
  30. package/docs/messaging-deviations.md +0 -118
  31. package/index.html +0 -12
  32. package/renovate.json +0 -10
  33. package/scripts/chart-api-census.mjs +0 -415
  34. package/src/auth/AuthContext.jsx +0 -158
  35. package/src/auth/ReauthModal.jsx +0 -82
  36. package/src/auth/UserMenu.jsx +0 -121
  37. package/src/auth/apiClient.jsx +0 -232
  38. package/src/auth/authApi.jsx +0 -539
  39. package/src/auth/authConfig.jsx +0 -34
  40. package/src/auth/reauth.js +0 -47
  41. package/src/chat/ChatComposer.jsx +0 -36
  42. package/src/chat/ChatList.jsx +0 -87
  43. package/src/chat/ChatThread.jsx +0 -128
  44. package/src/chat/ChatWorkspace.jsx +0 -244
  45. package/src/chat/MarkdownReply.jsx +0 -22
  46. package/src/chat/ModelPicker.jsx +0 -85
  47. package/src/chat/api.js +0 -18
  48. package/src/chat/useChatSocket.js +0 -32
  49. package/src/chat/wire.js +0 -7
  50. package/src/components/AccessCodeManager.jsx +0 -288
  51. package/src/components/AccessCodeSingleUseToggle.jsx +0 -92
  52. package/src/components/AllowedEmailDomainsManager.jsx +0 -92
  53. package/src/components/AuthFactorRequirementCard.jsx +0 -95
  54. package/src/components/BulkInviteCsvTab.jsx +0 -198
  55. package/src/components/EmptyState.jsx +0 -38
  56. package/src/components/LoginForm.jsx +0 -159
  57. package/src/components/MFAComponent.jsx +0 -350
  58. package/src/components/MfaLoginComponent.jsx +0 -252
  59. package/src/components/MobileBottomNav.jsx +0 -82
  60. package/src/components/PasskeysComponent.jsx +0 -252
  61. package/src/components/PasswordChangeForm.jsx +0 -87
  62. package/src/components/PasswordResetRequestForm.jsx +0 -43
  63. package/src/components/PasswordSetForm.jsx +0 -97
  64. package/src/components/ProfileComponent.jsx +0 -348
  65. package/src/components/QrSignupManager.jsx +0 -404
  66. package/src/components/QrSignupValidityManager.jsx +0 -106
  67. package/src/components/RegistrationMethodsManager.jsx +0 -150
  68. package/src/components/SecurityComponent.jsx +0 -141
  69. package/src/components/SocialLoginButtons.jsx +0 -83
  70. package/src/components/SoftChip.jsx +0 -93
  71. package/src/components/StatTile.jsx +0 -39
  72. package/src/components/SupportRecoveryRequestsTab.jsx +0 -482
  73. package/src/components/TurnstileWidget.jsx +0 -99
  74. package/src/components/UserInviteComponent.jsx +0 -76
  75. package/src/components/UserListComponent.jsx +0 -618
  76. package/src/components/charts/BarChart.jsx +0 -102
  77. package/src/components/charts/ChartFrame.jsx +0 -214
  78. package/src/components/charts/LineChart.jsx +0 -126
  79. package/src/components/charts/ScatterChart.jsx +0 -299
  80. package/src/components/charts/TimeSeriesChart.jsx +0 -300
  81. package/src/components/charts/chartDefaults.js +0 -644
  82. package/src/components/charts/chartLabels.js +0 -36
  83. package/src/components/charts/exportChart.js +0 -258
  84. package/src/components/charts/formatters.js +0 -39
  85. package/src/components/charts/palette.js +0 -33
  86. package/src/components/charts/yearTickInterval.js +0 -7
  87. package/src/i18n/authTranslations.ts +0 -2651
  88. package/src/i18n/chartsTranslations.ts +0 -91
  89. package/src/i18n/chatTranslations.ts +0 -55
  90. package/src/i18n/messagingTranslations.ts +0 -120
  91. package/src/i18n/notificationsTranslations.ts +0 -32
  92. package/src/i18n/onboardingTranslations.ts +0 -29
  93. package/src/i18n/sectionNavTranslations.ts +0 -20
  94. package/src/i18n/uiCoreTranslations.js +0 -29
  95. package/src/i18n/userMenuTranslations.ts +0 -5
  96. package/src/index.js +0 -158
  97. package/src/layout/PageLayout.jsx +0 -36
  98. package/src/layout/SectionNav.jsx +0 -320
  99. package/src/messaging/AttachmentList.jsx +0 -145
  100. package/src/messaging/Composer.jsx +0 -197
  101. package/src/messaging/ConversationLaunchers.jsx +0 -51
  102. package/src/messaging/ConversationList.jsx +0 -109
  103. package/src/messaging/DirectMessageLauncher.jsx +0 -112
  104. package/src/messaging/MessageBubble.jsx +0 -183
  105. package/src/messaging/MessagingProvider.jsx +0 -0
  106. package/src/messaging/MessagingScopeConfig.jsx +0 -24
  107. package/src/messaging/PollCard.jsx +0 -163
  108. package/src/messaging/ReactionBar.jsx +0 -43
  109. package/src/messaging/ReadTicks.jsx +0 -82
  110. package/src/messaging/Thread.jsx +0 -195
  111. package/src/messaging/api.js +0 -48
  112. package/src/messaging/composerImageCompression.js +0 -35
  113. package/src/messaging/linkifyText.jsx +0 -37
  114. package/src/notifications/NotificationBell.jsx +0 -84
  115. package/src/notifications/NotificationSettings.jsx +0 -330
  116. package/src/notifications/NotificationsProvider.jsx +0 -224
  117. package/src/notifications/PopupSurface.jsx +0 -105
  118. package/src/notifications/api.js +0 -55
  119. package/src/notifications/feedApi.js +0 -18
  120. package/src/notifications/realtime.jsx +0 -138
  121. package/src/notifications/serviceWorker/sw.js +0 -50
  122. package/src/onboarding/OnboardingProvider.jsx +0 -326
  123. package/src/onboarding/OnboardingWizard.jsx +0 -61
  124. package/src/onboarding/WizardDialogShell.jsx +0 -51
  125. package/src/onboarding/api.js +0 -13
  126. package/src/onboarding/stepSelection.js +0 -16
  127. package/src/onboarding/steps/BrowserPushStep.jsx +0 -121
  128. package/src/onboarding/steps/CompleteNameStep.jsx +0 -46
  129. package/src/onboarding/steps/CookieConsentStep.jsx +0 -133
  130. package/src/onboarding/steps/PwaInstallStep.jsx +0 -47
  131. package/src/pages/AccountPage.jsx +0 -394
  132. package/src/pages/LoginPage.jsx +0 -343
  133. package/src/pages/PasswordChangePage.jsx +0 -62
  134. package/src/pages/PasswordInvitePage.jsx +0 -177
  135. package/src/pages/PasswordResetRequestPage.jsx +0 -68
  136. package/src/pages/SignUpPage.jsx +0 -268
  137. package/src/pages/SignupConfirmPage.jsx +0 -155
  138. package/src/testing/checkKitIntegration.js +0 -233
  139. package/src/theme/createAppTheme.js +0 -423
  140. package/src/theme/fonts.js +0 -3
  141. package/src/theme/index.js +0 -21
  142. package/src/theme/kitSxRegistry.js +0 -285
  143. package/src/theme/themeCompleteness.js +0 -1045
  144. package/src/theme/tokens.js +0 -478
  145. package/src/utils/auth-errors.js +0 -76
  146. package/src/utils/authService.js +0 -223
  147. package/src/utils/resolveErrorText.js +0 -28
  148. package/src/utils/webauthn.js +0 -51
  149. package/tests/AccountPage.profileStatements.test.jsx +0 -162
  150. package/tests/AccountPage.test.jsx +0 -200
  151. package/tests/AuthContext.test.jsx +0 -129
  152. package/tests/BarChart.test.jsx +0 -112
  153. package/tests/BrowserPushStep.test.jsx +0 -110
  154. package/tests/ChartFrame.test.jsx +0 -336
  155. package/tests/ConversationList.test.jsx +0 -144
  156. package/tests/CookieConsentStep.test.jsx +0 -69
  157. package/tests/EmptyState.test.jsx +0 -62
  158. package/tests/LineChart.test.jsx +0 -80
  159. package/tests/MessagingProvider.test.jsx +0 -50
  160. package/tests/MobileBottomNav.test.jsx +0 -319
  161. package/tests/NotificationBell.test.jsx +0 -60
  162. package/tests/NotificationSettings.test.jsx +0 -276
  163. package/tests/NotificationsProvider.test.jsx +0 -240
  164. package/tests/OnboardingProvider.test.jsx +0 -294
  165. package/tests/OnboardingWizard.test.jsx +0 -143
  166. package/tests/PasswordChangeForm.test.jsx +0 -70
  167. package/tests/PasswordInvitePage.test.jsx +0 -142
  168. package/tests/PasswordSetForm.test.jsx +0 -70
  169. package/tests/PopupSurface.test.jsx +0 -142
  170. package/tests/ProfileComponent.refresh.test.jsx +0 -194
  171. package/tests/QrSignupManager.registrationContext.test.jsx +0 -106
  172. package/tests/QrSignupManagerInfoBox.test.jsx +0 -49
  173. package/tests/ScatterChart.test.jsx +0 -270
  174. package/tests/SectionNav.test.jsx +0 -363
  175. package/tests/SignUpPage.test.jsx +0 -208
  176. package/tests/SoftChip.test.jsx +0 -105
  177. package/tests/StatTile.test.jsx +0 -57
  178. package/tests/Thread.test.jsx +0 -151
  179. package/tests/TimeSeriesChart.test.jsx +0 -367
  180. package/tests/UserListComponent.test.jsx +0 -192
  181. package/tests/UserMenu.test.jsx +0 -171
  182. package/tests/apiClientLanguage.test.js +0 -33
  183. package/tests/apiClientPublicPaths.test.js +0 -178
  184. package/tests/authApiLogin.test.js +0 -42
  185. package/tests/authApiRegistration.test.js +0 -31
  186. package/tests/authErrorCodeRendering.test.jsx +0 -77
  187. package/tests/authErrors.test.js +0 -74
  188. package/tests/authTranslations.test.js +0 -74
  189. package/tests/chart-api-census.test.js +0 -366
  190. package/tests/chartAxisTickBand.test.jsx +0 -95
  191. package/tests/chartDefaults.test.js +0 -902
  192. package/tests/chartLabels.test.js +0 -50
  193. package/tests/chartsPalette.test.js +0 -99
  194. package/tests/chatApi.test.js +0 -48
  195. package/tests/chatDriverNames.test.jsx +0 -71
  196. package/tests/chatExports.test.js +0 -3
  197. package/tests/chatList.test.jsx +0 -84
  198. package/tests/chatPicker.test.jsx +0 -82
  199. package/tests/chatSocket.test.jsx +0 -26
  200. package/tests/chatThread.test.jsx +0 -116
  201. package/tests/chatTranslations.test.js +0 -47
  202. package/tests/chatWorkspace.test.jsx +0 -234
  203. package/tests/checkKitIntegration.test.jsx +0 -219
  204. package/tests/createAppTheme.test.js +0 -132
  205. package/tests/createAppThemeDark.test.js +0 -127
  206. package/tests/createAppThemeLightGuard.test.js +0 -40
  207. package/tests/createAppThemePageContrast.test.js +0 -108
  208. package/tests/createAppThemeTypography.test.js +0 -53
  209. package/tests/errorCallSiteGuard.test.js +0 -32
  210. package/tests/exportChart.test.js +0 -313
  211. package/tests/feedApi.test.js +0 -46
  212. package/tests/fixtures/light-theme-baseline.json +0 -2234
  213. package/tests/i18nAggregate.test.js +0 -120
  214. package/tests/i18nGermanVariant.test.js +0 -65
  215. package/tests/loginPageErrorResolution.test.jsx +0 -102
  216. package/tests/messagingApi.test.js +0 -32
  217. package/tests/messagingChunk6.test.jsx +0 -116
  218. package/tests/messagingCompactBubble.test.jsx +0 -64
  219. package/tests/messagingComposer.test.jsx +0 -136
  220. package/tests/messagingComposerChunk5.test.jsx +0 -113
  221. package/tests/messagingContractConformance.test.js +0 -69
  222. package/tests/messagingDirectMessageLauncher.test.jsx +0 -151
  223. package/tests/messagingExports.test.js +0 -21
  224. package/tests/messagingInteractions.test.jsx +0 -136
  225. package/tests/messagingLastMessagePreview.test.jsx +0 -93
  226. package/tests/messagingLinkify.test.jsx +0 -51
  227. package/tests/messagingMessageActions.test.jsx +0 -163
  228. package/tests/messagingMsg6.test.jsx +0 -105
  229. package/tests/messagingMsg6f.test.jsx +0 -155
  230. package/tests/messagingMsg6g.test.jsx +0 -166
  231. package/tests/messagingMsg6h.test.jsx +0 -136
  232. package/tests/messagingMsg6i.test.jsx +0 -129
  233. package/tests/messagingPollRendering.test.jsx +0 -142
  234. package/tests/messagingReactionMerge.test.jsx +0 -77
  235. package/tests/messagingReadTicksTooltip.test.jsx +0 -36
  236. package/tests/messagingThreadReceipts.test.jsx +0 -178
  237. package/tests/messagingThreadingQuoting.test.jsx +0 -87
  238. package/tests/messagingUnreadLifecycle.test.jsx +0 -78
  239. package/tests/notificationsApi.test.js +0 -41
  240. package/tests/notificationsExports.test.js +0 -34
  241. package/tests/onboardingDescriptors.test.js +0 -132
  242. package/tests/packageTreeShaking.test.js +0 -126
  243. package/tests/realtime.test.jsx +0 -219
  244. package/tests/resolveErrorText.test.js +0 -72
  245. package/tests/stepSelection.test.js +0 -53
  246. package/tests/themeCompleteness.test.js +0 -626
  247. package/tests/timeSeriesLegend.test.js +0 -99
  248. package/tests/userScreenTranslations.test.jsx +0 -218
  249. package/tsconfig.build.json +0 -18
  250. package/vite.config.mjs +0 -6
  251. package/work-orders/AUTH-1.md +0 -74
  252. package/work-orders/AUTH-2.md +0 -74
  253. package/work-orders/AUTH-4.md +0 -183
  254. package/work-orders/AUTH-5.md +0 -154
  255. package/work-orders/AUTH-7.md +0 -181
  256. package/work-orders/AUTH-8.md +0 -184
  257. package/work-orders/CHART-1.md +0 -285
  258. package/work-orders/CHART-2.md +0 -222
  259. package/work-orders/CHART-3.md +0 -163
  260. package/work-orders/CHART-4.md +0 -203
  261. package/work-orders/CHART-5.md +0 -226
  262. package/work-orders/CHART-6.md +0 -428
  263. package/work-orders/CHART-7.md +0 -175
  264. package/work-orders/DX-1.md +0 -169
  265. package/work-orders/DX-2.md +0 -188
  266. package/work-orders/DX-3.md +0 -195
  267. package/work-orders/I18N-1.md +0 -254
  268. package/work-orders/I18N-3.md +0 -207
  269. package/work-orders/MSG-3.md +0 -375
  270. package/work-orders/MSG-3b.md +0 -1002
  271. package/work-orders/MSG-3c.md +0 -378
  272. package/work-orders/MSG-3d.md +0 -268
  273. package/work-orders/MSG-3e.md +0 -164
  274. package/work-orders/MSG-3f.md +0 -391
  275. package/work-orders/MSG-6e.md +0 -308
  276. package/work-orders/MSG-6f.md +0 -214
  277. package/work-orders/MSG-6g.md +0 -244
  278. package/work-orders/MSG-6h.md +0 -238
  279. package/work-orders/MSG-6i.md +0 -271
  280. package/work-orders/MSG-6j.md +0 -117
  281. package/work-orders/NOTIF-12.md +0 -214
  282. package/work-orders/NOTIF-13.md +0 -142
  283. package/work-orders/NOTIF-14.md +0 -151
  284. package/work-orders/PRIM-1.md +0 -177
  285. package/work-orders/SHELL-1.md +0 -255
  286. package/work-orders/SHELL-2.md +0 -197
  287. package/work-orders/SHELL-3.md +0 -359
  288. package/work-orders/SHELL-4.md +0 -351
  289. package/work-orders/SHELL-5.md +0 -345
  290. package/work-orders/SHELL-6.md +0 -210
  291. package/work-orders/THEME-1.md +0 -587
  292. package/work-orders/THEME-10.md +0 -303
  293. package/work-orders/THEME-11.md +0 -208
  294. package/work-orders/THEME-2.md +0 -339
  295. package/work-orders/THEME-3.md +0 -210
  296. package/work-orders/THEME-4.md +0 -271
  297. package/work-orders/THEME-5.md +0 -341
  298. package/work-orders/THEME-6.md +0 -247
  299. package/work-orders/THEME-7.md +0 -82
  300. package/work-orders/THEME-8.md +0 -211
  301. package/work-orders/THEME-9.md +0 -194
  302. package/work-orders/UCM-AUTH-10.md +0 -490
  303. package/work-orders/UCM-AUTH-11.md +0 -185
  304. package/work-orders/UCM-AUTH-12.md +0 -98
  305. package/work-orders/UCM-AUTH-13.md +0 -47
  306. package/work-orders/UCM-AUTH-9.md +0 -207
  307. package/work-orders/UCM-CHART-10.md +0 -150
  308. package/work-orders/UCM-CHART-11.md +0 -132
  309. package/work-orders/UCM-CHART-12.md +0 -348
  310. package/work-orders/UCM-CHART-13.md +0 -122
  311. package/work-orders/UCM-CHART-14.md +0 -198
  312. package/work-orders/UCM-CHART-15.md +0 -187
  313. package/work-orders/UCM-CHART-16.md +0 -169
  314. package/work-orders/UCM-CHART-17.md +0 -195
  315. package/work-orders/UCM-CHART-18.md +0 -200
  316. package/work-orders/UCM-CHART-8.md +0 -147
  317. package/work-orders/UCM-CHART-9.md +0 -265
  318. package/work-orders/UCM-CHAT-1.md +0 -190
  319. package/work-orders/UCM-CHAT-2.md +0 -75
  320. package/work-orders/UCM-CI-1.md +0 -65
  321. package/work-orders/UCM-DEP-1.md +0 -63
  322. package/work-orders/UCM-I18N-4.md +0 -226
  323. package/work-orders/UCM-I18N-5.md +0 -211
  324. package/work-orders/UCM-MSG-20.md +0 -192
  325. package/work-orders/UCM-PRIM-2.md +0 -114
  326. package/work-orders/UCM-SHELL-7.md +0 -134
  327. package/work-orders/UCM-SHELL-8.md +0 -125
  328. package/work-orders/UCM-TEST-1.md +0 -304
  329. package/work-orders/UCM-THEME-13.md +0 -69
  330. package/work-orders/UCM-THEME-14.md +0 -312
  331. package/work-orders/UCM-THEME-15.md +0 -356
  332. package/work-orders/UCM-THEME-16.md +0 -80
  333. package/work-orders/UCM-THEME-17.md +0 -111
  334. package/work-orders/UCM-THEME-18.md +0 -184
  335. package/work-orders/assets/AUTH-4-user-list-card.html +0 -407
  336. package/work-orders/assets/PRIM-1-dashboard-primitives.html +0 -624
  337. package/work-orders/assets/SHELL-2-account-navigation.html +0 -1159
  338. package/work-orders/assets/THEME-1-baseline-reference-sheet.html +0 -1352
  339. package/work-orders/assets/UCM-CHAT-1-ai-chat.html +0 -447
  340. package/work-orders/assets/UCM-THEME-13-dark-prototype.html +0 -390
  341. 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
- }