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