@mui/internal-docs-infra 0.11.1-canary.9 → 0.11.1

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 (319) hide show
  1. package/ChunkProvider/ChunkContext.d.mts +10 -0
  2. package/ChunkProvider/ChunkContext.mjs +15 -0
  3. package/ChunkProvider/ChunkProvider.d.mts +14 -0
  4. package/ChunkProvider/ChunkProvider.mjs +38 -0
  5. package/ChunkProvider/PreloadContext.d.mts +14 -0
  6. package/ChunkProvider/PreloadContext.mjs +18 -0
  7. package/ChunkProvider/PreloadProvider.d.mts +13 -0
  8. package/ChunkProvider/PreloadProvider.mjs +33 -0
  9. package/ChunkProvider/index.d.mts +7 -0
  10. package/ChunkProvider/index.mjs +7 -0
  11. package/ChunkProvider/types.d.mts +23 -0
  12. package/ChunkProvider/types.mjs +1 -0
  13. package/ChunkProvider/usePreload.d.mts +8 -0
  14. package/ChunkProvider/usePreload.mjs +21 -0
  15. package/CodeControllerContext/CodeControllerContext.d.mts +11 -0
  16. package/CodeControllerContext/CodeControllerContext.mjs +2 -1
  17. package/CodeHighlighter/CodeHighlighter.d.mts +15 -1
  18. package/CodeHighlighter/CodeHighlighter.mjs +97 -319
  19. package/CodeHighlighter/CodeHighlighterChunk.d.mts +42 -0
  20. package/CodeHighlighter/CodeHighlighterChunk.mjs +77 -0
  21. package/CodeHighlighter/CodeHighlighterClient.mjs +597 -128
  22. package/CodeHighlighter/CodeHighlighterContext.d.mts +57 -1
  23. package/CodeHighlighter/CodeHighlighterFallbackContext.d.mts +14 -2
  24. package/CodeHighlighter/CodeHighlighterFallbackContext.mjs +1 -3
  25. package/CodeHighlighter/CodeInitialSourceLoader.d.mts +10 -0
  26. package/CodeHighlighter/CodeInitialSourceLoader.mjs +108 -0
  27. package/CodeHighlighter/CodeSourceLoader.d.mts +11 -0
  28. package/CodeHighlighter/CodeSourceLoader.mjs +128 -0
  29. package/CodeHighlighter/buildCodeHighlighterChunkProps.d.mts +47 -0
  30. package/CodeHighlighter/buildCodeHighlighterChunkProps.mjs +61 -0
  31. package/CodeHighlighter/buildStringFallback.d.mts +29 -0
  32. package/CodeHighlighter/buildStringFallback.mjs +42 -0
  33. package/CodeHighlighter/codeToFallbackProps.d.mts +31 -2
  34. package/CodeHighlighter/codeToFallbackProps.mjs +347 -42
  35. package/CodeHighlighter/createClientProps.d.mts +17 -0
  36. package/CodeHighlighter/createClientProps.mjs +78 -0
  37. package/CodeHighlighter/errors.d.mts +6 -0
  38. package/CodeHighlighter/errors.mjs +10 -0
  39. package/CodeHighlighter/fallbackCompression.d.mts +96 -0
  40. package/CodeHighlighter/fallbackCompression.mjs +253 -0
  41. package/CodeHighlighter/fallbackFormat.d.mts +137 -0
  42. package/CodeHighlighter/fallbackFormat.mjs +422 -0
  43. package/CodeHighlighter/index.d.mts +4 -1
  44. package/CodeHighlighter/index.mjs +3 -1
  45. package/CodeHighlighter/mergeComments.d.mts +38 -0
  46. package/CodeHighlighter/mergeComments.mjs +80 -0
  47. package/CodeHighlighter/prepareInitialSource.d.mts +42 -0
  48. package/CodeHighlighter/prepareInitialSource.mjs +292 -0
  49. package/CodeHighlighter/resolveFallbackCritical.d.mts +23 -0
  50. package/CodeHighlighter/resolveFallbackCritical.mjs +44 -0
  51. package/CodeHighlighter/types.d.mts +272 -8
  52. package/CodeHighlighter/useCodeFallback.d.mts +94 -0
  53. package/CodeHighlighter/useCodeFallback.mjs +204 -0
  54. package/CodeHighlighter/useGrammarsReady.d.mts +18 -0
  55. package/CodeHighlighter/useGrammarsReady.mjs +45 -0
  56. package/CodeHighlighter/useSpeculativeCodePreload.d.mts +26 -0
  57. package/CodeHighlighter/useSpeculativeCodePreload.mjs +40 -0
  58. package/CodeHighlighter/useSpeculativeEditingPreload.d.mts +33 -0
  59. package/CodeHighlighter/useSpeculativeEditingPreload.mjs +58 -0
  60. package/CodeHighlighter/useSpeculativeGrammarPreload.d.mts +23 -0
  61. package/CodeHighlighter/useSpeculativeGrammarPreload.mjs +31 -0
  62. package/CodeHighlighter/useSpeculativeUseCodePreload.d.mts +22 -0
  63. package/CodeHighlighter/useSpeculativeUseCodePreload.mjs +41 -0
  64. package/CodeProvider/CodeContext.d.mts +47 -12
  65. package/CodeProvider/CodeContext.mjs +7 -0
  66. package/CodeProvider/CodeProvider.d.mts +4 -2
  67. package/CodeProvider/CodeProvider.mjs +40 -102
  68. package/CodeProvider/CodeProviderLazy.d.mts +40 -0
  69. package/CodeProvider/CodeProviderLazy.mjs +96 -0
  70. package/CodeProvider/constants.d.mts +26 -0
  71. package/CodeProvider/constants.mjs +24 -0
  72. package/CodeProvider/createParseSourceWorkerClient.d.mts +6 -0
  73. package/CodeProvider/createParseSourceWorkerClient.mjs +22 -2
  74. package/CodeProvider/index.d.mts +2 -1
  75. package/CodeProvider/index.mjs +9 -1
  76. package/CodeProvider/parseSourceWorker.mjs +33 -0
  77. package/CodeProvider/useCodeProviderValue.d.mts +54 -0
  78. package/CodeProvider/useCodeProviderValue.mjs +188 -0
  79. package/CoordinatedLazy/ChunkServerLoader.d.mts +25 -0
  80. package/CoordinatedLazy/ChunkServerLoader.mjs +97 -0
  81. package/CoordinatedLazy/CoordinatedContentContext.d.mts +15 -0
  82. package/CoordinatedLazy/CoordinatedContentContext.mjs +22 -0
  83. package/CoordinatedLazy/CoordinatedFallbackContext.d.mts +11 -0
  84. package/CoordinatedLazy/CoordinatedFallbackContext.mjs +13 -0
  85. package/CoordinatedLazy/CoordinatedGateContext.d.mts +14 -0
  86. package/CoordinatedLazy/CoordinatedGateContext.mjs +19 -0
  87. package/CoordinatedLazy/CoordinatedLazy.d.mts +14 -0
  88. package/CoordinatedLazy/CoordinatedLazy.mjs +86 -0
  89. package/CoordinatedLazy/CoordinatedLazyClient.d.mts +24 -0
  90. package/CoordinatedLazy/CoordinatedLazyClient.mjs +65 -0
  91. package/CoordinatedLazy/LazyContent.d.mts +26 -0
  92. package/CoordinatedLazy/LazyContent.mjs +80 -0
  93. package/CoordinatedLazy/LazyContentServer.d.mts +18 -0
  94. package/CoordinatedLazy/LazyContentServer.mjs +25 -0
  95. package/CoordinatedLazy/buildChunkRenderInputs.d.mts +8 -0
  96. package/CoordinatedLazy/buildChunkRenderInputs.mjs +35 -0
  97. package/CoordinatedLazy/createCoordinatedLazy.d.mts +32 -0
  98. package/CoordinatedLazy/createCoordinatedLazy.mjs +127 -0
  99. package/CoordinatedLazy/index.d.mts +14 -0
  100. package/CoordinatedLazy/index.mjs +18 -0
  101. package/CoordinatedLazy/resolveChunkRender.d.mts +26 -0
  102. package/CoordinatedLazy/resolveChunkRender.mjs +73 -0
  103. package/CoordinatedLazy/types.d.mts +408 -0
  104. package/CoordinatedLazy/types.mjs +1 -0
  105. package/CoordinatedLazy/useChunk.d.mts +30 -0
  106. package/CoordinatedLazy/useChunk.mjs +135 -0
  107. package/CoordinatedLazy/useCoordinatedFallback.d.mts +12 -0
  108. package/CoordinatedLazy/useCoordinatedFallback.mjs +40 -0
  109. package/CoordinatedLazy/useCoordinatedSwap.d.mts +16 -0
  110. package/CoordinatedLazy/useCoordinatedSwap.mjs +124 -0
  111. package/LICENSE +1 -1
  112. package/abstractCreateDemo/abstractCreateDemo.d.mts +54 -3
  113. package/abstractCreateDemo/abstractCreateDemo.mjs +47 -7
  114. package/abstractCreateDemo/resolveDemoFlag.d.mts +20 -0
  115. package/abstractCreateDemo/resolveDemoFlag.mjs +25 -0
  116. package/abstractCreateStream/abstractCreateStream.d.mts +18 -0
  117. package/abstractCreateStream/abstractCreateStream.mjs +45 -0
  118. package/abstractCreateStream/index.d.mts +2 -0
  119. package/abstractCreateStream/index.mjs +1 -0
  120. package/abstractCreateStream/types.d.mts +34 -0
  121. package/abstractCreateStream/types.mjs +1 -0
  122. package/abstractCreateTypes/TypeCode.mjs +12 -11
  123. package/abstractCreateTypes/typesToJsx.mjs +30 -9
  124. package/cli/ensureDemoClients.mjs +4 -148
  125. package/cli/ensureDemoPages.d.mts +45 -0
  126. package/cli/ensureDemoPages.mjs +99 -0
  127. package/cli/fileUtils/index.d.mts +11 -0
  128. package/cli/fileUtils/index.mjs +48 -0
  129. package/cli/findDemoIndexFiles.d.mts +15 -0
  130. package/cli/findDemoIndexFiles.mjs +121 -0
  131. package/cli/index.mjs +1 -1
  132. package/cli/loadNextConfig.d.mts +25 -0
  133. package/cli/loadNextConfig.mjs +60 -1
  134. package/cli/runBrowser.mjs +1 -1
  135. package/cli/runValidate.mjs +44 -1
  136. package/package.json +84 -4
  137. package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasis.mjs +30 -0
  138. package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasisLazy.d.mts +17 -0
  139. package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasisLazy.mjs +52 -0
  140. package/pipeline/hastUtils/frameFallbackFromSpans.d.mts +18 -0
  141. package/pipeline/hastUtils/frameFallbackFromSpans.mjs +24 -0
  142. package/pipeline/hastUtils/hast.d.mts +27 -0
  143. package/pipeline/hastUtils/hastCompression.d.mts +3 -1
  144. package/pipeline/hastUtils/hastCompression.mjs +9 -1
  145. package/pipeline/hastUtils/hastDecompress.mjs +10 -4
  146. package/pipeline/hastUtils/hastDictionary.mjs +9 -0
  147. package/pipeline/hastUtils/hastUtils.d.mts +4 -3
  148. package/pipeline/hastUtils/hastUtils.mjs +24 -12
  149. package/pipeline/hastUtils/index.d.mts +2 -1
  150. package/pipeline/hastUtils/index.mjs +2 -1
  151. package/pipeline/hastUtils/stripHighlightingSpans.d.mts +6 -2
  152. package/pipeline/hastUtils/stripHighlightingSpans.mjs +22 -10
  153. package/pipeline/lintJavascriptDemoFocus/lintJavascriptDemoFocus.mjs +10 -7
  154. package/pipeline/loadIsomorphicCodeVariant/applyCodeTransform.d.mts +31 -13
  155. package/pipeline/loadIsomorphicCodeVariant/applyCodeTransform.mjs +50 -55
  156. package/pipeline/loadIsomorphicCodeVariant/applyCodeTransformWithComments.d.mts +78 -0
  157. package/pipeline/loadIsomorphicCodeVariant/applyCodeTransformWithComments.mjs +405 -0
  158. package/pipeline/loadIsomorphicCodeVariant/computeHastDeltas.d.mts +5 -5
  159. package/pipeline/loadIsomorphicCodeVariant/computeHastDeltas.mjs +36 -66
  160. package/pipeline/loadIsomorphicCodeVariant/decodeHastSource.d.mts +23 -0
  161. package/pipeline/loadIsomorphicCodeVariant/decodeHastSource.mjs +92 -0
  162. package/pipeline/loadIsomorphicCodeVariant/decodeSource.d.mts +19 -0
  163. package/pipeline/loadIsomorphicCodeVariant/decodeSource.mjs +25 -0
  164. package/pipeline/loadIsomorphicCodeVariant/decodeSourceToText.d.mts +17 -0
  165. package/pipeline/loadIsomorphicCodeVariant/decodeSourceToText.mjs +26 -0
  166. package/pipeline/loadIsomorphicCodeVariant/diffHast.d.mts +26 -2
  167. package/pipeline/loadIsomorphicCodeVariant/diffHast.mjs +563 -19
  168. package/pipeline/loadIsomorphicCodeVariant/embedTransforms.d.mts +49 -0
  169. package/pipeline/loadIsomorphicCodeVariant/embedTransforms.mjs +152 -0
  170. package/pipeline/loadIsomorphicCodeVariant/findExpandingRanges.d.mts +51 -0
  171. package/pipeline/loadIsomorphicCodeVariant/findExpandingRanges.mjs +161 -0
  172. package/pipeline/loadIsomorphicCodeVariant/flattenCodeVariant.mjs +6 -3
  173. package/pipeline/loadIsomorphicCodeVariant/getAvailableTransforms.d.mts +12 -0
  174. package/pipeline/loadIsomorphicCodeVariant/getAvailableTransforms.mjs +44 -0
  175. package/pipeline/loadIsomorphicCodeVariant/getInitialVisibleSourceLines.d.mts +16 -0
  176. package/pipeline/loadIsomorphicCodeVariant/getInitialVisibleSourceLines.mjs +74 -0
  177. package/pipeline/loadIsomorphicCodeVariant/loadCodeFallback.mjs +17 -5
  178. package/pipeline/loadIsomorphicCodeVariant/loadIsomorphicCodeVariant.mjs +229 -15
  179. package/pipeline/loadIsomorphicCodeVariant/transformSource.d.mts +2 -2
  180. package/pipeline/loadIsomorphicCodeVariant/transformSource.mjs +56 -22
  181. package/pipeline/loadPrecomputedCodeHighlighter/loadPrecomputedCodeHighlighter.d.mts +18 -0
  182. package/pipeline/loadPrecomputedCodeHighlighter/loadPrecomputedCodeHighlighter.mjs +11 -7
  183. package/pipeline/loadServerTypes/hastTypeUtils.d.mts +2 -2
  184. package/pipeline/loadServerTypes/hastTypeUtils.mjs +4 -4
  185. package/pipeline/loadServerTypes/loadServerTypes.mjs +1 -1
  186. package/pipeline/loadServerTypesMeta/extractJSDocText.d.mts +14 -0
  187. package/pipeline/loadServerTypesMeta/extractJSDocText.mjs +60 -0
  188. package/pipeline/loadServerTypesMeta/processTypes.mjs +43 -46
  189. package/pipeline/loadServerTypesText/order.mjs +1 -1
  190. package/pipeline/loadServerTypesText/parseTypesMarkdown.mjs +3 -1
  191. package/pipeline/loaderUtils/index.d.mts +0 -1
  192. package/pipeline/loaderUtils/index.mjs +0 -1
  193. package/pipeline/loaderUtils/parseImportsAndComments.d.mts +5 -1
  194. package/pipeline/loaderUtils/parseImportsAndComments.mjs +19 -9
  195. package/pipeline/loaderUtils/resolveModulePath.mjs +23 -1
  196. package/pipeline/parseCreateFactoryCall/parseCreateFactoryCall.d.mts +12 -0
  197. package/pipeline/parseCreateFactoryCall/parseCreateFactoryCall.mjs +17 -13
  198. package/pipeline/parseSource/addLineGutters.mjs +45 -11
  199. package/pipeline/parseSource/calculateFrameRanges.d.mts +22 -0
  200. package/pipeline/parseSource/calculateFrameRanges.mjs +69 -25
  201. package/pipeline/parseSource/detectGrammarScopes.d.mts +13 -0
  202. package/pipeline/parseSource/detectGrammarScopes.mjs +35 -0
  203. package/pipeline/parseSource/extendSyntaxTokens.mjs +501 -43
  204. package/pipeline/parseSource/frameVisibility.d.mts +47 -0
  205. package/pipeline/parseSource/frameVisibility.mjs +114 -0
  206. package/pipeline/parseSource/grammarCache.d.mts +33 -0
  207. package/pipeline/parseSource/grammarCache.mjs +73 -0
  208. package/pipeline/parseSource/grammarLoaders.d.mts +14 -0
  209. package/pipeline/parseSource/grammarLoaders.mjs +24 -0
  210. package/pipeline/parseSource/grammarMaps.d.mts +21 -1
  211. package/pipeline/parseSource/grammarMaps.mjs +36 -0
  212. package/pipeline/parseSource/isFrameSpan.d.mts +19 -0
  213. package/pipeline/parseSource/isFrameSpan.mjs +24 -0
  214. package/pipeline/parseSource/parseSource.d.mts +41 -6
  215. package/pipeline/parseSource/parseSource.mjs +184 -36
  216. package/pipeline/parseSource/redistributeFrameFallbacks.d.mts +40 -0
  217. package/pipeline/parseSource/redistributeFrameFallbacks.mjs +138 -0
  218. package/pipeline/parseSource/restructureFrames.d.mts +5 -0
  219. package/pipeline/parseSource/restructureFrames.mjs +179 -16
  220. package/pipeline/syncPageIndex/metadataToMarkdown.mjs +6 -2
  221. package/pipeline/transformHtmlCodeBlock/transformHtmlCodeBlock.d.mts +26 -0
  222. package/pipeline/transformHtmlCodeBlock/transformHtmlCodeBlock.mjs +181 -114
  223. package/pipeline/transformHtmlCodeInline/removeSuffixFromHighlightedNodes.d.mts +12 -0
  224. package/pipeline/transformHtmlCodeInline/removeSuffixFromHighlightedNodes.mjs +52 -0
  225. package/pipeline/transformHtmlCodeInline/transformHtmlCodeInline.mjs +22 -1
  226. package/pipeline/transformTypescriptToJavascript/removeTypes.d.mts +5 -8
  227. package/pipeline/transformTypescriptToJavascript/removeTypes.mjs +27 -93
  228. package/useCode/EditableEngine.d.mts +233 -0
  229. package/useCode/EditableEngine.mjs +1712 -0
  230. package/useCode/EditingEngine.d.mts +13 -0
  231. package/useCode/EditingEngine.mjs +14 -0
  232. package/useCode/Pre.browser.mjs +5 -1
  233. package/useCode/Pre.d.mts +127 -1
  234. package/useCode/Pre.mjs +417 -165
  235. package/useCode/SourceEditingEngine.d.mts +50 -0
  236. package/useCode/SourceEditingEngine.mjs +461 -0
  237. package/useCode/TransformEngine.d.mts +39 -0
  238. package/useCode/TransformEngine.mjs +208 -0
  239. package/useCode/editingEngineCache.d.mts +29 -0
  240. package/useCode/editingEngineCache.mjs +68 -0
  241. package/useCode/sourceLineCounts.d.mts +80 -0
  242. package/useCode/sourceLineCounts.mjs +284 -0
  243. package/useCode/subscribeToggleNudge.d.mts +3 -0
  244. package/useCode/subscribeToggleNudge.mjs +95 -0
  245. package/useCode/transformEngineCache.d.mts +21 -0
  246. package/useCode/transformEngineCache.mjs +60 -0
  247. package/useCode/useCode.d.mts +140 -1
  248. package/useCode/useCode.mjs +250 -19
  249. package/useCode/useCodeUtils.d.mts +131 -20
  250. package/useCode/useCodeUtils.mjs +267 -194
  251. package/useCode/useCopyFunctionality.d.mts +13 -1
  252. package/useCode/useCopyFunctionality.mjs +39 -9
  253. package/useCode/useEditable.browser.mjs +10 -2
  254. package/useCode/useEditable.d.mts +27 -106
  255. package/useCode/useEditable.integration.browser.d.mts +1 -0
  256. package/useCode/useEditable.integration.browser.mjs +870 -0
  257. package/useCode/useEditable.mjs +198 -1247
  258. package/useCode/useEditableUtils.d.mts +50 -1
  259. package/useCode/useEditableUtils.mjs +29 -0
  260. package/useCode/useFileNavigation.d.mts +91 -3
  261. package/useCode/useFileNavigation.mjs +201 -41
  262. package/useCode/useHighlightGate.d.mts +17 -0
  263. package/useCode/useHighlightGate.mjs +147 -0
  264. package/useCode/useSourceEditing.d.mts +8 -0
  265. package/useCode/useSourceEditing.mjs +158 -314
  266. package/useCode/useSourceEnhancing.d.mts +5 -1
  267. package/useCode/useSourceEnhancing.mjs +22 -36
  268. package/useCode/useTransformManagement.d.mts +93 -5
  269. package/useCode/useTransformManagement.mjs +496 -28
  270. package/useCode/useTransitionPhase.d.mts +24 -0
  271. package/useCode/useTransitionPhase.mjs +49 -0
  272. package/useCode/useUIState.d.mts +2 -2
  273. package/useCode/useUIState.mjs +8 -8
  274. package/useCode/useVariantSelection.d.mts +130 -6
  275. package/useCode/useVariantSelection.mjs +529 -93
  276. package/useCodeWindow/useCodeWindow.d.mts +19 -2
  277. package/useCodeWindow/useCodeWindow.mjs +98 -71
  278. package/useCoordinated/coordinatePreference.d.mts +439 -0
  279. package/useCoordinated/coordinatePreference.mjs +951 -0
  280. package/useCoordinated/coordinatePreference.testUtils.d.mts +21 -0
  281. package/useCoordinated/coordinatePreference.testUtils.mjs +69 -0
  282. package/useCoordinated/createSettleGate.d.mts +96 -0
  283. package/useCoordinated/createSettleGate.mjs +171 -0
  284. package/useCoordinated/index.d.mts +8 -0
  285. package/useCoordinated/index.mjs +8 -0
  286. package/useCoordinated/layoutShiftGate.d.mts +24 -0
  287. package/useCoordinated/layoutShiftGate.mjs +79 -0
  288. package/useCoordinated/pageSettleGate.d.mts +11 -0
  289. package/useCoordinated/pageSettleGate.mjs +13 -0
  290. package/useCoordinated/scheduleTasks.d.mts +23 -0
  291. package/useCoordinated/scheduleTasks.mjs +45 -0
  292. package/useCoordinated/useCoordinated.d.mts +193 -0
  293. package/useCoordinated/useCoordinated.mjs +469 -0
  294. package/useCoordinated/useCoordinatedLazy.d.mts +17 -0
  295. package/useCoordinated/useCoordinatedLazy.mjs +38 -0
  296. package/useCoordinated/useCoordinatedLocalStorage.d.mts +16 -0
  297. package/useCoordinated/useCoordinatedLocalStorage.mjs +22 -0
  298. package/useCoordinated/useCoordinatedPreference.d.mts +20 -0
  299. package/useCoordinated/useCoordinatedPreference.mjs +26 -0
  300. package/useCoordinated/useSettleGate.d.mts +11 -0
  301. package/useCoordinated/useSettleGate.mjs +34 -0
  302. package/useDemo/exportVariant.d.mts +12 -5
  303. package/useDemo/exportVariant.mjs +59 -5
  304. package/useDemo/useDemo.d.mts +5 -2
  305. package/useScrollAnchor/useScrollAnchor.mjs +28 -5
  306. package/useStream/index.d.mts +6 -0
  307. package/useStream/index.mjs +6 -0
  308. package/useStream/streamChunks.d.mts +23 -0
  309. package/useStream/streamChunks.mjs +85 -0
  310. package/useStream/types.d.mts +45 -0
  311. package/useStream/types.mjs +1 -0
  312. package/useStream/useStream.d.mts +57 -0
  313. package/useStream/useStream.mjs +119 -0
  314. package/useStream/useStreamController.d.mts +15 -0
  315. package/useStream/useStreamController.mjs +90 -0
  316. package/withDocsInfra/withDocsInfra.d.mts +19 -0
  317. package/withDocsInfra/withDocsInfra.mjs +13 -5
  318. package/pipeline/loaderUtils/convertCommentsToOneIndexed.d.mts +0 -8
  319. package/pipeline/loaderUtils/convertCommentsToOneIndexed.mjs +0 -16
