@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,118 +0,0 @@
1
- # Messaging parity outcomes (MSG-3b, extended by MSG-3c and MSG-3d)
2
-
3
- This is the authoritative parity record for the ucm messaging surfaces. It
4
- replaces the former file-level “final parity confirmation,” which was not
5
- sufficient evidence of feature parity. Each entry maps directly to the
6
- numbered checklist in Part A of `work-orders/MSG-3b.md`. `OK` means the
7
- capability is present; `DEV` means an intentional, documented redesign or
8
- non-parity; `BLOCKED` means the dependency (dcm) does not expose the data or
9
- realtime contract needed to implement it truthfully.
10
-
11
- **Binding rule, added by MSG-3c:** every `BLOCKED` entry below must name the
12
- dcm version it was verified blocked against. MSG-3b was itself blocked
13
- against dcm 2.36.1 and recorded seven rows as `BLOCKED` on "a future dcm work
14
- order" — dcm 2.37.0 shipped that work mid-flight, and MSG-3b's own WO-end
15
- review closed without noticing, because it cross-checked the checklist
16
- against ucm's landed code and took the dependency's state from the WO text
17
- instead of the live dependency. A version pin turns that re-check into a
18
- two-minute job instead of an archaeology exercise; `tests/messagingContractConformance.test.js`
19
- now fails the build if a `BLOCKED` line omits one.
20
-
21
- ## Unread lifecycle
22
-
23
- 1. **OK** — Selecting a conversation calls the existing mark-read endpoint and clears its local unread count.
24
- 2. **OK** — Opening a `Thread` also marks its conversation read; the duplicate call is idempotent.
25
- 3. **OK** — Marking a conversation read decrements the global count by that conversation’s prior unread value, clamped at zero.
26
- 4. **OK** — Expanding a reply thread calls the existing thread-read endpoint for its root message.
27
- 5. **OK** — Realtime incoming messages increment unread only outside the active conversation and never double-count optimistic-send reconciliation.
28
-
29
- ## Message actions
30
-
31
- 6. **DEV** — Desktop hover and right-click open the MUI action menu. jg’s dedicated mobile long-press gesture is not reproduced; the same actions remain available through the regular MUI affordance.
32
- 7. **OK** — Authors can edit their own messages inline. Moderator edit affordance is host-supplied because dcm exposes no per-viewer capability field; server authorization remains authoritative.
33
- 8. **OK** — An active editor closes when the message is deleted by another update.
34
- 9. **OK** — Authors and host-designated moderators can delete; the host boolean is UX gating only and dcm enforces the actual permission.
35
- 10. **OK** — Deletion is confirmed in a MUI dialog before the request is sent.
36
- 11. **OK** — A deleted reply/edit target clears dependent composer or editor state.
37
- 12. **OK** — Message text can be copied through the action menu.
38
- 13. **OK** — Reply targeting is present.
39
- 14. **DEV** — Reactions use five compact presets rather than jg’s six-preset presentation.
40
- 15. **DEV** — No full third-party emoji picker is added; the compact preset menu keeps the package dependency-free.
41
- 16. **OK** — Deleted and poll messages do not render the generic action menu.
42
-
43
- ## Direct conversations
44
-
45
- 17. **OK** — `DirectMessageLauncher` starts a direct conversation.
46
- 18. **OK** — The launcher displays the host-supplied candidate list (searchable via an Autocomplete since MSG-19) and selection state.
47
- 19. **OK** — It has distinct empty and in-flight creation states; candidate discovery remains host-owned because dcm has no directory endpoint.
48
- 20. **OK** — Start is disabled until a candidate is selected and during creation.
49
- 21. **OK** — Policy rejections render an API-derived readable error in the dialog.
50
- 22. **OK** — The picker uses a fullscreen MUI dialog at the small breakpoint.
51
-
52
- ## Threading and quoting
53
-
54
- 23. **OK** — Both dcm’s `reply_to_id` and optimistic rows’ `reply_to` are read, so REST and realtime replies group under their root.
55
- 24. **OK** — Replies show a sender and short quoted snippet.
56
- 25. **OK** — Clicking a quote scrolls its source message into view.
57
- 26. **OK** — Quotes of deleted messages render the deleted placeholder rather than stale content.
58
- 27. **OK** (delivered in MSG-3d, was BLOCKED as of dcm 2.37.0) — dcm 2.38.0's `serialize_message` gains viewer-independent `reply_count`/`last_reply_at`, and `thread_last_read_at` is attached REST-only from `MessageThreadReceipt`. `Thread` shows an unread-reply marker on the thread toggle when `last_reply_at > thread_last_read_at`, treats a `null` receipt with replies as unread, and shows no marker (and no toggle) when there are no replies. Opening a thread updates the receipt from the REST response, not a frame (the field is never present on one, by design). An incoming reply's `message` frame also live-advances the cached root's `reply_count`/`last_reply_at`, merged field-wise so a previously-fetched `thread_last_read_at` is never wiped — the same merge-trap discipline MSG-3c's three fixes established, now with its own mandatory regression test.
59
- 28. **OK** — Reply threads load lazily on expand and collapse locally.
60
- 29. **DEV** — Reply composition is supplied through the independently mountable `Composer` target prop rather than jg’s per-thread embedded composer.
61
-
62
- ## Composer
63
-
64
- 30. **OK** — Enter sends and Shift+Enter preserves a newline.
65
- 31. **OK** — Staged files show image thumbnails or file labels, with independent removal.
66
- 32. **OK** — Attachment uploads show real byte-percentage progress when the browser reports an upload total; otherwise an indeterminate indicator is shown while the upload is in flight.
67
- 33. **OK** — Images over 2 MB are compressed client-side to a maximum 2560 px JPEG at quality 0.85, with silent original-file fallback.
68
- 34. **DEV** — Composer emoji insertion uses the existing compact preset set instead of jg’s full picker, while preserving insertion at the cursor.
69
- 35. **OK** — Sending is optimistic and exposes retry plus inline error state.
70
- 36. **OK** — The poll button is disabled while an image is staged as a UI convenience matching jg’s composer behavior; no dcm-side constraint requires it. Poll creation and attachment sending are separate API calls and separate message rows.
71
- 37. **DEV** — Drafts are deliberately reset on conversation changes. jg’s cross-conversation draft leak is not reproduced.
72
-
73
- ## Conversation list
74
-
75
- 38. **OK** (delivered in MSG-3c, was BLOCKED as of dcm 2.36.1) — dcm 2.37.0 serializes `last_message` (`excerpt`/`created_at`) on the conversation payload and emits it live via `conversation_upsert` on new-message send. `ConversationList` was found reading the wrong field name (`last_message.body` instead of the real `excerpt`) — fixed, so the preview now actually renders against the real contract instead of always falling back to the empty state. Reorder and live preview refresh work for new messages; dcm does not publish `conversation_upsert` on message edit/delete, so those cases update on the next REST refresh rather than live (see row 58).
76
- 39. **OK** — `last_message_at` renders through browser-native, locale-aware `Intl.RelativeTimeFormat` labels.
77
- 40. **OK** — The existing MUI `selected={conversation.id === activeConversationId}` wiring produces the `Mui-selected` active-row highlight; it was verified and needs no redundant styling.
78
- 41. **OK** — Unread badges and bold unread titles are present.
79
- 42. **OK** (delivered in MSG-3d, was BLOCKED as of dcm 2.37.0) — dcm 2.38.0's `serialize_conversation_core` includes `external_key`. ucm does not interpret it — jg's `event_all`/`event_team` vocabulary (or any other app's) stays entirely host-side. `ConversationList` accepts an optional `resolveManagedLabel(conversation)` host resolver, called only for `kind: 'managed'` conversations, falling back to the existing title behavior when unset; no app-specific string appears anywhere in `src/` (asserted directly by test).
80
- 43. **DEV** — Generic MUI kind icons replace jg’s richer per-kind/person avatar treatment while retaining type identification.
81
- 44. **OK** — Mute and archive actions are present.
82
-
83
- ## Timeline
84
-
85
- 45. **OK** — New active-conversation messages auto-scroll when the reader was already near the bottom. Scrolled-up readers are not pulled away from history; this near-bottom gate is an intentional usability refinement.
86
- 46. **OK** — Announcement messages render their deep-link action only when a host supplies `onAnnouncementLink`; routing remains a host-app responsibility.
87
- 47. **OK** — Sender names remain visible for group and managed conversations and are suppressed in direct conversations. No sender-side bubble alignment scheme was added because it is not required for this capability.
88
- 48. **OK** — Deleted tombstones, edited marker, and timestamps are rendered.
89
-
90
- ## Read receipts
91
-
92
- 49. **OK** — Read ticks mount only for the current user’s own persisted messages.
93
- 50. **OK** — Non-direct recipient detail supplied by dcm is shown through a click/keyboard-openable popover list rather than hover-only tooltip. Direct conversations always suppress the detail control; dcm’s recipient-detail payload remains the moderator authorization boundary because no generic client moderator field exists.
94
-
95
- ## Polls
96
-
97
- 51. **OK** (delivered in MSG-3c, was BLOCKED as of dcm 2.36.1) — dcm 2.37.0's `serialize_poll` renders question, options and per-option `vote_count` through `PollCard`. Fixed against the real shape: the initial-selection effect previously read non-existent per-option `selected`/`voted` flags — it now derives from `voted_option_ids`, which dcm adds only to the create/vote/close REST responses (never to `serialize_poll` itself or the `poll_updated` frame), and correctly shows no known selection when that field is absent rather than guessing.
98
- 52. **DEV** (delivered in MSG-3c, was BLOCKED as of dcm 2.36.1) — dcm 2.37.0's poll options carry `voters` as user ids only, never names, and there is no user-directory endpoint (the same structural limit already established for the DM candidate list, row 19). `PollCard` renders the vote count and marks the viewer's own vote (derived from `voted_option_ids`) rather than a per-voter name list, which the dependency cannot supply.
99
- 53. **OK** (delivered in MSG-3c, was BLOCKED as of dcm 2.36.1) — `closed_at` is in the real poll projection; voting is disabled once set. The poll creator's close permission was found broken against the real contract (`created_by_id` is the real field; the prior code read a non-existent `created_by.id`) — fixed.
100
- 54. **OK** — Composer starts at two options and disables adding options once it reaches ten; the server’s two-option minimum remains enforced.
101
- 55. **OK** — Vote and close write paths are wired; full read rendering is now delivered per rows 51-53.
102
-
103
- ## Live synchronization
104
-
105
- 56. **OK** (delivered in MSG-3c, was BLOCKED as of dcm 2.36.1) — dcm 2.37.0 emits `reaction` frames. The handler had never run against a real frame; verification found it wholesale-replacing the message's reaction list with the server's aggregate-only projection (`{emoji, count}`, never a per-viewer `reacted` flag, by design) on every arrival — silently wiping every previously-known `reacted` flag on that message, not just whichever emoji changed. The same defect existed in the REST-confirm path for the viewer's own toggle. Both fixed to merge by emoji, preserving previously-known `reacted` state instead of discarding it.
106
- 57. **OK** (delivered in MSG-3c, was BLOCKED as of dcm 2.36.1) — dcm 2.37.0 emits `poll_updated` frames. Same defect class as row 56, found while implementing row 51-53: the frame handler wholesale-replaced a message's `poll` object, silently dropping a REST-known `voted_option_ids` the moment any `poll_updated` frame arrived (e.g. a vote-count bump from another viewer). Fixed to carry the previous value forward when the incoming frame is silent about it, per the WO's explicit "must not clear or invert the viewer's own vote state" requirement.
107
- 58. **OK** (delivered in MSG-3c, was BLOCKED as of dcm 2.36.1) — new messages live-reorder the conversation list and refresh its preview via `conversation_upsert` (row 38). dcm does not publish `conversation_upsert` on message edit or delete (only on new-message send — verified against every `_publish(..., "conversation_upsert", ...)` call site in `services.py`), so an edit/delete of a conversation's last message updates the preview on the next REST refresh rather than live; this is a dcm-side gap outside this WO's scope, not a ucm limitation.
108
- 59. **OK** — dcm’s emitted message, message-edited, and message-deleted frames are handled live.
109
-
110
- ## Required backend follow-up
111
-
112
- None outstanding. This checklist has **zero `BLOCKED` entries** as of MSG-3d. MSG-2c (dcm 2.37.0)
113
- delivered what MSG-3b's own audit had recorded as blocking rows 38, 51–53 and 56–58 — MSG-3c
114
- re-verified each against the live dependency and delivered them. MSG-2d (dcm 2.38.0) delivered what
115
- remained blocking rows 27 and 42 — MSG-3d re-verified and delivered them the same way. Every row of
116
- the original MSG-3b checklist is now either implemented or a recorded deliberate deviation; see the
117
- rows above for what was actually built, including the client-side bugs found and fixed only once real
118
- server data was checked against, not assumed from the WO text or the doc's own prior claims.
package/index.html DELETED
@@ -1,12 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>ui-core-micha development harness</title>
7
- </head>
8
- <body>
9
- <div id="root"></div>
10
- <script type="module" src="/dev/main.jsx"></script>
11
- </body>
12
- </html>
package/renovate.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
- "extends": [
4
- "github>bigler-webapps/renovate-config",
5
- "github>bigler-webapps/renovate-config:auto-merge"
6
- ],
7
- "baseBranchPatterns": [
8
- "main"
9
- ]
10
- }
@@ -1,415 +0,0 @@
1
- // NO SHEBANG. Run this with an explicit interpreter: `node scripts/chart-api-census.mjs
2
- // <workspace-root>`, which is how it is documented and how it is run.
3
- //
4
- // A `#!` line here is invisible to direct execution -- Node strips it -- but breaks anything
5
- // that loads this file through an ESM graph rather than running it: Vite's module runner
6
- // (hence vitest) rejects `#` as an illegal token, and other bundlers/parsers may too.
7
- // UCM-CHART-14 shipped with one. The script kept working and its output stayed correct, so
8
- // nothing user-facing complained; what broke was its own test suite, silently reduced to
9
- // 0 collected tests (`SyntaxError: Invalid or unexpected token`). Do not re-add it.
10
- // UCM-CHART-14: a parser, not a grep. This is the whole point of this file -- five wrong consumer
11
- // counts in this series (see work-orders/UCM-CHART-14.md) all came from a grep pattern that could
12
- // not see JSX structure: a prop several lines below the component name, an arrow function's `=>`
13
- // inside a prop value, an apostrophe inside a comment, a `>` inside a nested `{}` expression. A
14
- // regex line-match cannot tell any of those apart from the thing it's looking for. This script
15
- // tracks string/template/comment/regex-literal state and brace depth in ONE continuous pass over
16
- // each file, so its answer is reproducible output, not a claim someone has to trust.
17
- //
18
- // Second review round (both `reviewer` and `ui_reviewer`, independently) found the first version's
19
- // three-pass design (a raw `RegExp.exec` tag-open search, oblivious to string/comment context, then
20
- // a per-tag bounds scan, then a per-tag attribute scan) could itself produce a WRONG, non-UNPARSED
21
- // answer -- worse than an honest UNPARSED, because nothing flags it: JSX-looking text inside a
22
- // string or comment counted as a real element, and a regex literal's `}`/`>` characters (e.g.
23
- // `data={/[}>]/}`) corrupted brace-depth tracking and closed a tag early. Both are fixed here by
24
- // giving every stage the SAME shared state, tracked once, forward, per file.
25
- //
26
- // Dev script only -- never imported by the published package (kept out of package.json's `files`/
27
- // `exports`), never writes anything, and degrades to "not present" for a missing/frontend-less
28
- // sibling repo rather than throwing.
29
-
30
- import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
31
- import { join, relative, dirname } from 'node:path';
32
- import { fileURLToPath, pathToFileURL } from 'node:url';
33
-
34
- // The five props this WO's series has spent five work orders removing/relocating -- what a census
35
- // exists to count is exactly "who still passes one of these".
36
- export const TARGET_PROPS = ['aspect', 'height', 'minHeight', 'margin', 'xAxisAngle'];
37
- export const TARGET_COMPONENTS = ['BarChart', 'LineChart', 'ScatterChart', 'TimeSeriesChart', 'ChartFrame'];
38
- const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
39
- const SKIP_DIR_NAMES = new Set(['node_modules', 'dist', 'build', '.git']);
40
- const PACKAGE_SPECIFIER = '@micha.bigler/ui-core-micha';
41
-
42
- /**
43
- * Finds every `import { ... } from '@micha.bigler/ui-core-micha'` in `source` and returns a map
44
- * of LOCAL name -> CANONICAL export name, restricted to `TARGET_COMPONENTS`. Import declarations
45
- * are a much simpler grammar than JSX (no nested arbitrary expressions in the specifier list), so
46
- * a regex is appropriate here -- unlike the JSX scan below, which is not.
47
- */
48
- function findImportedLocalNames(source) {
49
- const localToCanonical = new Map();
50
- const importRe = /import\s*\{([^}]*)\}\s*from\s*['"]@micha\.bigler\/ui-core-micha['"]/g;
51
- let match;
52
- while ((match = importRe.exec(source)) !== null) {
53
- const specifiers = match[1].split(',').map((s) => s.trim()).filter(Boolean);
54
- for (const spec of specifiers) {
55
- const asMatch = spec.match(/^(\w+)\s+as\s+(\w+)$/);
56
- const canonical = asMatch ? asMatch[1] : spec;
57
- const local = asMatch ? asMatch[2] : spec;
58
- if (TARGET_COMPONENTS.includes(canonical)) localToCanonical.set(local, canonical);
59
- }
60
- }
61
- return localToCanonical;
62
- }
63
-
64
- function lineNumberAt(source, index) {
65
- let line = 1;
66
- for (let i = 0; i < index; i += 1) if (source[i] === '\n') line += 1;
67
- return line;
68
- }
69
-
70
- const IDENT_START = /[A-Za-z_$]/;
71
- const IDENT_CHAR = /[A-Za-z0-9_$]/;
72
- // Regex-literal-start heuristic: a `/` is treated as opening a regex literal (not division) only
73
- // when the last significant character before it is one of these -- the common "a regex can only
74
- // start where a VALUE is expected, not follow one" rule real JS tokenizers use. Not a full parser;
75
- // documented, and sufficient for `data={/[}>]/}`-shaped prop values, the concrete case found in
76
- // review. A `/` following an identifier/number/`)`/`]` is treated as division and left alone.
77
- //
78
- // Regression found DURING this same fix's own verification run (before it ever reached review):
79
- // `<` and `>` were originally in this set on the theory that `/` can start a regex right after a
80
- // comparison operator -- but `<`/`>` are also exactly what precede the `/` in EVERY JSX closing
81
- // tag (`</Something>`) and self-closing tag (`Something />`). With them included, `</ChartFrame>`
82
- // itself was misread as a regex-literal start, consuming everything up to the next stray `/` in
83
- // the file and corrupting brace depth for the rest of the scan -- turning every multi-line
84
- // ChartFrame usage in the estate into a false UNPARSED. A `/` genuinely starting a regex right
85
- // after `<`/`>` (e.g. `a < /regex/.test(b)`) does not occur in practice in this codebase; dropped.
86
- const REGEX_PRECEDING_CHARS = new Set(['(', '{', '[', ',', ';', ':', '=', '&', '|', '!', '?', '+', '-', '*', '%', '^', '~']);
87
-
88
- /**
89
- * Extracts top-level (tag-depth-0, RELATIVE TO `tagSource`) attribute names from `tagSource` (a
90
- * full, already-correctly-bounded `<Name ...>` span). Shares the same string/template/comment/
91
- * regex tracking as the outer scanner, so a prop value containing a regex literal, a nested
92
- * object, or a comment can never be mistaken for a top-level attribute name -- `sx={{ aspect: 1
93
- * }}` never reports a false `aspect` (an object key inside an expression, not a JSX attribute),
94
- * and `data={/[}>]/}` never corrupts this function's own depth count either.
95
- */
96
- function extractTopLevelAttributeNames(tagSource) {
97
- const found = new Set();
98
- let depth = 0;
99
- let inString = null;
100
- let inLineComment = false;
101
- let inBlockComment = false;
102
- let inRegex = false;
103
- let inRegexClass = false;
104
- let lastSignificant = null;
105
-
106
- for (let i = 0; i < tagSource.length; i += 1) {
107
- const c = tagSource[i];
108
- const next = tagSource[i + 1];
109
-
110
- if (inLineComment) { if (c === '\n') inLineComment = false; continue; }
111
- if (inBlockComment) { if (c === '*' && next === '/') { inBlockComment = false; i += 1; } continue; }
112
- if (inRegex) {
113
- if (c === '\\') { i += 1; continue; }
114
- if (c === '[') { inRegexClass = true; continue; }
115
- if (c === ']') { inRegexClass = false; continue; }
116
- if (c === '/' && !inRegexClass) {
117
- inRegex = false;
118
- let j = i + 1;
119
- while (j < tagSource.length && /[a-z]/i.test(tagSource[j])) j += 1;
120
- i = j - 1;
121
- }
122
- continue;
123
- }
124
- if (inString) {
125
- if (c === '\\') { i += 1; continue; }
126
- if (c === inString) inString = null;
127
- continue;
128
- }
129
- if (c === '/' && next === '/') { inLineComment = true; i += 1; continue; }
130
- if (c === '/' && next === '*') { inBlockComment = true; i += 1; continue; }
131
- if (c === '/' && (lastSignificant === null || REGEX_PRECEDING_CHARS.has(lastSignificant))) {
132
- inRegex = true; inRegexClass = false; lastSignificant = '/'; continue;
133
- }
134
- if (c === '"' || c === "'" || c === '`') { inString = c; lastSignificant = c; continue; }
135
- if (c === '{') { depth += 1; lastSignificant = c; continue; }
136
- if (c === '}') { depth = Math.max(0, depth - 1); lastSignificant = c; continue; }
137
-
138
- if (depth === 0 && IDENT_START.test(c) && (i === 0 || !IDENT_CHAR.test(tagSource[i - 1]))) {
139
- let j = i + 1;
140
- while (j < tagSource.length && IDENT_CHAR.test(tagSource[j])) j += 1;
141
- found.add(tagSource.slice(i, j));
142
- lastSignificant = tagSource[j - 1];
143
- i = j - 1;
144
- continue;
145
- }
146
- if (!/\s/.test(c)) lastSignificant = c;
147
- }
148
- return found;
149
- }
150
-
151
- /**
152
- * The single-pass scanner. Walks `source` exactly ONCE, maintaining ONE shared state for strings,
153
- * template literals (+ `${}` expression nesting), line/block comments, regex literals, and brace
154
- * depth -- so tag-open detection and tag-close detection agree on where the real code is, instead
155
- * of a separate context-blind pass for each. A `<Name` is only recognised as a real JSX tag open
156
- * when the scanner is not currently inside a string/comment/regex, which is what stops `// <BarChart
157
- * aspect />` or `"<BarChart aspect />"` from being counted. Multiple target tags can be pending at
158
- * once (a component nested inside another's own prop expression, e.g. `<ChartFrame content={<>
159
- * <BarChart aspect /></>} />`) -- each is closed independently, at the exact brace depth it opened
160
- * at, so a nested tag closing does not consume or corrupt the outer tag's own remaining scan.
161
- *
162
- * A tag still open when the file ends is reported UNPARSED, never dropped (Definition of Done).
163
- */
164
- function scanForComponents(source, localToCanonical) {
165
- const results = [];
166
- const openTags = []; // { canonical, tagStart, closeAtDepth, line }
167
- const localNames = [...localToCanonical.keys()];
168
-
169
- let inString = null;
170
- let inLineComment = false;
171
- let inBlockComment = false;
172
- let inRegex = false;
173
- let inRegexClass = false;
174
- let depth = 0;
175
- const templateDepthStack = [];
176
- let lastSignificant = null;
177
-
178
- const closePendingAtCurrentDepth = (closeIndex) => {
179
- for (let k = openTags.length - 1; k >= 0; k -= 1) {
180
- if (openTags[k].closeAtDepth === depth) {
181
- const tag = openTags.splice(k, 1)[0];
182
- const tagSource = source.slice(tag.tagStart, closeIndex + 1);
183
- const attrNames = extractTopLevelAttributeNames(tagSource);
184
- const props = {};
185
- for (const prop of TARGET_PROPS) props[prop] = attrNames.has(prop);
186
- results.push({ component: tag.canonical, line: tag.line, unparsed: false, props });
187
- }
188
- }
189
- };
190
-
191
- for (let i = 0; i < source.length; i += 1) {
192
- const c = source[i];
193
- const next = source[i + 1];
194
-
195
- if (inLineComment) { if (c === '\n') inLineComment = false; continue; }
196
- if (inBlockComment) { if (c === '*' && next === '/') { inBlockComment = false; i += 1; } continue; }
197
- if (inRegex) {
198
- if (c === '\\') { i += 1; continue; }
199
- if (c === '[') { inRegexClass = true; continue; }
200
- if (c === ']') { inRegexClass = false; continue; }
201
- if (c === '/' && !inRegexClass) {
202
- inRegex = false;
203
- let j = i + 1;
204
- while (j < source.length && /[a-z]/i.test(source[j])) j += 1;
205
- i = j - 1;
206
- }
207
- continue;
208
- }
209
- if (inString) {
210
- if (c === '\\') { i += 1; continue; }
211
- if (inString === '`' && c === '$' && next === '{') {
212
- templateDepthStack.push(depth);
213
- depth += 1;
214
- i += 1;
215
- inString = null;
216
- lastSignificant = '{';
217
- continue;
218
- }
219
- if (c === inString) inString = null;
220
- continue;
221
- }
222
-
223
- if (c === '/' && next === '/') { inLineComment = true; i += 1; continue; }
224
- if (c === '/' && next === '*') { inBlockComment = true; i += 1; continue; }
225
- if (c === '/' && (lastSignificant === null || REGEX_PRECEDING_CHARS.has(lastSignificant))) {
226
- inRegex = true; inRegexClass = false; lastSignificant = '/'; continue;
227
- }
228
- if (c === '"' || c === "'" || c === '`') { inString = c; lastSignificant = c; continue; }
229
-
230
- if (c === '{') { depth += 1; lastSignificant = c; continue; }
231
- if (c === '}') {
232
- if (templateDepthStack.length && depth - 1 === templateDepthStack[templateDepthStack.length - 1]) {
233
- templateDepthStack.pop();
234
- depth -= 1;
235
- inString = '`';
236
- lastSignificant = '}';
237
- continue;
238
- }
239
- depth = Math.max(0, depth - 1);
240
- lastSignificant = c;
241
- continue;
242
- }
243
-
244
- if (c === '<') {
245
- let matchedName = null;
246
- for (const name of localNames) {
247
- if (source.startsWith(name, i + 1)) {
248
- const after = source[i + 1 + name.length];
249
- if (after === undefined || /[\s/>]/.test(after)) { matchedName = name; break; }
250
- }
251
- }
252
- if (matchedName) {
253
- openTags.push({
254
- canonical: localToCanonical.get(matchedName),
255
- tagStart: i,
256
- closeAtDepth: depth,
257
- line: lineNumberAt(source, i),
258
- });
259
- i += matchedName.length;
260
- lastSignificant = matchedName[matchedName.length - 1];
261
- continue;
262
- }
263
- lastSignificant = c;
264
- continue;
265
- }
266
-
267
- if (c === '>') {
268
- closePendingAtCurrentDepth(i);
269
- lastSignificant = c;
270
- continue;
271
- }
272
-
273
- if (!/\s/.test(c)) lastSignificant = c;
274
- }
275
-
276
- for (const tag of openTags) {
277
- results.push({ component: tag.canonical, line: tag.line, unparsed: true, props: {} });
278
- }
279
- return results;
280
- }
281
-
282
- /**
283
- * The pure analysis function -- no filesystem access, so this is what the fixture tests below
284
- * exercise directly. Returns one entry per JSX element found whose tag name resolves (via the
285
- * file's own imports) to one of `TARGET_COMPONENTS`.
286
- *
287
- * Documented, accepted heuristic limitation (unchanged from the first version, not solved by the
288
- * single-pass rewrite): `<` is only ever treated as a JSX tag open, never as a less-than
289
- * comparison against an uppercase-starting identifier (`foo < BarChart`). Real component
290
- * references are never used in a numeric/relational comparison in practice, so this is judged a
291
- * theoretical gap, not a practical one -- unlike the string/comment/regex-literal blindness fixed
292
- * above, which were concrete, plausible false positives.
293
- */
294
- export function findChartApiUsage(source) {
295
- const localToCanonical = findImportedLocalNames(source);
296
- if (localToCanonical.size === 0) return [];
297
- return scanForComponents(source, localToCanonical);
298
- }
299
-
300
- function walkSourceFiles(rootDir) {
301
- const files = [];
302
- function walk(dir) {
303
- let entries;
304
- try {
305
- entries = readdirSync(dir, { withFileTypes: true });
306
- } catch {
307
- return;
308
- }
309
- for (const entry of entries) {
310
- if (entry.name.startsWith('.')) continue;
311
- if (SKIP_DIR_NAMES.has(entry.name)) continue;
312
- const full = join(dir, entry.name);
313
- if (entry.isDirectory()) {
314
- walk(full);
315
- } else if (entry.isFile()) {
316
- const dot = entry.name.lastIndexOf('.');
317
- if (dot === -1) continue;
318
- if (SOURCE_EXTENSIONS.has(entry.name.slice(dot))) files.push(full);
319
- }
320
- }
321
- }
322
- walk(rootDir);
323
- return files;
324
- }
325
-
326
- /**
327
- * Locates an app's frontend source root: `frontend/src` first (this estate's usual layout), then
328
- * a bare `src/` fallback. Returns `null` (never throws) if neither exists -- a repo with no
329
- * frontend, or absent entirely, is reported as "not present", not an error.
330
- */
331
- function findAppSourceRoot(appDir) {
332
- const candidates = [join(appDir, 'frontend', 'src'), join(appDir, 'src')];
333
- for (const candidate of candidates) {
334
- if (existsSync(candidate) && statSync(candidate).isDirectory()) return candidate;
335
- }
336
- return null;
337
- }
338
-
339
- export function runCensus(workspaceRoot, { excludeDirs = ['ui-core-micha'] } = {}) {
340
- let appNames;
341
- try {
342
- appNames = readdirSync(workspaceRoot, { withFileTypes: true })
343
- .filter((e) => e.isDirectory() && !e.name.startsWith('.') && !excludeDirs.includes(e.name))
344
- .map((e) => e.name);
345
- } catch (err) {
346
- return { workspaceRoot, apps: [], error: `workspace root unreadable: ${err.message}` };
347
- }
348
-
349
- const apps = appNames.map((appName) => {
350
- const appDir = join(workspaceRoot, appName);
351
- const sourceRoot = findAppSourceRoot(appDir);
352
- if (!sourceRoot) return { app: appName, present: false, entries: [] };
353
-
354
- const entries = [];
355
- for (const file of walkSourceFiles(sourceRoot)) {
356
- let source;
357
- try {
358
- source = readFileSync(file, 'utf8');
359
- } catch {
360
- continue; // unreadable file (permissions, symlink race) -- not a parse failure, skip quietly
361
- }
362
- if (!source.includes(PACKAGE_SPECIFIER)) continue; // fast path: no ucm import at all
363
- const usages = findChartApiUsage(source);
364
- for (const usage of usages) {
365
- entries.push({ ...usage, file: relative(workspaceRoot, file).replace(/\\/g, '/') });
366
- }
367
- }
368
- return { app: appName, present: true, entries };
369
- });
370
-
371
- return { workspaceRoot, apps };
372
- }
373
-
374
- /**
375
- * Reports EVERY element found (Definition of Done: "every `<ChartFrame>` and every ucm chart
376
- * preset element"), not just the ones carrying a target prop -- a clean element is printed as
377
- * `(clean)`, not omitted, so "this app has zero matching elements at all" and "this app has
378
- * elements, all clean" stay visibly distinct instead of looking identical.
379
- */
380
- function formatReport(result) {
381
- const lines = [];
382
- let totalUnparsed = 0;
383
- for (const { app, present, entries } of result.apps) {
384
- if (!present) { lines.push(`${app}: not present (no frontend/src or src)`); continue; }
385
- if (entries.length === 0) continue;
386
- lines.push(`${app}:`);
387
- for (const entry of entries) {
388
- if (entry.unparsed) {
389
- totalUnparsed += 1;
390
- lines.push(` UNPARSED ${entry.file}:${entry.line} <${entry.component}> -- could not close this tag, not counted as clean`);
391
- continue;
392
- }
393
- const activeProps = TARGET_PROPS.filter((p) => entry.props[p]);
394
- const propsText = activeProps.length > 0 ? activeProps.join(' ') : '(clean)';
395
- lines.push(` ${entry.file}:${entry.line} <${entry.component} ${propsText}>`);
396
- }
397
- }
398
- if (totalUnparsed > 0) {
399
- lines.push('');
400
- lines.push(`${totalUnparsed} tag(s) could not be parsed -- see UNPARSED lines above. Not counted as clean.`);
401
- }
402
- return lines.join('\n');
403
- }
404
-
405
- // `pathToFileURL` (not manual string-building) is what makes this comparison correct on Windows,
406
- // where a bare `file://${path}` is missing the extra slash before the drive letter that
407
- // `import.meta.url` actually has (`file:///C:/...`) -- a naive comparison silently never matches,
408
- // so `node scripts/chart-api-census.mjs` would run to completion with zero output and no error.
409
- const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
410
- if (isMain) {
411
- const workspaceRoot = process.argv[2] || join(dirname(fileURLToPath(import.meta.url)), '..', '..');
412
- const result = runCensus(workspaceRoot);
413
- // eslint-disable-next-line no-console
414
- console.log(formatReport(result));
415
- }