@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,644 +0,0 @@
1
- export const DEFAULT_LEGEND_POSITION = {
2
- vertical: 'bottom',
3
- horizontal: 'start',
4
- };
5
-
6
- export function withGridDefaults(grid) {
7
- return { horizontal: true, ...grid };
8
- }
9
-
10
- export function withAxisDefaults(axes, label, defaults, tickFontSize) {
11
- const values = axes?.length ? axes : [defaults];
12
- return values.map((axis, index) => ({
13
- // `defaults.scaleType` (and any other scale-defining default) must
14
- // survive even when the caller passes their OWN xAxis/yAxis array --
15
- // confirmed live in the dev harness: a caller-supplied axis with `data`
16
- // but no `scaleType` previously lost 'point'/'band' entirely (only
17
- // applied when the whole array was absent), leaving MUI's scale
18
- // undefined and every point position NaN (`<path> attribute d:
19
- // Expected number, "MNaN,..."` in the real rendered SVG -- a mocked
20
- // prop-assertion test cannot catch this, it only reproduces on an
21
- // actual MUI scale computation).
22
- ...defaults,
23
- ...axis,
24
- tickLabelStyle: {
25
- fontSize: tickFontSize,
26
- ...axis.tickLabelStyle,
27
- },
28
- // THEME-9: the single `label` argument is the caller's ONE string for
29
- // this whole axis family (xAxisLabel/yAxisLabel) -- it may only ever
30
- // fall back onto ONE axis. Applying it via .map() to every entry
31
- // broadcast the identical label onto every axis in a multi-axis array
32
- // (measured live: hram's dual-axis chart carried the same 43-character
33
- // label on both y-axes). Only the first entry -- the common single-axis
34
- // case is index 0 -- gets the fallback; a caller building a genuine
35
- // multi-axis array must label each entry itself (as TimeSeriesChart's
36
- // own secondary-axis support already does), exactly like MUI's own
37
- // default `position: 'left'` (only the first entry, MUI docs).
38
- label: axis.label || (index === 0 ? label : undefined),
39
- }));
40
- }
41
-
42
- function cssLengthToPixels(value) {
43
- if (typeof value === 'number') return value;
44
- if (typeof value !== 'string') return 0;
45
-
46
- const parsed = Number.parseFloat(value);
47
- if (!Number.isFinite(parsed)) return 0;
48
- if (value.endsWith('rem') || value.endsWith('em')) return parsed * 16;
49
- return parsed;
50
- }
51
-
52
- const MUI_LABELLED_X_AXIS_HEIGHT = 45;
53
- // Still used by `sizeYAxisForContent` (THEME-9, y-axis width) below -- unrelated to CHART-11,
54
- // which only replaces how the ROTATED x-axis path (`rotatedTickMetrics`) estimates width.
55
- const AVERAGE_GLYPH_WIDTH_EM = 0.6;
56
-
57
- // CHART-11: per-character width table (em units), the ESTIMATE fallback used only when no DOM is
58
- // available to actually measure a tick label's rendered extent (SSR, or a test environment where
59
- // `measureTickTextWidthPx` returns null -- jsdom does not implement real SVG text layout). A
60
- // single flat multiplier (the pre-CHART-11 constant, `AVERAGE_GLYPH_WIDTH_EM = 0.6`) systematically
61
- // overestimated real ward-name-shaped strings: they mix spaces, apostrophes and narrow lowercase
62
- // letters ("Mang'ula", "Msolwa Station") with a few wide/uppercase ones, and a flat multiplier
63
- // prices every character at the wide end. Three tiers, not a full per-glyph font metrics table --
64
- // proportionate to "an estimate", not a replacement for real measurement.
65
- const NARROW_GLYPH_WIDTH_EM = 0.28; // i l j I ' . , : ; ! | and space
66
- const WIDE_GLYPH_WIDTH_EM = 0.78; // uppercase (except I), plus lowercase m/w
67
- const DEFAULT_GLYPH_WIDTH_EM = 0.52; // everything else -- most lowercase letters and digits
68
- const NARROW_GLYPH_CHARS = new Set(["i", "l", "j", "I", "'", ".", ",", ":", ";", "!", "|", " "]);
69
- const WIDE_GLYPH_CHARS = new Set(["m", "w"]);
70
-
71
- function glyphWidthEm(char) {
72
- if (NARROW_GLYPH_CHARS.has(char)) return NARROW_GLYPH_WIDTH_EM;
73
- if (WIDE_GLYPH_CHARS.has(char) || (char >= 'A' && char <= 'Z')) return WIDE_GLYPH_WIDTH_EM;
74
- return DEFAULT_GLYPH_WIDTH_EM;
75
- }
76
-
77
- /**
78
- * CHART-11 ESTIMATE fallback (declared explicitly, not dressed up as a measurement): sums a
79
- * per-character width table instead of applying one flat multiplier to the whole string. Only
80
- * reached when `measureTickTextWidthPx` returns null.
81
- */
82
- function estimateTextWidthPx(text, fontSizePx) {
83
- let widthEm = 0;
84
- for (const char of text) widthEm += glyphWidthEm(char);
85
- return widthEm * fontSizePx;
86
- }
87
-
88
- let hiddenMeasurementTextNode;
89
-
90
- function getHiddenMeasurementTextNode() {
91
- if (typeof document === 'undefined') return null;
92
- if (hiddenMeasurementTextNode && hiddenMeasurementTextNode.isConnected) {
93
- return hiddenMeasurementTextNode;
94
- }
95
- const svgNS = 'http://www.w3.org/2000/svg';
96
- const svg = document.createElementNS(svgNS, 'svg');
97
- svg.setAttribute('aria-hidden', 'true');
98
- svg.style.position = 'absolute';
99
- svg.style.width = '0';
100
- svg.style.height = '0';
101
- svg.style.overflow = 'hidden';
102
- svg.style.pointerEvents = 'none';
103
- const text = document.createElementNS(svgNS, 'text');
104
- svg.appendChild(text);
105
- (document.body || document.documentElement).appendChild(svg);
106
- hiddenMeasurementTextNode = text;
107
- return text;
108
- }
109
-
110
- /**
111
- * CHART-11: measures a tick label's real rendered width via a transient, invisible, shared SVG
112
- * `<text>` node -- the same element type MUI's own axis ticks render as, so glyph metrics match
113
- * whatever font actually cascades to it (this package has no access to the consumer's theme font
114
- * at this call site; the transient node inherits the page's own cascade, same as a real tick
115
- * would). Returns `null` (never throws) when no DOM is available (SSR) or when
116
- * `getComputedTextLength` is unsupported/unreliable there (jsdom does not implement real SVG text
117
- * layout) -- callers fall back to `estimateTextWidthPx` in that case, and tests inject their own
118
- * stub via `spaceForRotatedTicks`'s optional `measureTextWidth` parameter for determinism.
119
- */
120
- function measureTickTextWidthPx(text, fontSizePx) {
121
- try {
122
- const node = getHiddenMeasurementTextNode();
123
- if (!node) return null;
124
- node.setAttribute('font-size', String(fontSizePx));
125
- node.textContent = text;
126
- const length = node.getComputedTextLength();
127
- return Number.isFinite(length) && length > 0 ? length : null;
128
- } catch {
129
- return null;
130
- }
131
- }
132
-
133
- // THEME-11: this package never formed an opinion on chart margins, so every chart fell back to
134
- // MUI's own DEFAULT_MARGINS (20px on all four sides) -- measured live (hram's KPI cards) as 167
135
- // of a 270px-tall chart being furniture. Per-side, not a uniform trim, because each side is for
136
- // something different: `left`/`bottom` are pure padding (the tick labels already live inside
137
- // axisSizeLeft / DEFAULT_AXIS_SIZE_HEIGHT), `top` keeps room for the topmost y-tick label's own
138
- // ~9px overhang past the plot (it is centred on its gridline, not below it), and `right` stays
139
- // less trimmed than `left` because a LINEAR x-axis's last tick label sits AT the edge and
140
- // overhangs by about half its width (~13px at caption size) -- a band axis wouldn't need it, but
141
- // making this conditional on scale type would be inferred behaviour with no named consumer.
142
- //
143
- // UCM-CHART-12 Rule 3: no caller-supplied `margin` exists any more (the model owns spacing
144
- // completely, `resolveChartLayout` builds the final margin object directly) -- this is now only
145
- // the base every side starts from before `resolveChartLayout` adds the legend band to one side.
146
- export const PACKAGE_DEFAULT_MARGIN = {
147
- top: 10, bottom: 8, left: 8, right: 16,
148
- };
149
-
150
- // CHART-11: small clearance between the rotated text block's own projected extent and the
151
- // bottom of the axis's reserved band -- the same order of magnitude as the package's own
152
- // PACKAGE_DEFAULT_MARGIN breathing-room, applied here for the same reason (a hard 0px gap reads
153
- // as clipped even when nothing is actually cut off).
154
- const ROTATED_TICK_CLEARANCE_PX = 8;
155
-
156
- // UCM-CHART-16: the FLAT (angle 0) tick band used to be the unconditional TICK_BAND_BASE_PX
157
- // regardless of the actual tick font size -- fine for MUI's own generic default, but MUI's
158
- // internal fit check (`shortenLabels`, against `tickLabelsMaxHeight`) blanks EVERY tick label the
159
- // moment the real rendered text is even slightly taller than what that flat floor leaves room
160
- // for once MUI's own tick/gap overhead (and, when an axis title shares the band, its own gap) is
161
- // subtracted. `TICK_BAND_BASE_PX` alone was a near-miss with essentially no margin at any font
162
- // size above whatever MUI itself implicitly assumed -- two hram consumers independently patched
163
- // around it with the same empirically-measured `height: 56`, and a third, titleless case broke at
164
- // a tick font just 1px above the untitled default.
165
- //
166
- // These three constants are DERIVED, not guessed: binary-searched against the real, unmocked
167
- // `BarChart` rendered in this repo's own dev harness (`dev/entries.jsx`'s `chart-probe` entry,
168
- // Chrome, this package's own DM Sans default -- the taller of the two fonts checked; MUI's Roboto
169
- // fallback needs visibly less and was never the trigger) at tick font sizes 11-18px, with and
170
- // without an axis title:
171
- // required flat tickBand ~= ceil(fontSize * TICK_TEXT_HEIGHT_FACTOR) + TICK_BAND_MUI_OVERHEAD_PX
172
- // required tickBand+title ~= that, plus TICK_BAND_TITLE_EXTRA_PX, plus AXIS_TITLE_BAND_PX
173
- // matched every measured threshold exactly or over by at most 1px, never under, across ten
174
- // font-size/title combinations -- see UCM-CHART-16's work order for the full measurement table.
175
- const TICK_TEXT_HEIGHT_FACTOR = 1.3;
176
- const TICK_BAND_MUI_OVERHEAD_PX = 9;
177
- const TICK_BAND_TITLE_EXTRA_PX = 2;
178
-
179
- // THEME-9: MUI X-Charts' own default y-axis width is a flat 45px (ticks) +
180
- // 20px (label, if present) = 65px regardless of what the ticks actually say
181
- // -- confirmed against @mui/x-charts' own DEFAULT_AXIS_SIZE_WIDTH /
182
- // AXIS_LABEL_DEFAULT_HEIGHT constants and its `useChartDimensions` selector
183
- // (drawingArea.left = margin.left + axisSizeLeft, i.e. margin and axis size
184
- // are ADDITIVE, not one containing the other). That flat default is both too
185
- // generous for short ticks ("0%"/"50%"/"100%") and too stingy for wide ones
186
- // ("50.0 min"), which is exactly the measured overlap on the Accessibility
187
- // y-axis. Mirrors spaceForRotatedTicks' existing text-width heuristic below.
188
- const Y_AXIS_TICK_SIZE = 6;
189
- const Y_AXIS_TICK_LABEL_GAP = 2;
190
- const Y_AXIS_LABEL_TICK_GAP = 2;
191
- // A rotated (90deg) axis-label's line-box "thickness" -- what it costs in
192
- // WIDTH once rotated -- runs a bit past its raw font size once ascender/
193
- // descender room is included; matches the ~18px measured live for a 14px
194
- // label (14 * 1.3 ~= 18).
195
- const Y_AXIS_LABEL_THICKNESS_FACTOR = 1.3;
196
- // A small safety margin on the estimate: `spaceForRotatedTicks`'s own risk
197
- // note applies here too -- a too-tight cut recreates the overlap it was
198
- // meant to fix, and glyph-width heuristics are approximate by nature.
199
- const Y_AXIS_WIDTH_SAFETY_FACTOR = 1.1;
200
- // MUI X-Charts' ChartsYAxisImpl hardcodes the axis-label font at 14px
201
- // (`additionalProps.style: {...theme.typography.body1, fontSize: 14, ...}`
202
- // -- fontSize:14 is set AFTER the theme.typography.body1 spread, so it wins
203
- // regardless of what body1 actually resolves to). Matching that literal
204
- // constant here -- rather than reading it off `theme.typography.body1` --
205
- // keeps this sizing correct even if this package's body1 size ever changes;
206
- // coupling it to body1 was only ever coincidentally right because body1
207
- // happens to be 14px today.
208
- const MUI_AXIS_LABEL_FONT_SIZE_PX = 14;
209
-
210
- function longestFormattedTick(values, valueFormatter) {
211
- return values.reduce((longest, value) => {
212
- let text;
213
- try {
214
- text = valueFormatter ? String(valueFormatter(value, { location: 'tick' })) : String(value);
215
- } catch {
216
- // Some formatters require MUI's complete scale context; falling back
217
- // to the raw value is conservative -- it reserves at least as much
218
- // space as the unformatted number would need.
219
- text = String(value);
220
- }
221
- return text.length > longest.length ? text : longest;
222
- }, '');
223
- }
224
-
225
- /**
226
- * Sizes a linear y-axis to its actual tick content instead of MUI's flat
227
- * default. Only touches axes the caller left unsized (`axis.width == null`)
228
- * and only linear/numeric ones -- a categorical axis's ticks are names, not
229
- * numbers, and sizing those is not this function's job. Candidate values come
230
- * from the axis' own `min`/`max` when the caller set them (as every current
231
- * consumer does), falling back to the series actually plotted against this
232
- * axis; if neither yields a candidate, the axis is left untouched rather than
233
- * guessed at.
234
- *
235
- * `tickFontSize` is only the FALLBACK -- an axis' own `tickLabelStyle.fontSize`
236
- * (set by `withAxisDefaults`, itself overridable per-axis by the caller) is
237
- * read first, so a caller-tuned tick font is sized correctly rather than
238
- * against the package-wide default. The axis LABEL's font is not a caller
239
- * knob at all (`MUI_AXIS_LABEL_FONT_SIZE_PX` -- MUI hardcodes it).
240
- *
241
- * The series-to-axis match for the fallback deliberately mirrors MUI's own
242
- * default-assignment rule, not a naive `undefined === undefined`: a series
243
- * with no explicit `yAxisId` is assigned to the FIRST axis in the array
244
- * (`yAxisIds[0]` in MUI's own selector), never to "no id". Matching only
245
- * `undefined === undefined` left the primary axis of a real dual-axis chart
246
- * (id `'primary'`, its series never set `yAxisId`) with zero matched series
247
- * and therefore MUI's untouched flat default -- a silent no-op on exactly
248
- * the dual-axis case this WO was measured against.
249
- */
250
- export function sizeYAxisForContent(yAxis, series, tickFontSize) {
251
- const fallbackTickFontSizePx = cssLengthToPixels(tickFontSize);
252
- const defaultAxisId = yAxis[0]?.id;
253
- return yAxis.map((axis) => {
254
- if (axis.width != null) return axis;
255
- if (axis.scaleType && axis.scaleType !== 'linear') return axis;
256
-
257
- const tickFontSizePx = cssLengthToPixels(axis.tickLabelStyle?.fontSize) || fallbackTickFontSizePx;
258
- if (tickFontSizePx === 0) return axis;
259
-
260
- const candidates = [];
261
- if (typeof axis.min === 'number') candidates.push(axis.min);
262
- if (typeof axis.max === 'number') candidates.push(axis.max);
263
- if (candidates.length === 0) {
264
- (series || [])
265
- .filter((item) => (item.yAxisId ?? defaultAxisId) === axis.id)
266
- .forEach((item) => (item.data || []).forEach((value) => {
267
- if (typeof value === 'number') candidates.push(value);
268
- }));
269
- }
270
- if (candidates.length === 0) return axis;
271
-
272
- const longestTick = longestFormattedTick(candidates, axis.valueFormatter);
273
- // UCM-CHART-12 Rule 2 (codex second-pass review finding): candidates existed, but a caller
274
- // `valueFormatter` reduced every one of them to a blank string -- this is the y-axis analogue
275
- // of `xAxisHasVisibleTicks`'s Access-scatter case, and must collapse the same way. Distinct
276
- // from `candidates.length === 0` above (nothing to measure at all, MUI's own default applies,
277
- // untouched): here there WAS content, it just renders as nothing. With no axis title either,
278
- // the axis has genuinely nothing to show, so the whole band collapses to 0 -- not merely the
279
- // text-width term floored, matching the x-axis band's own all-or-nothing zero.
280
- if (longestTick.length === 0 && !axis.label) return { ...axis, width: 0 };
281
- const tickTextWidth = longestTick.length === 0 ? 0 : Math.max(
282
- tickFontSizePx,
283
- longestTick.length * tickFontSizePx * AVERAGE_GLYPH_WIDTH_EM,
284
- );
285
- const labelThickness = axis.label
286
- ? Math.ceil(MUI_AXIS_LABEL_FONT_SIZE_PX * Y_AXIS_LABEL_THICKNESS_FACTOR) + Y_AXIS_LABEL_TICK_GAP
287
- : 0;
288
- const width = Math.ceil(
289
- (tickTextWidth + Y_AXIS_TICK_SIZE + Y_AXIS_TICK_LABEL_GAP + labelThickness)
290
- * Y_AXIS_WIDTH_SAFETY_FACTOR,
291
- );
292
-
293
- return { ...axis, width };
294
- });
295
- }
296
-
297
- /**
298
- * Locale-correct, unit-free tick numbers as the default -- BarChart set none
299
- * at all (raw numbers straight from the domain), and a hand-rolled `Intl`
300
- * compact-notation default was measured to be actively wrong: German does
301
- * not compact thousands at all (`12'500` stays `12'500`) and Swahili prepends
302
- * a word that is WIDER than the raw number (`elfu 12.5`) -- compact notation
303
- * does not solve the width problem and worsens it in one locale. Plain
304
- * grouped formatting (no `notation: 'compact'`) is safe across all four
305
- * locales this package's consumers use. Ticks get a short, rounded form;
306
- * tooltip/legend keep full precision -- teaching screens must not round away
307
- * a meaningful difference. A caller's own `valueFormatter` always wins (this
308
- * is only ever used as a default, per `withAxisDefaults`' `{...defaults,
309
- * ...axis}` merge).
310
- */
311
- export function defaultNumericTickFormatter(locale) {
312
- return (value, context) => (context?.location === 'tick'
313
- ? new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value)
314
- : new Intl.NumberFormat(locale).format(value));
315
- }
316
-
317
- // UCM-CHART-15: named size tokens, resolved through the theme's 8px spacing unit
318
- // (`src/theme/tokens.js`'s `spacing: 8`). Through UCM-CHART-12/14, `standard` stayed pinned to
319
- // the pre-existing deployed default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so that
320
- // migration did not also silently redraw every already-shipped default-sized chart -- that pin
321
- // was a migration guardrail, not a design verdict. The migration it protected is complete, and
322
- // the operator judged the whole scale too cramped (`tall` had already forced hram to use the
323
- // `height` px escape to get past the old ceiling), so this WO shifts every token up one step and
324
- // adds two steps above the old `tall`, chosen rather than inherited. Documented plainly in
325
- // `docs/CHART-LAYOUT.md` -- these are visible product decisions, not internal plumbing.
326
- export const CHART_SIZE_SPACING_UNITS = {
327
- compact: 40, standard: 50, tall: 60, extra_tall: 70, super_tall: 80,
328
- };
329
-
330
- // UCM-CHART-12 Rule 2: every band is its own measured content, or zero -- no band is ever a
331
- // constant taken on faith. These two ARE constants, but only for the "content is present" case;
332
- // each one collapses to 0 the moment its content is absent (`resolveXAxisGeometry`,
333
- // `resolveChartLayout` below).
334
- //
335
- // ui_reviewer finding U1: before this WO, a plain (unrotated, untitled) axis got NO explicit
336
- // `axis.height` at all -- MUI computed its own default internally. This resolver now always sets
337
- // `axis.height` explicitly (Rule 1: nothing outside the resolver does chart arithmetic), which
338
- // looked like a silent behaviour change until checked against MUI's own source
339
- // (`@mui/x-charts/constants`): `DEFAULT_AXIS_SIZE_HEIGHT = 25` and `AXIS_LABEL_DEFAULT_HEIGHT =
340
- // 20` -- i.e. `TICK_BAND_BASE_PX`/`AXIS_TITLE_BAND_PX` below are not an approximation, they are
341
- // MUI's own constants, confirmed pixel-for-pixel. Making them explicit changes nothing about what
342
- // renders for that common case; `TICK_BAND_BASE_PX + AXIS_TITLE_BAND_PX` = 45 is also exactly the
343
- // pre-CHART-12 `MUI_LABELLED_X_AXIS_HEIGHT`, decomposed into the two bands Rule 2 requires instead
344
- // of one opaque "labelled axis height" figure.
345
- const TICK_BAND_BASE_PX = 25;
346
- const AXIS_TITLE_BAND_PX = MUI_LABELLED_X_AXIS_HEIGHT - TICK_BAND_BASE_PX;
347
- // One row of legend chips + gap -- MUI does not reserve this space itself (confirmed: nothing in
348
- // this package's pre-CHART-12 margin/axis math ever accounted for the legend), so a visible
349
- // legend growing the SVG uncounted was the sixth defect this WO's Rule 2 also closes.
350
- const LEGEND_BAND_PX = 32;
351
-
352
- // UCM-CHART-12 `xLabels: "auto"`: with no real container-width measurement available before
353
- // render (the presets stay responsive -- MUI measures actual width via ResizeObserver, which this
354
- // pure function cannot see), "auto" is a documented HEURISTIC, not a true fit computation: rotate
355
- // once there are enough categories AND the longest one, MEASURED (same mechanism as the rotated
356
- // projection below -- real DOM measurement when available, the per-glyph estimate otherwise), is
357
- // wide enough that a horizontal row would plausibly collide. Codex second-pass review finding:
358
- // the first cut of this heuristic used a raw character-count threshold instead of the measured
359
- // width this function already has available -- replaced, since "decide from the measured labels"
360
- // (Rule 3) is achievable even without a real container width to compare against. The count
361
- // threshold stays as the OTHER half of the heuristic: a handful of wide labels can still fit
362
- // spread across a wide chart, so width alone should not trigger rotation either.
363
- const AUTO_ROTATE_TICK_COUNT = 6;
364
- const AUTO_ROTATE_LABEL_WIDTH_PX = 50;
365
-
366
- function xAxisTickContentText(axis) {
367
- const data = axis?.data;
368
- if (!Array.isArray(data)) return null; // no data array -- can't tell (e.g. a numeric scale); assume content
369
- return longestFormattedTick(data, axis.valueFormatter);
370
- }
371
-
372
- // The Access-scatter case (WO Part A): an axis whose tick labels are all blank/absent must
373
- // collapse its reserved band to zero, not keep the package's floor. `null` (no data array at
374
- // all -- a linear numeric axis, whose ticks come from the scale, not a caller-supplied list)
375
- // is treated as "assume content", since there is nothing here to measure as empty.
376
- function xAxisHasVisibleTicks(axis) {
377
- const text = xAxisTickContentText(axis);
378
- return text === null || text.length > 0;
379
- }
380
-
381
- /**
382
- * UCM-CHART-12: decides the x-axis tick band's height and rotation angle. Replaces CHART-10/11's
383
- * `spaceForRotatedTicks`/`rotatedTickMetrics` -- same measured-text-width mechanism
384
- * (`measureTickTextWidthPx`, with `estimateTextWidthPx` as the no-DOM fallback and
385
- * `measureTextWidth` as the test injection point), but now driven by `xLabels`
386
- * ("auto"|"horizontal"|"angled") instead of a caller-supplied `tickLabelStyle.angle`, and
387
- * returning 0 outright when the axis has no visible tick content (Rule 2).
388
- */
389
- function resolveXAxisGeometry(axis, xLabelsMode, defaultLineHeight, measureTextWidth, fallbackTickFontSize) {
390
- if (!xAxisHasVisibleTicks(axis)) return { tickBand: 0, angle: 0 };
391
-
392
- const fontSize = cssLengthToPixels(axis?.tickLabelStyle?.fontSize) || cssLengthToPixels(fallbackTickFontSize);
393
- const longestLabel = xAxisTickContentText(axis) || '';
394
- const dataCount = Array.isArray(axis?.data) ? axis.data.length : 0;
395
- const measure = measureTextWidth || measureTickTextWidthPx;
396
-
397
- // Measured once, up front -- both "auto"'s rotation DECISION and the rotated-band PROJECTION
398
- // below need the label's real width, and must agree on what "real" means (same measurement,
399
- // same estimate fallback) or the two could disagree about the same label.
400
- const labelWidthPx = fontSize > 0
401
- ? Math.max(fontSize, (() => {
402
- const measured = measure(longestLabel, fontSize);
403
- return Number.isFinite(measured) && measured > 0 ? measured : estimateTextWidthPx(longestLabel, fontSize);
404
- })())
405
- : 0;
406
-
407
- let angle = 0;
408
- if (xLabelsMode === 'angled') {
409
- angle = Number(axis?.tickLabelStyle?.angle) || -45;
410
- } else if (xLabelsMode === 'auto') {
411
- const needsRotation = dataCount > AUTO_ROTATE_TICK_COUNT && labelWidthPx > AUTO_ROTATE_LABEL_WIDTH_PX;
412
- angle = needsRotation ? -45 : 0;
413
- } // "horizontal" always stays at angle 0
414
-
415
- if (angle === 0) {
416
- // UCM-CHART-16: derive the flat band from the actual tick font size instead of the
417
- // unconditional floor -- `axis?.label` is the SAME signal `resolveChartLayout` uses to decide
418
- // whether an axis title also shares this band (`xTitleBand`), so a titled axis reserves the
419
- // extra room its own gap costs, precisely where the near-miss was.
420
- const hasTitle = Boolean(axis?.label);
421
- const flatTickBand = fontSize > 0
422
- ? Math.ceil(fontSize * TICK_TEXT_HEIGHT_FACTOR) + TICK_BAND_MUI_OVERHEAD_PX
423
- + (hasTitle ? TICK_BAND_TITLE_EXTRA_PX : 0)
424
- : TICK_BAND_BASE_PX;
425
- return { tickBand: Math.max(TICK_BAND_BASE_PX, flatTickBand), angle: 0 };
426
- }
427
- // Rotation is a visual instruction independent of whether a font size is knowable here -- an
428
- // "angled" axis still rotates even when `fontSize` can't be resolved (no theme fallback given),
429
- // it just can't project a tighter band than the floor without one.
430
- if (fontSize === 0) return { tickBand: TICK_BAND_BASE_PX, angle };
431
-
432
- const lineHeight = Number(axis?.tickLabelStyle?.lineHeight) || defaultLineHeight;
433
- const textWidth = labelWidthPx;
434
- const radians = (angle * Math.PI) / 180;
435
- const projectedHeight = Math.abs(Math.cos(radians)) * fontSize * lineHeight
436
- + Math.abs(Math.sin(radians)) * textWidth;
437
- const tickBand = Math.max(TICK_BAND_BASE_PX, Math.ceil(projectedHeight) + ROTATED_TICK_CLEARANCE_PX);
438
- return { tickBand, angle };
439
- }
440
-
441
- function resolveChartHeightPx(size, height, spacing) {
442
- if (height != null) return height;
443
- const units = CHART_SIZE_SPACING_UNITS[size];
444
- if (units == null) {
445
- throw new Error(
446
- `[ui-core-micha] Unknown chart size="${size}". Use "compact" | "standard" | "tall" | `
447
- + '"extra_tall" | "super_tall", or pass height (in px) for the documented escape.',
448
- );
449
- }
450
- // MUI's `theme.spacing(n)` returns a CSS length STRING ("320px"), not a number -- the chart
451
- // height feeds straight into MUI X-Charts' own numeric `height` prop, so it must be unwrapped
452
- // back to a px number via the same `cssLengthToPixels` the rest of this file already uses.
453
- return typeof spacing === 'function' ? cssLengthToPixels(spacing(units)) : units * 8;
454
- }
455
-
456
- /**
457
- * UCM-CHART-12: the one function that owns a chart's geometry (Rule 1-3), replacing
458
- * `resolveChartHeight` + `spaceForRotatedTicks` + `warnOnHeightMismatch`. Returns the COMPLETE
459
- * geometry a preset needs -- wrapper `sx`, chart height, margins, and both resolved axis arrays --
460
- * plus the named `bands` the composition invariant is asserted against:
461
- *
462
- * chartHeight === bands.plotHeight + bands.xAxisBand + bands.xTitleBand + bands.legendBand
463
- * containerWidth === bands.yAxisBand + bands.plotWidth + bands.rightPad (when `containerWidth` is given)
464
- *
465
- * `plotHeight`/`plotWidth` are the RESIDUAL of `chartHeight`/`containerWidth` minus the OTHER
466
- * fields this function itself returns (`margin.top/bottom`, `xAxis[0].height`, `yAxisBand`) --
467
- * by construction, summing them back up can never fail to balance, so that arithmetic identity
468
- * ALONE proves nothing about a `UCM-CHART-10`-shaped bug (a term double-counted between
469
- * `xAxis.height` and `margin.bottom`). What actually catches that class of bug is asserting each
470
- * individual band lands in exactly one place downstream, independently of the others:
471
- * `xAxis[0].height === xAxisBand + xTitleBand` (nothing else may also carry the tick/title
472
- * allowance) and `margin.top + margin.bottom - PACKAGE_DEFAULT_MARGIN.top -
473
- * PACKAGE_DEFAULT_MARGIN.bottom === legendBand` (the ONLY thing margin grows for, over its own
474
- * package baseline, is the legend). The test suite asserts both forms side by side.
475
- *
476
- * `plotHeight` deliberately does NOT subtract the base `PACKAGE_DEFAULT_MARGIN` breathing room --
477
- * that margin is intentional spacing around the plot (THEME-11), not one of the three itemized
478
- * "furniture" bands (tick/title/legend) this WO exists to zero out, so it is treated as part of
479
- * "the rest of the chart", the same as `plotHeight` itself.
480
- *
481
- * `containerWidth` is optional and only meaningful for tests / the browser-level check -- the
482
- * presets themselves stay responsive (no explicit width) and never pass it.
483
- */
484
- export function resolveChartLayout({
485
- size = 'standard',
486
- height,
487
- xAxis,
488
- yAxis,
489
- series = [],
490
- xLabels = 'auto',
491
- hideLegend = false,
492
- legendPosition = DEFAULT_LEGEND_POSITION,
493
- slotProps,
494
- tickFontSize,
495
- spacing,
496
- defaultLineHeight = 1,
497
- measureTextWidth,
498
- containerWidth,
499
- } = {}) {
500
- const chartHeight = resolveChartHeightPx(size, height, spacing);
501
-
502
- const xAxisList = xAxis?.length ? xAxis : [{}];
503
- const primaryXAxis = xAxisList[0];
504
- const { tickBand: xAxisBand, angle } = resolveXAxisGeometry(
505
- primaryXAxis, xLabels, defaultLineHeight, measureTextWidth, tickFontSize,
506
- );
507
- const xTitleBand = primaryXAxis?.label ? AXIS_TITLE_BAND_PX : 0;
508
-
509
- // Codex second-pass review finding: a caller-set `slotProps.legend.position` overrides
510
- // `legendPosition` at RENDER time (`withChartSlotDefaults`) but was invisible here, so the
511
- // margin/height this function reserved could disagree with where the legend actually rendered.
512
- // `resolveLegendPosition` is the one merge both call sites now share.
513
- const effectiveLegendPosition = resolveLegendPosition(legendPosition, slotProps);
514
-
515
- // A `vertical: 'middle'` legend renders on the SIDE (consuming width, not height) -- reviewer
516
- // finding: only branching on 'top' vs. not-'top' always reserved the legend band vertically,
517
- // over-reserving height and under-reserving width for a side-placed legend. This WO's width
518
- // invariant does not itemize a legend-width band, so a side legend simply gets none of the
519
- // height reservation here rather than a wrong one -- consistent with the WO's own text ("0 when
520
- // legend is off or horizontal").
521
- const legendVertical = effectiveLegendPosition.vertical;
522
- const legendIsSidePlaced = legendVertical === 'middle';
523
- const legendHeightBand = hideLegend || legendIsSidePlaced ? 0 : LEGEND_BAND_PX;
524
- const legendOnTop = legendHeightBand > 0 && legendVertical === 'top';
525
-
526
- const sizedYAxis = sizeYAxisForContent(yAxis?.length ? yAxis : [{}], series, tickFontSize);
527
- // Codex second-pass review finding: a dual-axis chart (TimeSeriesChart's `axis: 'secondary'`
528
- // feature, CHART-5) has a SECOND y-axis on the right, consuming its own width -- the resolver
529
- // only ever accounted for `sizedYAxis[0]`. `position` defaults to `'left'` for an unmarked axis
530
- // (matching MUI's own default-assignment rule, same as `sizeYAxisForContent`'s own series-match
531
- // logic above), so only an explicit `'right'` counts as the secondary axis.
532
- const primaryYAxis = sizedYAxis.find((axisEntry) => axisEntry.position !== 'right') ?? sizedYAxis[0];
533
- const secondaryYAxisEntry = sizedYAxis.find((axisEntry) => axisEntry.position === 'right');
534
- const yAxisBand = primaryYAxis?.width ?? 0;
535
- const secondaryYAxisBand = secondaryYAxisEntry?.width ?? 0;
536
- const rightPad = PACKAGE_DEFAULT_MARGIN.right;
537
-
538
- const resolvedXAxis = xAxisList.map((axisEntry, index) => {
539
- if (index !== 0) return axisEntry;
540
- // `xLabels` is the SOLE source of truth for rotation (Rule 3) -- `angle` is always written
541
- // explicitly here, even when 0, rather than only overriding a truthy value, and regardless of
542
- // whether the caller also set an explicit `height` (own `height` bypasses the model's SIZE,
543
- // never its ROTATION -- codex second-pass review finding: the two were previously coupled,
544
- // so a caller-set `height` could silently keep a stale rotated `tickLabelStyle.angle` even
545
- // under `xLabels="horizontal"`). Leaving a stale caller-supplied angle in place when `xLabels`
546
- // resolved to no rotation would let MUI still render a rotated label whose band this resolver
547
- // reserved as if it were flat -- exactly the space/render mismatch this WO exists to close,
548
- // just inverted (under-reservation instead of CHART-10's over-reservation).
549
- const { angle: _callerAngle, ...restTickLabelStyle } = axisEntry.tickLabelStyle || {};
550
- return {
551
- ...axisEntry,
552
- height: axisEntry.height ?? (xAxisBand + xTitleBand),
553
- tickLabelStyle: { ...restTickLabelStyle, angle },
554
- };
555
- });
556
-
557
- const margin = {
558
- top: PACKAGE_DEFAULT_MARGIN.top + (legendOnTop ? legendHeightBand : 0),
559
- bottom: PACKAGE_DEFAULT_MARGIN.bottom + (legendOnTop ? 0 : legendHeightBand),
560
- left: PACKAGE_DEFAULT_MARGIN.left,
561
- right: rightPad + secondaryYAxisBand,
562
- };
563
-
564
- // `plotHeight`/`plotWidth`: the residual of `chartHeight`/`containerWidth` minus the three named
565
- // furniture bands (see this function's own docblock for why this alone does not prove the
566
- // absence of a double-counted term, and what does).
567
- const plotHeight = chartHeight - xAxisBand - xTitleBand - legendHeightBand;
568
- const plotWidth = containerWidth != null
569
- ? containerWidth - yAxisBand - secondaryYAxisBand - rightPad
570
- : undefined;
571
-
572
- return {
573
- chartHeight,
574
- sx: { width: '100%', height: chartHeight },
575
- margin,
576
- xAxis: resolvedXAxis,
577
- yAxis: sizedYAxis,
578
- bands: {
579
- plotHeight,
580
- xAxisBand,
581
- secondaryYAxisBand,
582
- xTitleBand,
583
- legendBand: legendHeightBand,
584
- yAxisBand,
585
- rightPad,
586
- ...(containerWidth != null ? { plotWidth, containerWidth } : {}),
587
- },
588
- };
589
- }
590
-
591
- /**
592
- * UCM-CHART-12 Rule 1/3: `minHeight`, `aspect`, and `margin` are gone from the four chart presets
593
- * -- the model owns sizing and spacing completely. Passing one is a dev-mode error naming its
594
- * replacement (never a silent ignore, so the 38 existing call sites this breaks get a clear
595
- * migration path instead of a layout that quietly stops matching what the prop says) -- gated on
596
- * `NODE_ENV !== 'production'` like the deleted `warnOnHeightMismatch` was, so a shared package
597
- * cannot crash a consumer's production page over a layout prop; loud in dev, inert in prod.
598
- *
599
- * `removedIn` (codex review finding, UCM-CHART-13): a shared helper hardcoding ONE WO/version
600
- * string would misattribute the removal for every OTHER call site that reuses it -- `ChartFrame`
601
- * (UCM-CHART-13, v3.1.0) removes `height`/`aspect` for a different reason, in a different WO, than
602
- * the four presets' own `minHeight`/`aspect`/`margin` (UCM-CHART-12, v3.0.0). The default keeps
603
- * every existing call site's message byte-identical; only a NEW call site needs to override it.
604
- */
605
- export function assertRemovedChartProp(componentName, propName, value, replacement, removedIn = 'UCM-CHART-12, v3.0.0') {
606
- if (value === undefined || process.env.NODE_ENV === 'production') return;
607
- throw new Error(
608
- `[ui-core-micha] <${componentName}> no longer accepts "${propName}" (removed in ${removedIn} `
609
- + `-- see docs/CHART-LAYOUT.md). ${replacement}`,
610
- );
611
- }
612
-
613
- /**
614
- * UCM-CHART-12 (codex second-pass review finding): the single source of truth for where the
615
- * legend actually renders -- `legendPosition` merged with a `slotProps.legend.position` override,
616
- * same precedence `withChartSlotDefaults` already applied. Before this fix, `resolveChartLayout`
617
- * only ever looked at `legendPosition` when deciding how much space to reserve and on which side,
618
- * while `withChartSlotDefaults` (below) separately let `slotProps.legend.position` win at RENDER
619
- * time -- a caller passing both could get a legend rendered top/side while the resolver had
620
- * reserved space for bottom (or none), the exact reserved-vs-rendered mismatch this WO exists to
621
- * close. Both call sites now merge through this one function.
622
- */
623
- export function resolveLegendPosition(legendPosition, slotProps) {
624
- return {
625
- ...DEFAULT_LEGEND_POSITION,
626
- ...legendPosition,
627
- ...slotProps?.legend?.position,
628
- };
629
- }
630
-
631
- export function withChartSlotDefaults(slotProps, legendPosition) {
632
- const position = resolveLegendPosition(legendPosition, slotProps);
633
- return {
634
- ...slotProps,
635
- tooltip: {
636
- trigger: 'axis',
637
- ...slotProps?.tooltip,
638
- },
639
- legend: {
640
- ...slotProps?.legend,
641
- position,
642
- },
643
- };
644
- }