@@ -1,8 +1,12 @@
1
1
  import * as React from 'react';
2
2
  import { usePreference } from "../usePreference/index.mjs";
3
3
  import { useUrlHashState } from "../useUrlHashState/index.mjs";
4
+ import { useCoordinated } from "../useCoordinated/index.mjs";
5
+ import { useHighlightGate } from "./useHighlightGate.mjs";
6
+ import { useTransitionPhase } from "./useTransitionPhase.mjs";
4
7
  import { isHashRelevantToDemo } from "./useFileNavigation.mjs";
5
8
  import { toKebabCase } from "../pipeline/loaderUtils/toKebabCase.mjs";
9
+ import { variantHasLayoutShift } from "./sourceLineCounts.mjs";
6
10
 
7
11
  /**
8
12
  * Parses the variant name from a URL hash
@@ -49,17 +53,63 @@ function parseVariantFromHash(urlHash, mainSlug, variantKeys) {
49
53
  }
50
54
  return null;
51
55
  }
56
+ /**
57
+ * Resolve a stored / hash / initial preference into a valid variant
58
+ * key. Priority: URL hash > localStorage > initialVariant > first
59
+ * variant. Returns an empty string only when no variants are
60
+ * available — callers should treat that as "no selection".
61
+ */
62
+ function resolveVariantKey(hashVariant, storedValue, initialVariant, variantKeys) {
63
+ if (hashVariant && variantKeys.includes(hashVariant)) {
64
+ return hashVariant;
65
+ }
66
+ if (storedValue && variantKeys.includes(storedValue)) {
67
+ return storedValue;
68
+ }
69
+ if (initialVariant && variantKeys.includes(initialVariant)) {
70
+ return initialVariant;
71
+ }
72
+ return variantKeys[0] || '';
73
+ }
74
+
52
75
  /**
53
76
  * Hook for managing variant selection and providing variant-related data
54
77
  * Priority: URL hash > localStorage > initialVariant > first variant
55
78
  * When hash has a variant, it overrides localStorage and is saved to localStorage
79
+ *
80
+ * Wraps the selection in `useCoordinated` so sibling demos that share
81
+ * the same variant set commit variant swaps together — preventing
82
+ * staggered layout shifts when multiple demos on the page react to
83
+ * the same preference change.
84
+ */
85
+ /**
86
+ * Minimum coordinator barrier wait used when `variantSwapDelay` is
87
+ * unset or zero but a layout-shift-prone swap still needs to land on
88
+ * the same frame as peer demos. One animation frame at ~60fps so the
89
+ * coordinated paint feels instantaneous but every peer commits
90
+ * together. Mirrors the same constant in `useTransformManagement`.
56
91
  */
92
+ const MIN_VARIANT_WAIT_MS = 16;
93
+
94
+ /**
95
+ * Time after an originator's announce by which all peers should have
96
+ * acked. Beyond this, the barrier surfaces `pendingVariantKey` so
97
+ * consumers can render a transient loading indicator while continuing
98
+ * to wait up to `ultimateTimeoutMs` (10s). Mirrors
99
+ * `TRANSFORM_GRACE_PERIOD_MS`.
100
+ */
101
+ const VARIANT_GRACE_PERIOD_MS = 300;
57
102
  export function useVariantSelection({
58
103
  effectiveCode,
59
104
  initialVariant,
60
105
  variantType,
61
106
  mainSlug,
62
- saveHashVariantToLocalStorage = 'on-interaction'
107
+ saveHashVariantToLocalStorage = 'on-interaction',
108
+ variantLayoutShift = 'selected',
109
+ selectedFileName,
110
+ expanded,
111
+ variantSwapDelay,
112
+ deferHighlight
63
113
  }) {
64
114
  // Get variant keys from effective code
65
115
  const variantKeys = React.useMemo(() => {
@@ -78,102 +128,374 @@ export function useVariantSelection({
78
128
  return null;
79
129
  });
80
130
 
81
- // Track if the last change was user-initiated (to prevent hash from overriding)
82
- const isUserInitiatedChange = React.useRef(false);
83
- // Track previous hash variant to detect hash changes
84
- const prevHashVariant = React.useRef(hashVariant);
85
- // Track previous storedValue to detect localStorage changes
86
- const prevStoredValue = React.useRef(storedValue);
87
-
88
- // Determine initial variant: hash > localStorage > initialVariant > first variant
89
- const [selectedVariantKey, setSelectedVariantKeyState] = React.useState(() => {
90
- // Priority 1: Hash variant
91
- if (hashVariant && variantKeys.includes(hashVariant)) {
92
- return hashVariant;
93
- }
94
- // Priority 2: localStorage
95
- if (storedValue && variantKeys.includes(storedValue)) {
96
- return storedValue;
97
- }
98
- // Priority 3: initialVariant prop
99
- if (initialVariant && variantKeys.includes(initialVariant)) {
100
- return initialVariant;
101
- }
102
- // Priority 4: First available variant
103
- return variantKeys[0] || '';
131
+ // When a delay is configured, start from the boot-time value
132
+ // (initialVariant or first variant), then adopt the stored value on
133
+ // a later tick as a normal coordinated receiver swap. This allows
134
+ // the initial→stored transition to open `data-transforming` windows
135
+ // instead of resolving to the stored variant before first paint —
136
+ // most visibly when the full content component replaces a loading
137
+ // skeleton and needs to animate from the default variant to the
138
+ // user's saved preference.
139
+ //
140
+ // The bootstrap is gated on the *stored* variant's source being
141
+ // available as HAST (not a raw string). On a fresh mount, only the
142
+ // default variant typically has source data; non-default variants
143
+ // are lazy-loaded as URL refs and then parsed into HAST by the
144
+ // highlighter pipeline. Firing the bootstrap before that lands
145
+ // commits a swap to a variant whose `<Pre>` has no HAST to render,
146
+ // producing the user-visible sequence: unhighlighted initial paint
147
+ // → swap-and-animate against still-unhighlighted content → content
148
+ // snaps to highlighted text mid-animation. Waiting for the stored
149
+ // variant's HAST guarantees the receiver-flow animation plays once,
150
+ // against a fully-highlighted target tree, so the visible order is
151
+ // swap → highlight → animate.
152
+ //
153
+ // We also gate on the parent's `deferHighlight` flipping to `false`.
154
+ // `deferHighlight` reflects the current variant's highlight pipeline
155
+ // state (parsing + transforms). Without this wait the combobox
156
+ // pending value flips to the stored variant as soon as its HAST is
157
+ // available, but the receiver flow's `preload` then blocks on
158
+ // `awaitHighlight` (which tracks the *current* variant's
159
+ // `deferHighlight`). The visible result is a large gap where the
160
+ // combo says "Tailwind" but the content is still CSS Modules with
161
+ // `data-transforming` running against the stale tree. Waiting for
162
+ // `deferHighlight=false` before flipping `allowStoredBootstrap`
163
+ // keeps the combo and the content swap in lockstep — and, paired
164
+ // with the unconditional `storedValueForResolve` gate below, means
165
+ // the swap commit lands on an already-highlighted destination
166
+ // instead of flashing the stored variant through its raw-source
167
+ // fallback while its parse completes.
168
+ const storedVariantSourceLoaded = React.useMemo(() => {
169
+ // When there's no stored preference (or it's not a valid variant key),
170
+ // the bootstrap doesn't change the resolved variant, so no wait needed.
171
+ if (!storedValue || !variantKeys.includes(storedValue)) {
172
+ return true;
173
+ }
174
+ const variantEntry = effectiveCode[storedValue];
175
+ if (!variantEntry || typeof variantEntry === 'string') {
176
+ return false;
177
+ }
178
+ // Require HAST (not a raw string source) so the receiver-flow
179
+ // animation runs against the already-highlighted target tree.
180
+ return variantEntry.source != null && typeof variantEntry.source !== 'string';
181
+ }, [storedValue, variantKeys, effectiveCode]);
182
+ // One-way latch: opens after the stored variant's HAST is
183
+ // available and the parent highlighter is no longer deferring
184
+ // highlights. Driven by an effect (not a render-time set-state)
185
+ // so the first render always resolves to `initialVariant`; the
186
+ // effect then flips the latch on a later tick, the resolved value
187
+ // swings to `storedValue`, and the change drives the coordinated
188
+ // receiver-flow swap (with its `data-transforming` animation).
189
+ // Adopting the stored value synchronously on the first render
190
+ // would skip the swap entirely — no animation, and
191
+ // `pendingBootstrap` would never latch so `useCode` wouldn't
192
+ // suppress highlight on the outgoing initial variant.
193
+ const [allowStoredBootstrap, setAllowStoredBootstrap] = React.useState(false);
194
+ React.useEffect(() => {
195
+ if (!storedVariantSourceLoaded) {
196
+ return;
197
+ }
198
+ if (deferHighlight) {
199
+ return;
200
+ }
201
+ // Intentional later-tick latch: see the bootstrap-gate comment above.
202
+ // Flipping this during render skips the receiver-flow swap animation and
203
+ // prevents `pendingBootstrap` from ever latching.
204
+ // eslint-disable-next-line react-hooks/set-state-in-effect
205
+ setAllowStoredBootstrap(true);
206
+ }, [storedVariantSourceLoaded, deferHighlight]);
207
+
208
+ // Barrier wait length. Falls back to one frame when
209
+ // `variantSwapDelay` isn't configured so peers still align on the
210
+ // same paint without making the click feel sluggish.
211
+ const hasDelay = typeof variantSwapDelay === 'number' && variantSwapDelay > 0;
212
+ const effectiveSwapWindowMs = hasDelay ? variantSwapDelay : MIN_VARIANT_WAIT_MS;
213
+
214
+ // Hold the resolved value on the initial variant until the
215
+ // bootstrap gate (`allowStoredBootstrap`) releases whenever there
216
+ // is a highlight pipeline to wait for — either because a
217
+ // `variantSwapDelay` is configured (delayed swaps must always
218
+ // settle on HAST) or because a `CodeHighlighter` parent is
219
+ // publishing a `deferHighlight` signal (in which case the stored
220
+ // variant's parse hasn't necessarily completed even though the
221
+ // coordinator's `minWaitMs` is zero). Without this second clause,
222
+ // the no-delay path would commit the swap on the very first
223
+ // render and the stored variant would flash through its raw-source
224
+ // fallback while its parse completed. When neither condition
225
+ // applies (bare `useCode` consumers in tests / non-highlighted
226
+ // contexts) we skip the gate so raw-string sources continue to
227
+ // bootstrap synchronously.
228
+ const shouldGateBootstrap = hasDelay || deferHighlight !== undefined;
229
+ const storedValueForResolve = shouldGateBootstrap && !allowStoredBootstrap ? null : storedValue;
230
+
231
+ // Resolved underlying value combining hash and localStorage (and
232
+ // the initial/first-variant fallbacks). This is what `useCoordinated`
233
+ // observes as its external source of truth — any change to hash or
234
+ // storage opens a receiver-flow barrier so peer demos commit
235
+ // together.
236
+ const resolvedValue = React.useMemo(() => resolveVariantKey(hashVariant, storedValueForResolve, initialVariant, variantKeys), [hashVariant, storedValueForResolve, initialVariant, variantKeys]);
237
+
238
+ // Stable underlying tuple. The setter is intentionally a no-op:
239
+ // localStorage writes are performed *eagerly* by
240
+ // `setSelectedVariantAsUser` (so user intent is persisted and
241
+ // broadcast to peer demos on the same tick as the click), not
242
+ // lazily on barrier commit. The engine sees the eager write echo
243
+ // back through `usePreference` and dedupes it via its
244
+ // `inFlightTargetRef` guard so the receiver flow doesn't double-fire.
245
+ const underlying = React.useMemo(() => [resolvedValue, () => {}], [resolvedValue]);
246
+
247
+ // Coordinator key. Demos sharing the same variant set belong to the
248
+ // same coordination group. Use the variant-type bucket when set so
249
+ // unrelated variant sets that share a type (e.g. Yarn/Npm/Pnpm
250
+ // installs) still coordinate even if their key list differs.
251
+ const channelKey = React.useMemo(() => {
252
+ // Single-variant demos have nothing to coordinate (no choice to
253
+ // swap to) — skip the coordinator entirely so we don't trigger
254
+ // its localStorage reads. Mirrors `usePreference`'s
255
+ // single-element-array short-circuit.
256
+ if (variantKeys.length < 2) {
257
+ return null;
258
+ }
259
+ if (variantType) {
260
+ return `variant:${variantType}`;
261
+ }
262
+ return `variant:${[...variantKeys].sort().join(':')}`;
263
+ }, [variantKeys, variantType]);
264
+
265
+ // Stable per-hook identity used by the coordinator to track which
266
+ // demos have acked the current barrier. `React.useId` gives us a
267
+ // unique-per-mount string without the impure `Math.random()` /
268
+ // `Date.now()` dance, and stays stable across re-renders.
269
+ const demoId = React.useId();
270
+
271
+ // Latest props read by the engine's `causesLayoutShift` callback.
272
+ // Kept in a ref so the callback itself can be referentially stable.
273
+ const layoutShiftPropsRef = React.useRef({
274
+ effectiveCode,
275
+ variantLayoutShift,
276
+ selectedFileName,
277
+ expanded
104
278
  });
279
+ // eslint-disable-next-line react-hooks/refs
280
+ layoutShiftPropsRef.current = {
281
+ effectiveCode,
282
+ variantLayoutShift,
283
+ selectedFileName,
284
+ expanded
285
+ };
105
286
 
106
- // Track selected variant key in a ref for use in effect without causing re-runs
107
- const selectedVariantKeyRef = React.useRef(selectedVariantKey);
108
- React.useEffect(() => {
109
- selectedVariantKeyRef.current = selectedVariantKey;
287
+ // Latest committed variant key read by `causesLayoutShift` to
288
+ // compare against the swap target. Initialized to `''` (rather than
289
+ // `resolvedValue`) because the post-`useCoordinated` assignment
290
+ // below is the single source of truth: relying on the init argument
291
+ // would leave the ref stale if `resolvedValue` later changed
292
+ // outside of a commit. The empty string is a sentinel —
293
+ // `variantHasLayoutShift` treats a falsy `from` key as "no shift".
294
+ const committedRef = React.useRef('');
295
+ const causesLayoutShift = React.useCallback(target => {
296
+ const props = layoutShiftPropsRef.current;
297
+ return variantHasLayoutShift(props.effectiveCode, committedRef.current, target, {
298
+ mode: props.variantLayoutShift,
299
+ selectedFileName: props.selectedFileName,
300
+ expanded: props.expanded
301
+ });
302
+ }, []);
303
+
304
+ // Track whether the most recent commit landed on `hashVariant` so
305
+ // we can opt-in save to localStorage under `'on-load'` semantics.
306
+ const lastStoredValueRef = React.useRef(storedValue);
307
+ // eslint-disable-next-line react-hooks/refs
308
+ lastStoredValueRef.current = storedValue;
309
+ const lastHashVariantRef = React.useRef(hashVariant);
310
+ // eslint-disable-next-line react-hooks/refs
311
+ lastHashVariantRef.current = hashVariant;
312
+ const onCommit = React.useCallback(target => {
313
+ // Mirror the historical `'on-load'` behavior: when a swap
314
+ // commits to whatever the hash currently points at, persist
315
+ // that variant to localStorage so a subsequent visit without a
316
+ // hash still lands on the same variant.
317
+ if (saveHashVariantToLocalStorage === 'on-load' && lastHashVariantRef.current && lastHashVariantRef.current === target && target !== lastStoredValueRef.current) {
318
+ setStoredValue(target);
319
+ }
320
+ }, [saveHashVariantToLocalStorage, setStoredValue]);
321
+
322
+ // Tracks the previous render's committed variant so we can decide
323
+ // the originator's `minWaitMs` synchronously inside
324
+ // `selectVariantDispatch`: leaving a non-empty variant needs the
325
+ // pre-swap expand window. Initialised to the resolved boot value
326
+ // (mirroring `useTransformManagement`'s `prevCommittedTransformRef`)
327
+ // so the very first user-driven dispatch already sees a non-empty
328
+ // ref and applies `variantSwapDelay`. The empty-string sentinel is
329
+ // kept as a fallback for the (rare) case where the variant list
330
+ // resolves empty during boot.
331
+ const prevCommittedVariantKeyRef = React.useRef(resolvedValue);
332
+
333
+ // Hold the originator's coordinator barrier open while the
334
+ // highlighter pipeline is still working on the incoming variant.
335
+ // Without this, an interactive variant swap can commit after
336
+ // `variantSwapDelay` even when the new variant's `parseCode` /
337
+ // `computeHastDeltas` hasn't landed — the incoming `<Pre>` paints
338
+ // from raw source then snaps to highlighted text a frame later.
339
+ // See `useHighlightGate` for the gate plumbing.
340
+ const awaitHighlight = useHighlightGate(!!deferHighlight);
341
+ const preload = React.useCallback((_target, signal) => {
342
+ const wait = awaitHighlight(signal);
343
+ if (wait === null) {
344
+ return undefined;
345
+ }
346
+ return wait;
347
+ }, [awaitHighlight]);
348
+ const [committedVariantKey, selectVariantDispatch, coordinationExtras] = useCoordinated(underlying, {
349
+ channelKey,
350
+ peerId: demoId,
351
+ causesLayoutShift,
352
+ preload,
353
+ onCommit,
354
+ // eslint-disable-next-line react-hooks/refs
355
+ minWaitMs: hasDelay && prevCommittedVariantKeyRef.current !== '' ? variantSwapDelay : 0,
356
+ multiPeerExtraMinWaitMs: hasDelay ? 0 : MIN_VARIANT_WAIT_MS,
357
+ lazyMinWaitMs: hasDelay ? variantSwapDelay : 0,
358
+ gracePeriodMs: VARIANT_GRACE_PERIOD_MS
110
359
  });
111
360
 
112
- // When hash changes and has a variant, override current selection
113
- // When hash is removed, fall back to localStorage
361
+ // eslint-disable-next-line react-hooks/refs
362
+ prevCommittedVariantKeyRef.current = committedVariantKey;
363
+
364
+ // Keep the outgoing-tree probe in sync with whatever the engine
365
+ // just committed. Mutating a ref during render is safe — React
366
+ // tolerates it as long as the value derives deterministically from
367
+ // inputs of the current render.
368
+ // eslint-disable-next-line react-hooks/refs
369
+ committedRef.current = committedVariantKey;
370
+
371
+ // User-facing selected variant key. Prefer the pending value so UI
372
+ // controls (tabs, dropdowns) react immediately to a click, even if
373
+ // the engine is briefly holding the visible value back for a
374
+ // coordinated barrier.
375
+ const selectedVariantKey = coordinationExtras.pendingValue;
376
+
377
+ // Track the initial committed variant so we can detect the very
378
+ // first commit (whether driven by bootstrap or a user click that
379
+ // races bootstrap). `pendingBootstrap` derives from this so callers
380
+ // can suppress highlighting of the outgoing initial variant when a
381
+ // stored-preference swap is known to be in flight — without it,
382
+ // the initial variant briefly paints fully highlighted right at
383
+ // the moment the combobox flips to the stored value, then flashes
384
+ // through the animation against stale content before the incoming
385
+ // tree commits. Latching on first commit (not on
386
+ // `committedVariantKey === storedValue`) keeps the gate honest if
387
+ // the user clicks during the bootstrap window: their click commits
388
+ // a different variant and `pendingBootstrap` releases so the new
389
+ // selection lights up normally.
390
+ const [initialCommittedVariantKey, setInitialCommittedVariantKey] = React.useState(null);
391
+ const [hasCommittedPastInitial, setHasCommittedPastInitial] = React.useState(false);
114
392
  React.useEffect(() => {
115
- // Skip if this was a user-initiated change
116
- if (isUserInitiatedChange.current) {
117
- // Only reset the flag once the hash has actually been cleared
118
- if (hashVariant === null && urlHash === null) {
119
- isUserInitiatedChange.current = false;
120
- }
121
- prevHashVariant.current = hashVariant;
393
+ if (hasCommittedPastInitial || !committedVariantKey) {
394
+ return;
395
+ }
396
+ // Intentional later-tick latch: see the bootstrap-gate comment above.
397
+ // This freezes the first non-empty committed value and only later detects
398
+ // moving past it; moving the detection into render shifts exactly when
399
+ // `pendingBootstrap` releases relative to paint, which the
400
+ // highlight-suppression sequence was tuned around.
401
+ /* eslint-disable react-hooks/set-state-in-effect */
402
+ if (initialCommittedVariantKey === null) {
403
+ setInitialCommittedVariantKey(committedVariantKey);
404
+ } else if (committedVariantKey !== initialCommittedVariantKey) {
405
+ setHasCommittedPastInitial(true);
406
+ }
407
+ /* eslint-enable react-hooks/set-state-in-effect */
408
+ }, [committedVariantKey, hasCommittedPastInitial, initialCommittedVariantKey]);
409
+
410
+ // Reset both bootstrap latches whenever the storage bucket
411
+ // identity changes (the consumer swaps in a new lesson with a
412
+ // different variant set, or `variantType` switches the
413
+ // `usePreference` bucket). Without this, a second payload
414
+ // inherits the first payload's "already bootstrapped" state and
415
+ // its newly resolved stored preference is never adopted.
416
+ //
417
+ // The identity is derived from the bucket coordinates — the same
418
+ // values `usePreference` keys off — rather than `effectiveCode`'s
419
+ // object identity. `CodeHighlighterClient` rebuilds and republishes
420
+ // the code object during ordinary parse / transform progress, so
421
+ // keying on `effectiveCode` would re-arm the bootstrap path in
422
+ // the middle of a user-driven swap (whose stored value is
423
+ // persisted eagerly by `setSelectedVariantAsUser`), making an
424
+ // interactive variant change look like an initial-mount stored
425
+ // bootstrap and replaying it again.
426
+ //
427
+ // Tracking the previous identity in state (rather than a ref)
428
+ // follows React's "adjusting state when a prop changes" pattern
429
+ // so the reset stays a synchronous render-time decision without
430
+ // violating the refs-during-render rule.
431
+ const bootstrapIdentity = React.useMemo(() => `${variantType ?? ''}\u0000${[...variantKeys].sort().join('\u0001')}`, [variantType, variantKeys]);
432
+ const [prevBootstrapIdentity, setPrevBootstrapIdentity] = React.useState(bootstrapIdentity);
433
+ if (prevBootstrapIdentity !== bootstrapIdentity) {
434
+ setPrevBootstrapIdentity(bootstrapIdentity);
435
+ // Reset to `false` so the effect re-runs and the resolved value
436
+ // swings from initial → stored on a later tick. Setting it true
437
+ // synchronously here would skip the receiver-flow swap animation
438
+ // for the new bucket — same regression class as bootstrapping
439
+ // synchronously on first render.
440
+ setAllowStoredBootstrap(false);
441
+ setHasCommittedPastInitial(false);
442
+ setInitialCommittedVariantKey(null);
443
+ }
444
+ // A stored-preference bootstrap is only actually pending when no
445
+ // higher-precedence source (the URL hash) is already winning. When
446
+ // the hash takes precedence, the resolved value matches the hash
447
+ // forever and no bootstrap swap will ever fire — gating only on
448
+ // `storedValue !== committedVariantKey` here would leave
449
+ // `pendingBootstrap` latched forever, which `useCode` translates
450
+ // into "never highlight". The hash precedence guard keeps
451
+ // permalinked / hash-selected demos highlighting normally even when
452
+ // the user's saved preference points at a different variant.
453
+ const hashOverridesStorage = !!hashVariant && variantKeys.includes(hashVariant);
454
+ const pendingBootstrap = !hasCommittedPastInitial && !hashOverridesStorage && !!storedValue && variantKeys.includes(storedValue) && storedValue !== committedVariantKey;
455
+
456
+ // User setter: persists to localStorage (and clears any relevant
457
+ // URL hash) before dispatching the coordinator so peer demos
458
+ // observe the new preference on the same tick as the click.
459
+ // Validation differs from `resolveVariantKey`: an explicit `null`
460
+ // means "fall back to the first variant" — never re-resolved to
461
+ // `initialVariant` (which is only consulted for the
462
+ // never-set-storage hydration path).
463
+ const setSelectedVariantAsUser = React.useCallback(value => {
464
+ const resolved = typeof value === 'function' ? value(selectedVariantKey) : value;
465
+ const effectiveValue = resolved ?? variantKeys[0];
466
+ if (!effectiveValue || !variantKeys.includes(effectiveValue)) {
467
+ return;
468
+ }
469
+ if (effectiveValue === selectedVariantKey && effectiveValue === committedVariantKey) {
122
470
  return;
123
471
  }
472
+ // Clear hash first so the receiver flow doesn't observe a
473
+ // stale hash → variant mapping after the storage write echoes
474
+ // back through `usePreference`. Only clear when the current
475
+ // hash is one this demo cares about.
476
+ if (urlHash && mainSlug && isHashRelevantToDemo(urlHash, mainSlug)) {
477
+ setUrlHash(null);
478
+ }
479
+ // Start local coordination from the user action first so this
480
+ // demo is always treated as the originator (matching the
481
+ // pattern in `useTransformManagement`), then persist.
482
+ selectVariantDispatch(effectiveValue);
483
+ setStoredValue(effectiveValue);
484
+ }, [selectedVariantKey, committedVariantKey, variantKeys, urlHash, mainSlug, setUrlHash, selectVariantDispatch, setStoredValue]);
124
485
 
125
- // Only apply hash if it actually changed (not just a re-render with same hash)
126
- const hashChanged = prevHashVariant.current !== hashVariant;
127
- const storedValueChanged = prevStoredValue.current !== storedValue;
128
- prevHashVariant.current = hashVariant;
129
- prevStoredValue.current = storedValue;
130
- if (!hashChanged && !storedValueChanged) {
486
+ // Programmatic setter: doesn't save to localStorage and doesn't
487
+ // clear the hash. Used for hash-driven changes routed through
488
+ // `useFileNavigation`.
489
+ const setSelectedVariantProgrammatic = React.useCallback(value => {
490
+ const resolved = typeof value === 'function' ? value(selectedVariantKey) : value;
491
+ if (!variantKeys.includes(resolved)) {
131
492
  return;
132
493
  }
133
- if (hashVariant && variantKeys.includes(hashVariant) && hashVariant !== selectedVariantKeyRef.current) {
134
- // Hash has a variant - use it
135
- setSelectedVariantKeyState(hashVariant);
136
- // Save hash variant to localStorage based on configuration
137
- if (saveHashVariantToLocalStorage === 'on-load' && hashVariant !== storedValue) {
138
- setStoredValue(hashVariant);
139
- }
140
- } else if (!hashVariant && !urlHash &&
141
- // Only fall back to localStorage when hash is truly empty
142
- storedValue && variantKeys.includes(storedValue) && storedValue !== selectedVariantKeyRef.current) {
143
- // Hash is empty but localStorage has a variant - use it
144
- setSelectedVariantKeyState(storedValue);
145
- }
146
- }, [hashVariant, urlHash, variantKeys,
147
- // Note: selectedVariantKey is intentionally NOT in dependencies
148
- // to avoid effect running when user manually changes variant
149
- storedValue, setStoredValue, saveHashVariantToLocalStorage]);
150
-
151
- // Programmatic setter: doesn't save to localStorage (used for hash-driven changes)
152
- const setSelectedVariantKeyProgrammatic = React.useCallback(value => {
153
- const resolvedValue = typeof value === 'function' ? value(selectedVariantKey) : value;
154
- if (variantKeys.includes(resolvedValue)) {
155
- setSelectedVariantKeyState(resolvedValue);
156
- }
157
- }, [selectedVariantKey, variantKeys]);
158
-
159
- // User setter: saves to localStorage (used for user-initiated changes like dropdown)
160
- const setSelectedVariantKeyAsUser = React.useCallback(value => {
161
- const resolvedValue = typeof value === 'function' ? value(selectedVariantKey) : value;
162
- // If value is null, select the first variant (default)
163
- const effectiveValue = resolvedValue ?? variantKeys[0];
164
- if (effectiveValue && variantKeys.includes(effectiveValue)) {
165
- // Mark as user-initiated to prevent hash effect from overriding
166
- isUserInitiatedChange.current = true;
167
- // Clear hash if it exists and is relevant to this demo
168
- if (urlHash && mainSlug && isHashRelevantToDemo(urlHash, mainSlug)) {
169
- setUrlHash(null);
170
- // Update prevHashVariant to reflect that hash is now null
171
- prevHashVariant.current = null;
172
- }
173
- setSelectedVariantKeyState(effectiveValue);
174
- setStoredValue(effectiveValue);
175
- }
176
- }, [setStoredValue, selectedVariantKey, variantKeys, urlHash, mainSlug, setUrlHash]);
494
+ if (resolved === selectedVariantKey && resolved === committedVariantKey) {
495
+ return;
496
+ }
497
+ selectVariantDispatch(resolved);
498
+ }, [selectedVariantKey, committedVariantKey, variantKeys, selectVariantDispatch]);
177
499
  const selectedVariant = React.useMemo(() => {
178
500
  const variant = effectiveCode[selectedVariantKey];
179
501
  if (variant && typeof variant === 'object' && 'source' in variant) {
@@ -182,12 +504,119 @@ export function useVariantSelection({
182
504
  return null;
183
505
  }, [effectiveCode, selectedVariantKey]);
184
506
 
185
- // Safety check: if selectedVariant doesn't exist, fall back to first variant
507
+ // Variant resolved from the *committed* key lags `selectedVariant`
508
+ // by `variantSwapDelay` ms when a delay is configured. Used by the
509
+ // renderer so the outgoing tree stays put for the pre-swap window.
510
+ const committedVariant = React.useMemo(() => {
511
+ if (committedVariantKey === selectedVariantKey) {
512
+ return selectedVariant;
513
+ }
514
+ const variant = effectiveCode[committedVariantKey];
515
+ if (variant && typeof variant === 'object' && 'source' in variant) {
516
+ return variant;
517
+ }
518
+ return null;
519
+ }, [effectiveCode, committedVariantKey, selectedVariantKey, selectedVariant]);
520
+
521
+ // Post-swap `data-transforming="expanded"`/`"collapsing"` window. Mirrors the
522
+ // equivalent in `useTransformManagement`: fires after the engine
523
+ // commits a swap so the incoming tree has a chance to enter-animate
524
+ // any bridge `.collapse` placeholder appended by `<Pre>`. Only
525
+ // armed when `variantSwapDelay` is configured.
526
+ //
527
+ // `collapseSourceVariantKey` captures the variant we just left at
528
+ // the moment of commit so the post-swap window has a stable bridge
529
+ // target even if `selectedVariantKey` keeps changing.
530
+ const [postSwapWindowActive, setPostSwapWindowActive] = React.useState(false);
531
+ const [collapseSourceVariantKey, setCollapseSourceVariantKey] = React.useState(null);
532
+ const [prevAppliedVariant, setPrevAppliedVariant] = React.useState(committedVariantKey);
533
+ if (prevAppliedVariant !== committedVariantKey) {
534
+ if (hasDelay && prevAppliedVariant !== '') {
535
+ setPostSwapWindowActive(true);
536
+ setCollapseSourceVariantKey(prevAppliedVariant);
537
+ }
538
+ setPrevAppliedVariant(committedVariantKey);
539
+ }
540
+ // Tear down a stale window synchronously at render time: no animation
541
+ // window should exist without a delay, so a window left open after
542
+ // `hasDelay` flips to false is cleared a tick earlier than an effect would,
543
+ // with no animation to disrupt (there is no delay).
544
+ if (postSwapWindowActive && !hasDelay) {
545
+ setPostSwapWindowActive(false);
546
+ setCollapseSourceVariantKey(null);
547
+ }
548
+ React.useEffect(() => {
549
+ if (!postSwapWindowActive) {
550
+ return undefined;
551
+ }
552
+ const timerId = setTimeout(() => {
553
+ setPostSwapWindowActive(false);
554
+ setCollapseSourceVariantKey(null);
555
+ }, effectiveSwapWindowMs);
556
+ return () => clearTimeout(timerId);
557
+ }, [postSwapWindowActive, effectiveSwapWindowMs, committedVariantKey]);
558
+
559
+ // If both phases are technically eligible, the pending pre-swap
560
+ // takes priority — the visible tree IS the just-applied one and it
561
+ // needs to expand out for the next swap. When `variantSwapDelay`
562
+ // isn't configured, no animation window is opening (the coordinator
563
+ // wait is the one-frame `MIN_VARIANT_WAIT_MS`, too short to
564
+ // animate) so the phase stays `null`.
565
+ //
566
+ // Each phase enters a "paused" value first (`'collapsed'` for the
567
+ // pre-swap window, `'expanded'` for the post-swap window). The
568
+ // rendered `<Pre>` calls `notifyVariantTransitionReady` once it has
569
+ // painted the new tree at that paused value, flipping
570
+ // `variantTransitionReady` to `true` which advances the phase to
571
+ // the matching active value (`'expanding'` / `'collapsing'`). The
572
+ // readiness flag is keyed on `(committedVariantKey,
573
+ // selectedVariantKey)` so each new paused window starts with a
574
+ // fresh wait.
575
+ const variantTransitionWindowKey = `${committedVariantKey}|${selectedVariantKey}|${postSwapWindowActive ? '1' : '0'}`;
576
+ const {
577
+ ready: variantTransitionReady,
578
+ notify: notifyVariantTransitionReady
579
+ } = useTransitionPhase(variantTransitionWindowKey);
580
+ const variantSwappingPhase = (() => {
581
+ if (!hasDelay) {
582
+ return null;
583
+ }
584
+ if (committedVariantKey !== selectedVariantKey) {
585
+ return variantTransitionReady ? 'expanding' : 'collapsed';
586
+ }
587
+ if (postSwapWindowActive) {
588
+ return variantTransitionReady ? 'collapsing' : 'expanded';
589
+ }
590
+ return null;
591
+ })();
592
+ const swapPartnerVariantKey = (() => {
593
+ if (variantSwappingPhase === 'collapsed' || variantSwappingPhase === 'expanding') {
594
+ return selectedVariantKey || null;
595
+ }
596
+ if (variantSwappingPhase === 'expanded' || variantSwappingPhase === 'collapsing') {
597
+ return collapseSourceVariantKey;
598
+ }
599
+ return null;
600
+ })();
601
+ const pendingVariantKey = coordinationExtras.isWaitingForPeers ? coordinationExtras.pendingValue : undefined;
602
+
603
+ // Safety check: if the selected variant truly disappears from the
604
+ // code map (e.g. variant keys re-resolved), fall back to the first
605
+ // variant. We deliberately *don't* trip on a key whose entry exists
606
+ // but is still a lazy placeholder (string / partial object without
607
+ // `source`): during incremental loading the variant key is valid,
608
+ // the source just hasn't arrived yet, and bouncing through the
609
+ // coordinator here would round-trip the selection
610
+ // stored → first-variant → stored (the receiver flow re-resolves
611
+ // back to the underlying value the moment the placeholder swaps
612
+ // for a real `VariantCode`). `variantKeys` itself only includes
613
+ // fully-loaded variants, so it's not a reliable signal here.
614
+ const keyExistsInCode = selectedVariantKey ? Object.prototype.hasOwnProperty.call(effectiveCode, selectedVariantKey) : false;
186
615
  React.useEffect(() => {
187
- if (!selectedVariant && variantKeys.length > 0) {
188
- setSelectedVariantKeyProgrammatic(variantKeys[0]);
616
+ if (!keyExistsInCode && variantKeys.length > 0) {
617
+ setSelectedVariantProgrammatic(variantKeys[0]);
189
618
  }
190
- }, [selectedVariant, variantKeys, setSelectedVariantKeyProgrammatic]);
619
+ }, [keyExistsInCode, variantKeys, setSelectedVariantProgrammatic]);
191
620
 
192
621
  // Function to save variant to localStorage (used for on-interaction mode)
193
622
  const saveVariantToLocalStorage = React.useCallback(variant => {
@@ -199,8 +628,15 @@ export function useVariantSelection({
199
628
  variantKeys,
200
629
  selectedVariantKey,
201
630
  selectedVariant,
202
- selectVariant: setSelectedVariantKeyAsUser,
203
- selectVariantProgrammatic: setSelectedVariantKeyProgrammatic,
631
+ committedVariantKey,
632
+ committedVariant,
633
+ variantSwappingPhase,
634
+ swapPartnerVariantKey,
635
+ pendingVariantKey,
636
+ pendingBootstrap,
637
+ notifyVariantTransitionReady,
638
+ selectVariant: setSelectedVariantAsUser,
639
+ selectVariantProgrammatic: setSelectedVariantProgrammatic,
204
640
  saveVariantToLocalStorage,
205
641
  hashVariant
206
642
  };