astro-tractstack 2.0.0-rc.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 (427) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +56 -0
  3. package/astro.d.ts +64 -0
  4. package/bin/create-tractstack.js +483 -0
  5. package/dist/config.js +80 -0
  6. package/dist/index.js +2129 -0
  7. package/package.json +89 -0
  8. package/templates/artpacks/kCz/captainBreakfast_1080px.webp +0 -0
  9. package/templates/artpacks/kCz/captainBreakfast_1920px.webp +0 -0
  10. package/templates/artpacks/kCz/captainBreakfast_600px.webp +0 -0
  11. package/templates/artpacks/kCz/cleanDrips_1080px.webp +0 -0
  12. package/templates/artpacks/kCz/cleanDrips_1920px.webp +0 -0
  13. package/templates/artpacks/kCz/cleanDrips_600px.webp +0 -0
  14. package/templates/artpacks/kCz/crispwaves_1080px.webp +0 -0
  15. package/templates/artpacks/kCz/crispwaves_1920px.webp +0 -0
  16. package/templates/artpacks/kCz/crispwaves_600px.webp +0 -0
  17. package/templates/artpacks/kCz/dragonSkin_1080px.webp +0 -0
  18. package/templates/artpacks/kCz/dragonSkin_1920px.webp +0 -0
  19. package/templates/artpacks/kCz/dragonSkin_600px.webp +0 -0
  20. package/templates/artpacks/kCz/dragon_1080px.webp +0 -0
  21. package/templates/artpacks/kCz/dragon_1920px.webp +0 -0
  22. package/templates/artpacks/kCz/dragon_600px.webp +0 -0
  23. package/templates/artpacks/kCz/nightcity_1080px.webp +0 -0
  24. package/templates/artpacks/kCz/nightcity_1920px.webp +0 -0
  25. package/templates/artpacks/kCz/nightcity_600px.webp +0 -0
  26. package/templates/artpacks/kCz/pattern1_1080px.webp +0 -0
  27. package/templates/artpacks/kCz/pattern1_1920px.webp +0 -0
  28. package/templates/artpacks/kCz/pattern1_600px.webp +0 -0
  29. package/templates/artpacks/kCz/pattern2_1080px.webp +0 -0
  30. package/templates/artpacks/kCz/pattern2_1920px.webp +0 -0
  31. package/templates/artpacks/kCz/pattern2_600px.webp +0 -0
  32. package/templates/artpacks/kCz/skindrips_1080px.webp +0 -0
  33. package/templates/artpacks/kCz/skindrips_1920px.webp +0 -0
  34. package/templates/artpacks/kCz/skindrips_600px.webp +0 -0
  35. package/templates/artpacks/kCz/slimetime_1080px.webp +0 -0
  36. package/templates/artpacks/kCz/slimetime_1920px.webp +0 -0
  37. package/templates/artpacks/kCz/slimetime_600px.webp +0 -0
  38. package/templates/artpacks/kCz/snake_1080px.webp +0 -0
  39. package/templates/artpacks/kCz/snake_1920px.webp +0 -0
  40. package/templates/artpacks/kCz/snake_600px.webp +0 -0
  41. package/templates/artpacks/kCz/toxicshock_1080px.webp +0 -0
  42. package/templates/artpacks/kCz/toxicshock_1920px.webp +0 -0
  43. package/templates/artpacks/kCz/toxicshock_600px.webp +0 -0
  44. package/templates/artpacks/kCz/tractstack_1080px.webp +0 -0
  45. package/templates/artpacks/kCz/tractstack_1920px.webp +0 -0
  46. package/templates/artpacks/kCz/tractstack_600px.webp +0 -0
  47. package/templates/artpacks/kCz/tripdrips_1080px.webp +0 -0
  48. package/templates/artpacks/kCz/tripdrips_1920px.webp +0 -0
  49. package/templates/artpacks/kCz/tripdrips_600px.webp +0 -0
  50. package/templates/artpacks/kCz/wavedrips_1080px.webp +0 -0
  51. package/templates/artpacks/kCz/wavedrips_1920px.webp +0 -0
  52. package/templates/artpacks/kCz/wavedrips_600px.webp +0 -0
  53. package/templates/artpacks/t8k/beach_1080px.webp +0 -0
  54. package/templates/artpacks/t8k/beach_1920px.webp +0 -0
  55. package/templates/artpacks/t8k/beach_600px.webp +0 -0
  56. package/templates/artpacks/t8k/blast_1080px.webp +0 -0
  57. package/templates/artpacks/t8k/blast_1920px.webp +0 -0
  58. package/templates/artpacks/t8k/blast_600px.webp +0 -0
  59. package/templates/artpacks/t8k/bokeh_1080px.webp +0 -0
  60. package/templates/artpacks/t8k/bokeh_1920px.webp +0 -0
  61. package/templates/artpacks/t8k/bokeh_600px.webp +0 -0
  62. package/templates/artpacks/t8k/cartoon_1080px.webp +0 -0
  63. package/templates/artpacks/t8k/cartoon_1920px.webp +0 -0
  64. package/templates/artpacks/t8k/cartoon_600px.webp +0 -0
  65. package/templates/artpacks/t8k/darkeggshell_1080px.webp +0 -0
  66. package/templates/artpacks/t8k/darkeggshell_1920px.webp +0 -0
  67. package/templates/artpacks/t8k/darkeggshell_600px.webp +0 -0
  68. package/templates/artpacks/t8k/explosion_1080px.webp +0 -0
  69. package/templates/artpacks/t8k/explosion_1920px.webp +0 -0
  70. package/templates/artpacks/t8k/explosion_600px.webp +0 -0
  71. package/templates/artpacks/t8k/floral_1080px.webp +0 -0
  72. package/templates/artpacks/t8k/floral_1920px.webp +0 -0
  73. package/templates/artpacks/t8k/floral_600px.webp +0 -0
  74. package/templates/artpacks/t8k/flower_1080px.webp +0 -0
  75. package/templates/artpacks/t8k/flower_1920px.webp +0 -0
  76. package/templates/artpacks/t8k/flower_600px.webp +0 -0
  77. package/templates/artpacks/t8k/foliage_1080px.webp +0 -0
  78. package/templates/artpacks/t8k/foliage_1920px.webp +0 -0
  79. package/templates/artpacks/t8k/foliage_600px.webp +0 -0
  80. package/templates/artpacks/t8k/mist_1080px.webp +0 -0
  81. package/templates/artpacks/t8k/mist_1920px.webp +0 -0
  82. package/templates/artpacks/t8k/mist_600px.webp +0 -0
  83. package/templates/artpacks/t8k/portal_1080px.webp +0 -0
  84. package/templates/artpacks/t8k/portal_1920px.webp +0 -0
  85. package/templates/artpacks/t8k/portal_600px.webp +0 -0
  86. package/templates/artpacks/t8k/storytime_1080px.webp +0 -0
  87. package/templates/artpacks/t8k/storytime_1920px.webp +0 -0
  88. package/templates/artpacks/t8k/storytime_600px.webp +0 -0
  89. package/templates/artpacks/t8k/tacky_1080px.webp +0 -0
  90. package/templates/artpacks/t8k/tacky_1920px.webp +0 -0
  91. package/templates/artpacks/t8k/tacky_600px.webp +0 -0
  92. package/templates/artpacks/t8k/wallpaper_1080px.webp +0 -0
  93. package/templates/artpacks/t8k/wallpaper_1920px.webp +0 -0
  94. package/templates/artpacks/t8k/wallpaper_600px.webp +0 -0
  95. package/templates/brand/favicon.ico +0 -0
  96. package/templates/brand/logo.svg +19 -0
  97. package/templates/brand/static.jpg +0 -0
  98. package/templates/brand/wordmark.svg +4 -0
  99. package/templates/css/custom.css +51 -0
  100. package/templates/css/frontend.css +3519 -0
  101. package/templates/css/storykeep.css +92872 -0
  102. package/templates/custom/minimal/CodeHook.astro +53 -0
  103. package/templates/custom/minimal/CustomRoutes.astro +46 -0
  104. package/templates/custom/with-examples/CodeHook.astro +49 -0
  105. package/templates/custom/with-examples/CustomHero.astro +13 -0
  106. package/templates/custom/with-examples/CustomRoutes.astro +39 -0
  107. package/templates/custom/with-examples/pages/Collections.astro +110 -0
  108. package/templates/env.example +8 -0
  109. package/templates/fonts/Inter-Black.woff2 +0 -0
  110. package/templates/fonts/Inter-Bold.woff2 +0 -0
  111. package/templates/fonts/Inter-Regular.woff2 +0 -0
  112. package/templates/icons/h2.svg +1 -0
  113. package/templates/icons/h3.svg +1 -0
  114. package/templates/icons/h4.svg +1 -0
  115. package/templates/icons/h5.svg +1 -0
  116. package/templates/icons/image.svg +7 -0
  117. package/templates/icons/text.svg +6 -0
  118. package/templates/socials/codepen.svg +1 -0
  119. package/templates/socials/discord.svg +1 -0
  120. package/templates/socials/facebook.svg +1 -0
  121. package/templates/socials/github.svg +1 -0
  122. package/templates/socials/instagram.svg +1 -0
  123. package/templates/socials/linkedin.svg +1 -0
  124. package/templates/socials/mail.svg +1 -0
  125. package/templates/socials/rumble.svg +1 -0
  126. package/templates/socials/tiktok.svg +1 -0
  127. package/templates/socials/twitch.svg +1 -0
  128. package/templates/socials/twitter.svg +1 -0
  129. package/templates/socials/x.svg +1 -0
  130. package/templates/socials/youtube.svg +1 -0
  131. package/templates/src/client/analytics-events.ts +213 -0
  132. package/templates/src/client/belief-events.ts +205 -0
  133. package/templates/src/client/sse.ts +667 -0
  134. package/templates/src/components/Footer.astro +246 -0
  135. package/templates/src/components/Fragment.astro +70 -0
  136. package/templates/src/components/Header.astro +458 -0
  137. package/templates/src/components/Menu.tsx +196 -0
  138. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +692 -0
  139. package/templates/src/components/codehooks/BunnyVideoWrapper.astro +78 -0
  140. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +1020 -0
  141. package/templates/src/components/codehooks/EpinetTableView.tsx +594 -0
  142. package/templates/src/components/codehooks/EpinetWrapper.tsx +424 -0
  143. package/templates/src/components/codehooks/FeaturedContent.astro +273 -0
  144. package/templates/src/components/codehooks/FeaturedContentSetup.tsx +738 -0
  145. package/templates/src/components/codehooks/ListContent.astro +460 -0
  146. package/templates/src/components/codehooks/ListContentSetup.tsx +649 -0
  147. package/templates/src/components/codehooks/SankeyDiagram.tsx +359 -0
  148. package/templates/src/components/compositor/Compositor.tsx +144 -0
  149. package/templates/src/components/compositor/Node.tsx +415 -0
  150. package/templates/src/components/compositor/NodeWithGuid.tsx +25 -0
  151. package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +87 -0
  152. package/templates/src/components/compositor/elements/Belief.tsx +148 -0
  153. package/templates/src/components/compositor/elements/BgImage.tsx +118 -0
  154. package/templates/src/components/compositor/elements/BgVisualBreak.tsx +102 -0
  155. package/templates/src/components/compositor/elements/BunnyVideo.tsx +63 -0
  156. package/templates/src/components/compositor/elements/IdentifyAs.tsx +66 -0
  157. package/templates/src/components/compositor/elements/PlayButton.tsx +19 -0
  158. package/templates/src/components/compositor/elements/SignUp.tsx +179 -0
  159. package/templates/src/components/compositor/elements/Svg.tsx +33 -0
  160. package/templates/src/components/compositor/elements/ToggleBelief.tsx +36 -0
  161. package/templates/src/components/compositor/elements/YouTubeWrapper.tsx +33 -0
  162. package/templates/src/components/compositor/nodes/BgPaneWrapper.tsx +35 -0
  163. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +189 -0
  164. package/templates/src/components/compositor/nodes/Markdown.tsx +179 -0
  165. package/templates/src/components/compositor/nodes/Pane.tsx +277 -0
  166. package/templates/src/components/compositor/nodes/Pane_eraser.tsx +69 -0
  167. package/templates/src/components/compositor/nodes/Pane_layout.tsx +77 -0
  168. package/templates/src/components/compositor/nodes/RenderChildren.tsx +19 -0
  169. package/templates/src/components/compositor/nodes/StoryFragment.tsx +35 -0
  170. package/templates/src/components/compositor/nodes/TagElement.tsx +14 -0
  171. package/templates/src/components/compositor/nodes/Widget.tsx +115 -0
  172. package/templates/src/components/compositor/nodes/tagElements/NodeA.tsx +4 -0
  173. package/templates/src/components/compositor/nodes/tagElements/NodeA_eraser.tsx +26 -0
  174. package/templates/src/components/compositor/nodes/tagElements/NodeAnchorComponent.tsx +248 -0
  175. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +684 -0
  176. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_eraser.tsx +62 -0
  177. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_insert.tsx +120 -0
  178. package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_settings.tsx +62 -0
  179. package/templates/src/components/compositor/nodes/tagElements/NodeButton.tsx +5 -0
  180. package/templates/src/components/compositor/nodes/tagElements/NodeButton_eraser.tsx +26 -0
  181. package/templates/src/components/compositor/nodes/tagElements/NodeImg.tsx +28 -0
  182. package/templates/src/components/compositor/nodes/tagElements/NodeText.tsx +18 -0
  183. package/templates/src/components/compositor/nodes/tagElements/TabIndicator.tsx +51 -0
  184. package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +128 -0
  185. package/templates/src/components/compositor/preview/ListContentPreview.tsx +213 -0
  186. package/templates/src/components/compositor/preview/OgImagePreview.tsx +223 -0
  187. package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +199 -0
  188. package/templates/src/components/compositor/preview/PanesPreviewGenerator.tsx +123 -0
  189. package/templates/src/components/compositor/preview/VisualBreakPreview.tsx +154 -0
  190. package/templates/src/components/edit/Header.tsx +181 -0
  191. package/templates/src/components/edit/PanelSwitch.tsx +446 -0
  192. package/templates/src/components/edit/SettingsPanel.tsx +70 -0
  193. package/templates/src/components/edit/ToolBar.tsx +101 -0
  194. package/templates/src/components/edit/ToolMode.tsx +121 -0
  195. package/templates/src/components/edit/context/ContextPaneConfig.tsx +91 -0
  196. package/templates/src/components/edit/context/ContextPaneConfig_slug.tsx +174 -0
  197. package/templates/src/components/edit/context/ContextPaneConfig_title.tsx +186 -0
  198. package/templates/src/components/edit/pane/AddPanePanel.tsx +136 -0
  199. package/templates/src/components/edit/pane/AddPanePanel_break.tsx +470 -0
  200. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +264 -0
  201. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +623 -0
  202. package/templates/src/components/edit/pane/AddPanePanel_newAICopy.tsx +107 -0
  203. package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +217 -0
  204. package/templates/src/components/edit/pane/AddPanePanel_newCopyMode.tsx +109 -0
  205. package/templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx +39 -0
  206. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +445 -0
  207. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +245 -0
  208. package/templates/src/components/edit/pane/PageGen.tsx +485 -0
  209. package/templates/src/components/edit/pane/PageGenSelector.tsx +238 -0
  210. package/templates/src/components/edit/pane/PageGenSpecial.tsx +362 -0
  211. package/templates/src/components/edit/pane/PageGen_preview.tsx +495 -0
  212. package/templates/src/components/edit/pane/PanePanel_impression.tsx +258 -0
  213. package/templates/src/components/edit/pane/PanePanel_path.tsx +268 -0
  214. package/templates/src/components/edit/pane/PanePanel_slug.tsx +219 -0
  215. package/templates/src/components/edit/pane/PanePanel_title.tsx +142 -0
  216. package/templates/src/components/edit/panels/StyleBreakPanel.tsx +182 -0
  217. package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +439 -0
  218. package/templates/src/components/edit/panels/StyleElementPanel.tsx +177 -0
  219. package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +349 -0
  220. package/templates/src/components/edit/panels/StyleElementPanel_remove.tsx +159 -0
  221. package/templates/src/components/edit/panels/StyleElementPanel_update.tsx +320 -0
  222. package/templates/src/components/edit/panels/StyleImagePanel.tsx +460 -0
  223. package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +296 -0
  224. package/templates/src/components/edit/panels/StyleImagePanel_remove.tsx +153 -0
  225. package/templates/src/components/edit/panels/StyleImagePanel_update.tsx +312 -0
  226. package/templates/src/components/edit/panels/StyleLiElementPanel.tsx +273 -0
  227. package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +301 -0
  228. package/templates/src/components/edit/panels/StyleLiElementPanel_remove.tsx +132 -0
  229. package/templates/src/components/edit/panels/StyleLiElementPanel_update.tsx +313 -0
  230. package/templates/src/components/edit/panels/StyleLinkPanel.tsx +346 -0
  231. package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +265 -0
  232. package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +240 -0
  233. package/templates/src/components/edit/panels/StyleLinkPanel_remove.tsx +94 -0
  234. package/templates/src/components/edit/panels/StyleLinkPanel_update.tsx +110 -0
  235. package/templates/src/components/edit/panels/StyleParentPanel.tsx +263 -0
  236. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +275 -0
  237. package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +112 -0
  238. package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +87 -0
  239. package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +141 -0
  240. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +428 -0
  241. package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +292 -0
  242. package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +190 -0
  243. package/templates/src/components/edit/panels/StyleWidgetPanel_remove.tsx +152 -0
  244. package/templates/src/components/edit/panels/StyleWidgetPanel_update.tsx +308 -0
  245. package/templates/src/components/edit/state/SaveModal.tsx +811 -0
  246. package/templates/src/components/edit/state/StylesMemory.tsx +310 -0
  247. package/templates/src/components/edit/storyfragment/StoryFragmentConfigPanel.tsx +289 -0
  248. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +320 -0
  249. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +888 -0
  250. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +269 -0
  251. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_title.tsx +190 -0
  252. package/templates/src/components/edit/widgets/BeliefWidget.tsx +183 -0
  253. package/templates/src/components/edit/widgets/BunnyWidget.tsx +134 -0
  254. package/templates/src/components/edit/widgets/IdentifyAsWidget.tsx +193 -0
  255. package/templates/src/components/edit/widgets/SignupWidget.tsx +177 -0
  256. package/templates/src/components/edit/widgets/ToggleWidget.tsx +152 -0
  257. package/templates/src/components/edit/widgets/YouTubeWidget.tsx +65 -0
  258. package/templates/src/components/fields/ActionBuilderTimeSelector.tsx +353 -0
  259. package/templates/src/components/fields/ArtpackImage.tsx +480 -0
  260. package/templates/src/components/fields/BackgroundImage.tsx +530 -0
  261. package/templates/src/components/fields/BackgroundImageWrapper.tsx +192 -0
  262. package/templates/src/components/fields/BooleanParam.tsx +67 -0
  263. package/templates/src/components/fields/BunnyMomentSelector.tsx +56 -0
  264. package/templates/src/components/fields/ColorPickerCombo.tsx +284 -0
  265. package/templates/src/components/fields/ImageUpload.tsx +405 -0
  266. package/templates/src/components/fields/MultiParam.tsx +75 -0
  267. package/templates/src/components/fields/PaneBreakCollectionSelector.tsx +97 -0
  268. package/templates/src/components/fields/PaneBreakShapeSelector.tsx +134 -0
  269. package/templates/src/components/fields/SelectedTailwindClass.tsx +44 -0
  270. package/templates/src/components/fields/SingleParam.tsx +73 -0
  271. package/templates/src/components/fields/ViewportComboBox.tsx +252 -0
  272. package/templates/src/components/form/ActionBuilderField.tsx +282 -0
  273. package/templates/src/components/form/ActionBuilderSlugSelector.tsx +182 -0
  274. package/templates/src/components/form/BooleanToggle.tsx +94 -0
  275. package/templates/src/components/form/ColorPicker.tsx +153 -0
  276. package/templates/src/components/form/DateTimeInput.tsx +638 -0
  277. package/templates/src/components/form/EnumSelect.tsx +88 -0
  278. package/templates/src/components/form/FileUpload.tsx +465 -0
  279. package/templates/src/components/form/MagicPathBuilder.tsx +546 -0
  280. package/templates/src/components/form/NumberInput.tsx +101 -0
  281. package/templates/src/components/form/ParagraphArrayInput.tsx +207 -0
  282. package/templates/src/components/form/StringArrayInput.tsx +163 -0
  283. package/templates/src/components/form/StringInput.tsx +88 -0
  284. package/templates/src/components/form/UnsavedChangesBar.tsx +295 -0
  285. package/templates/src/components/form/advanced/APIConfigSection.tsx +69 -0
  286. package/templates/src/components/form/advanced/AuthConfigSection.tsx +97 -0
  287. package/templates/src/components/form/brand/BrandAssetsSection.tsx +93 -0
  288. package/templates/src/components/form/brand/BrandColorsSection.tsx +201 -0
  289. package/templates/src/components/form/brand/SEOSection.tsx +101 -0
  290. package/templates/src/components/form/brand/SiteConfigSection.tsx +61 -0
  291. package/templates/src/components/form/brand/SocialLinksSection.tsx +393 -0
  292. package/templates/src/components/profile/ProfileConsent.tsx +65 -0
  293. package/templates/src/components/profile/ProfileCreate.tsx +462 -0
  294. package/templates/src/components/profile/ProfileEdit.tsx +409 -0
  295. package/templates/src/components/profile/ProfileSwitch.tsx +255 -0
  296. package/templates/src/components/profile/ProfileUnlock.tsx +221 -0
  297. package/templates/src/components/storykeep/Dashboard.tsx +160 -0
  298. package/templates/src/components/storykeep/Dashboard_Activity.tsx +56 -0
  299. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +165 -0
  300. package/templates/src/components/storykeep/Dashboard_Analytics.tsx +451 -0
  301. package/templates/src/components/storykeep/Dashboard_Branding.tsx +95 -0
  302. package/templates/src/components/storykeep/Dashboard_Content.tsx +191 -0
  303. package/templates/src/components/storykeep/controls/UsageCell.tsx +71 -0
  304. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +378 -0
  305. package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +329 -0
  306. package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +385 -0
  307. package/templates/src/components/storykeep/controls/content/ContentSummary.tsx +149 -0
  308. package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +397 -0
  309. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +260 -0
  310. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +439 -0
  311. package/templates/src/components/storykeep/controls/content/MenuForm.tsx +239 -0
  312. package/templates/src/components/storykeep/controls/content/MenuTable.tsx +332 -0
  313. package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +724 -0
  314. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +355 -0
  315. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +222 -0
  316. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +482 -0
  317. package/templates/src/components/storykeep/state/BrandingWrapper.tsx +42 -0
  318. package/templates/src/components/storykeep/state/FetchAnalytics.tsx +350 -0
  319. package/templates/src/components/storykeep/widgets/ResponsiveLine.tsx +319 -0
  320. package/templates/src/components/storykeep/widgets/Wizard.tsx +278 -0
  321. package/templates/src/components/tenant/RegistrationForm.tsx +447 -0
  322. package/templates/src/components/widgets/BunnyVideoHero.astro +775 -0
  323. package/templates/src/components/widgets/Impression.tsx +102 -0
  324. package/templates/src/components/widgets/ImpressionWrapper.tsx +214 -0
  325. package/templates/src/constants/beliefs.ts +61 -0
  326. package/templates/src/constants/brandThemes.ts +133 -0
  327. package/templates/src/constants/prompts.json +55 -0
  328. package/templates/src/constants/shapes.ts +556 -0
  329. package/templates/src/constants/stopWords.ts +116 -0
  330. package/templates/src/constants/tailwindColors.json +344 -0
  331. package/templates/src/constants.ts +274 -0
  332. package/templates/src/hooks/useFormState.ts +203 -0
  333. package/templates/src/layouts/Layout.astro +290 -0
  334. package/templates/src/lib/session.ts +126 -0
  335. package/templates/src/lib/storyData.ts +56 -0
  336. package/templates/src/middleware.ts +52 -0
  337. package/templates/src/pages/404.astro +54 -0
  338. package/templates/src/pages/[...slug]/edit.astro +216 -0
  339. package/templates/src/pages/[...slug].astro +148 -0
  340. package/templates/src/pages/api/auth/decode.ts +101 -0
  341. package/templates/src/pages/api/auth/login.ts +122 -0
  342. package/templates/src/pages/api/auth/logout.ts +37 -0
  343. package/templates/src/pages/api/auth/profile.ts +76 -0
  344. package/templates/src/pages/api/orphan-analysis.ts +106 -0
  345. package/templates/src/pages/api/tailwind.ts +116 -0
  346. package/templates/src/pages/collections/[param1].astro +65 -0
  347. package/templates/src/pages/context/[...contextSlug]/edit.astro +207 -0
  348. package/templates/src/pages/context/[...contextSlug].astro +161 -0
  349. package/templates/src/pages/llms.txt.ts +122 -0
  350. package/templates/src/pages/maint.astro +183 -0
  351. package/templates/src/pages/media/[...slug].astro +67 -0
  352. package/templates/src/pages/robots.txt.ts +36 -0
  353. package/templates/src/pages/sandbox/activate.astro +258 -0
  354. package/templates/src/pages/sandbox/register.astro +44 -0
  355. package/templates/src/pages/sandbox/success.astro +179 -0
  356. package/templates/src/pages/sitemap.xml.ts +119 -0
  357. package/templates/src/pages/storykeep/advanced.astro +69 -0
  358. package/templates/src/pages/storykeep/branding.astro +57 -0
  359. package/templates/src/pages/storykeep/content.astro +71 -0
  360. package/templates/src/pages/storykeep/init.astro +36 -0
  361. package/templates/src/pages/storykeep/login.astro +266 -0
  362. package/templates/src/pages/storykeep/logout.astro +84 -0
  363. package/templates/src/pages/storykeep/profile.astro +98 -0
  364. package/templates/src/pages/storykeep.astro +81 -0
  365. package/templates/src/stores/analytics.ts +171 -0
  366. package/templates/src/stores/backend.ts +16 -0
  367. package/templates/src/stores/navigation.ts +149 -0
  368. package/templates/src/stores/nodes.ts +2390 -0
  369. package/templates/src/stores/nodesHistory.ts +85 -0
  370. package/templates/src/stores/notificationSystem.ts +41 -0
  371. package/templates/src/stores/orphanAnalysis.ts +409 -0
  372. package/templates/src/stores/storykeep.ts +247 -0
  373. package/templates/src/types/astro.ts +86 -0
  374. package/templates/src/types/compositorTypes.ts +456 -0
  375. package/templates/src/types/formTypes.ts +281 -0
  376. package/templates/src/types/multiTenant.ts +77 -0
  377. package/templates/src/types/nodeProps.ts +66 -0
  378. package/templates/src/types/tractstack.ts +445 -0
  379. package/templates/src/utils/aai/getTitleSlug.ts +72 -0
  380. package/templates/src/utils/actions/actionButton.ts +101 -0
  381. package/templates/src/utils/actions/lispLexer.ts +57 -0
  382. package/templates/src/utils/actions/preParse_Action.ts +85 -0
  383. package/templates/src/utils/actions/preParse_Bunny.ts +50 -0
  384. package/templates/src/utils/actions/preParse_Clicked.ts +87 -0
  385. package/templates/src/utils/actions/preParse_Impression.ts +71 -0
  386. package/templates/src/utils/api/advancedConfig.ts +66 -0
  387. package/templates/src/utils/api/advancedHelpers.ts +134 -0
  388. package/templates/src/utils/api/beliefConfig.ts +87 -0
  389. package/templates/src/utils/api/beliefHelpers.ts +196 -0
  390. package/templates/src/utils/api/brandConfig.ts +126 -0
  391. package/templates/src/utils/api/brandHelpers.ts +155 -0
  392. package/templates/src/utils/api/fileHelpers.ts +306 -0
  393. package/templates/src/utils/api/menuConfig.ts +57 -0
  394. package/templates/src/utils/api/menuHelpers.ts +156 -0
  395. package/templates/src/utils/api/resourceConfig.ts +158 -0
  396. package/templates/src/utils/api/resourceHelpers.ts +72 -0
  397. package/templates/src/utils/api/tenantConfig.ts +97 -0
  398. package/templates/src/utils/api/tenantHelpers.ts +172 -0
  399. package/templates/src/utils/api.ts +183 -0
  400. package/templates/src/utils/auth.ts +150 -0
  401. package/templates/src/utils/backend.ts +243 -0
  402. package/templates/src/utils/compositor/TemplateMarkdowns.ts +118 -0
  403. package/templates/src/utils/compositor/TemplateNodes.ts +138 -0
  404. package/templates/src/utils/compositor/TemplatePanes.ts +100 -0
  405. package/templates/src/utils/compositor/allowInsert.ts +100 -0
  406. package/templates/src/utils/compositor/domHelpers.ts +37 -0
  407. package/templates/src/utils/compositor/handleClickEvent.ts +131 -0
  408. package/templates/src/utils/compositor/nodesHelper.ts +491 -0
  409. package/templates/src/utils/compositor/nodesMarkdownGenerator.ts +292 -0
  410. package/templates/src/utils/compositor/processMarkdown.ts +431 -0
  411. package/templates/src/utils/compositor/reduceNodesClassNames.ts +192 -0
  412. package/templates/src/utils/compositor/tailwindClasses.ts +1795 -0
  413. package/templates/src/utils/compositor/tailwindColors.ts +227 -0
  414. package/templates/src/utils/compositor/templateMarkdownStyles.ts +1265 -0
  415. package/templates/src/utils/compositor/typeGuards.ts +193 -0
  416. package/templates/src/utils/etl/extractor.ts +119 -0
  417. package/templates/src/utils/etl/index.ts +88 -0
  418. package/templates/src/utils/etl/loader.ts +36 -0
  419. package/templates/src/utils/etl/transformer.ts +286 -0
  420. package/templates/src/utils/helpers.ts +435 -0
  421. package/templates/src/utils/layout.ts +209 -0
  422. package/templates/src/utils/profileStorage.ts +306 -0
  423. package/templates/src/utils/useInterval.ts +27 -0
  424. package/templates/tailwind.config.cjs +169 -0
  425. package/utils/create-resolver.ts +10 -0
  426. package/utils/inject-files.ts +2140 -0
  427. package/utils/validate-config.ts +43 -0
@@ -0,0 +1,2390 @@
1
+ import { atom, map } from 'nanostores';
2
+ import {
3
+ hasButtonPayload,
4
+ hasTagName,
5
+ isDefined,
6
+ isValidTag,
7
+ toTag,
8
+ } from '@/utils/compositor/typeGuards';
9
+ import { startLoadingAnimation } from '@/utils/helpers';
10
+ import { settingsPanelStore } from '@/stores/storykeep';
11
+ import {
12
+ PaneAddMode,
13
+ StoryFragmentMode,
14
+ ContextPaneMode,
15
+ } from '@/types/compositorTypes';
16
+ import type {
17
+ PanelState,
18
+ BaseNode,
19
+ FlatNode,
20
+ ImpressionNode,
21
+ MarkdownPaneFragmentNode,
22
+ MenuNode,
23
+ NodeType,
24
+ PaneFragmentNode,
25
+ PaneNode,
26
+ StoryFragmentNode,
27
+ Tag,
28
+ TemplateMarkdown,
29
+ TemplateNode,
30
+ TemplatePane,
31
+ ToolModeVal,
32
+ ToolAddMode,
33
+ TractStackNode,
34
+ ViewportKey,
35
+ OgImageParams,
36
+ VisualBreakNode,
37
+ BeliefDatum,
38
+ LoadData,
39
+ ArtpackImageNode,
40
+ } from '@/types/compositorTypes';
41
+ import type { NodeProps, WidgetProps } from '@/types/nodeProps';
42
+ import type { CSSProperties } from 'react';
43
+ import { processClassesForViewports } from '@/utils/compositor/reduceNodesClassNames';
44
+ import { ulid } from 'ulid';
45
+ import { NotificationSystem } from '@/stores/notificationSystem';
46
+ import { cloneDeep, isDeepEqual } from '@/utils/helpers';
47
+ import { extractClassesFromNodes } from '@/utils/compositor/nodesHelper';
48
+ import { handleClickEventDefault } from '@/utils/compositor/handleClickEvent';
49
+ import allowInsert from '@/utils/compositor/allowInsert';
50
+ import { reservedSlugs } from '@/constants';
51
+ import { NodesHistory, PatchOp } from '@/stores/nodesHistory';
52
+ import { moveNodeAtLocationInContext } from '@/utils/compositor/nodesHelper';
53
+ import { MarkdownGenerator } from '@/utils/compositor/nodesMarkdownGenerator';
54
+ import type { CompositorProps } from '@/components/compositor/Compositor';
55
+
56
+ const blockedClickNodes = new Set<string>(['em', 'strong']);
57
+ export const ROOT_NODE_NAME = 'root';
58
+ export const UNDO_REDO_HISTORY_CAPACITY = 500;
59
+
60
+ function strippedStyles(obj: Record<string, string[]>) {
61
+ return Object.fromEntries(
62
+ Object.entries(obj).map(([key, value]) => [key, value[0]])
63
+ );
64
+ }
65
+ function addHoverPrefix(str: string): string {
66
+ return str
67
+ .split(' ')
68
+ .map((word) => `hover:${word}`)
69
+ .join(' ');
70
+ }
71
+
72
+ export class NodesContext {
73
+ constructor() {}
74
+
75
+ notifications = new NotificationSystem<BaseNode>();
76
+ allNodes = atom<Map<string, BaseNode>>(new Map<string, BaseNode>());
77
+ impressionNodes = atom<Set<ImpressionNode>>(new Set<ImpressionNode>());
78
+ parentNodes = atom<Map<string, string[]>>(new Map<string, string[]>());
79
+ hasTitle = atom<boolean>(false);
80
+ hasPanes = atom<boolean>(false);
81
+ isTemplate = atom<boolean>(false);
82
+ rootNodeId = atom<string>('');
83
+ clickedNodeId = atom<string>('');
84
+ ghostTextActiveId = atom<string>('');
85
+ clickedParentLayer = atom<number | null>(null);
86
+ activePaneMode = atom<PanelState>({
87
+ paneId: '',
88
+ mode: '',
89
+ panel: '',
90
+ });
91
+ editingNodeId = atom<string | null>(null);
92
+ history = new NodesHistory(this, UNDO_REDO_HISTORY_CAPACITY);
93
+
94
+ toolModeValStore = map<{ value: ToolModeVal }>({
95
+ value: 'text',
96
+ });
97
+ toolAddModeStore = map<{ value: ToolAddMode }>({
98
+ value: 'p',
99
+ });
100
+ showGuids = atom<boolean>(false);
101
+
102
+ /**
103
+ * Sets an edit lock on a specific node to prevent re-renders during editing
104
+ * @param nodeId - The node ID to lock, or null to clear the lock
105
+ */
106
+ setEditLock(nodeId: string | null): void {
107
+ this.editingNodeId.set(nodeId);
108
+ }
109
+
110
+ /**
111
+ * Checks if a specific node is currently edit-locked
112
+ * @param nodeId - The node ID to check
113
+ * @returns true if the node is locked for editing
114
+ */
115
+ isEditLocked(nodeId: string): boolean {
116
+ return this.editingNodeId.get() === nodeId;
117
+ }
118
+
119
+ /**
120
+ * Clears the current edit lock
121
+ */
122
+ clearEditLock(): void {
123
+ this.editingNodeId.set(null);
124
+ }
125
+
126
+ /**
127
+ * Cleanup method to handle orphaned edit locks
128
+ */
129
+ cleanupEditState(): void {
130
+ const editingId = this.editingNodeId.get();
131
+ if (editingId) {
132
+ // Check if the node still exists
133
+ const node = this.allNodes.get().get(editingId);
134
+ if (!node) {
135
+ this.clearEditLock();
136
+ }
137
+ }
138
+ }
139
+
140
+ notifyNode(nodeId: string, payload?: BaseNode) {
141
+ // Skip notification if this node is edit-locked
142
+ if (this.isEditLocked(nodeId)) {
143
+ // Still notify parent nodes as they may need updates
144
+ const node = this.allNodes.get().get(nodeId);
145
+ if (node && node.parentId) {
146
+ const parentNodeToNotify = this.nodeToNotify(nodeId, node.nodeType);
147
+ if (parentNodeToNotify && parentNodeToNotify !== nodeId) {
148
+ this.notifyNode(parentNodeToNotify, payload);
149
+ }
150
+ }
151
+ return;
152
+ }
153
+ // Original notifyNode implementation
154
+ let notifyNodeId = nodeId;
155
+ if (notifyNodeId === this.rootNodeId.get()) {
156
+ notifyNodeId = ROOT_NODE_NAME;
157
+ }
158
+ if (nodeId === `root`) startLoadingAnimation();
159
+ this.updateHasPanesStatus();
160
+ this.notifications.notify(notifyNodeId, payload);
161
+ }
162
+
163
+ getPanelMode(nodeId: string, panel: string): string {
164
+ const activeMode = this.activePaneMode.get();
165
+ if (activeMode.panel === panel && activeMode.paneId === nodeId) {
166
+ return activeMode.mode;
167
+ }
168
+ return '';
169
+ }
170
+
171
+ setPanelMode(nodeId: string, panel: string, mode: string) {
172
+ this.closeAllPanelsExcept(nodeId, panel);
173
+ this.activePaneMode.set({
174
+ paneId: nodeId,
175
+ panel: panel,
176
+ mode: mode,
177
+ });
178
+ }
179
+
180
+ getPaneAddMode(nodeId: string): PaneAddMode {
181
+ const mode = this.getPanelMode(nodeId, 'add');
182
+ return mode ? (mode as PaneAddMode) : PaneAddMode.DEFAULT;
183
+ }
184
+
185
+ setPaneAddMode(nodeId: string, mode: PaneAddMode) {
186
+ this.setPanelMode(nodeId, 'add', mode);
187
+ }
188
+
189
+ getContextPaneMode(nodeId: string): ContextPaneMode {
190
+ const mode = this.getPanelMode(nodeId, 'context');
191
+ return mode ? (mode as ContextPaneMode) : ContextPaneMode.DEFAULT;
192
+ }
193
+
194
+ setContextPaneMode(nodeId: string, mode: ContextPaneMode) {
195
+ this.setPanelMode(nodeId, 'context', mode);
196
+ }
197
+
198
+ getStoryFragmentMode(nodeId: string): StoryFragmentMode {
199
+ const mode = this.getPanelMode(nodeId, 'storyfragment');
200
+ return mode ? (mode as StoryFragmentMode) : StoryFragmentMode.DEFAULT;
201
+ }
202
+
203
+ setStoryFragmentMode(nodeId: string, mode: StoryFragmentMode) {
204
+ this.setPanelMode(nodeId, 'storyfragment', mode);
205
+ }
206
+
207
+ closeAllPanels() {
208
+ this.activePaneMode.set({
209
+ paneId: '',
210
+ panel: '',
211
+ mode: '',
212
+ });
213
+ }
214
+
215
+ closeAllPanelsExcept(nodeId: string, panel: string) {
216
+ if (panel === 'styles-memory') {
217
+ return;
218
+ }
219
+ const currentPanel = this.activePaneMode.get();
220
+ if (currentPanel.paneId !== nodeId || currentPanel.panel !== panel) {
221
+ settingsPanelStore.set(null);
222
+ }
223
+ }
224
+
225
+ ogImageParamsStore = map<Record<string, OgImageParams>>({});
226
+
227
+ getOgImageParams(nodeId: string): OgImageParams {
228
+ const params = this.ogImageParamsStore.get()[nodeId];
229
+ return (
230
+ params || {
231
+ textColor: '#fcfcfc',
232
+ bgColor: '#10120d',
233
+ fontSize: undefined,
234
+ }
235
+ );
236
+ }
237
+
238
+ setOgImageParams(nodeId: string, params: Partial<OgImageParams>): void {
239
+ const currentParams = this.getOgImageParams(nodeId);
240
+ this.ogImageParamsStore.setKey(nodeId, {
241
+ ...currentParams,
242
+ ...params,
243
+ });
244
+ }
245
+
246
+ //setActiveGhost(nodeId: string): void {
247
+ // const currentActiveId = this.ghostTextActiveId.get();
248
+ // // If this is already the active ghost, do nothing
249
+ // if (currentActiveId === nodeId) return;
250
+ // // If another ghost is active, clear it first
251
+ // if (currentActiveId && currentActiveId !== nodeId) {
252
+ // // Set to empty string to close any existing ghost
253
+ // this.ghostTextActiveId.set("");
254
+ // // After a short delay to allow the previous ghost to close,
255
+ // // set the new active ghost
256
+ // setTimeout(() => {
257
+ // this.ghostTextActiveId.set(nodeId);
258
+ // }, 100);
259
+ // } else {
260
+ // this.ghostTextActiveId.set(nodeId);
261
+ // }
262
+ //}
263
+
264
+ updateHasPanesStatus() {
265
+ const allNodes = this.allNodes.get();
266
+ const storyFragments = Array.from(allNodes.values()).filter(
267
+ (node) => node.nodeType === 'StoryFragment'
268
+ );
269
+ const hasPanes = storyFragments.some(
270
+ (node) => 'paneIds' in node && (node.paneIds as string[]).length > 0
271
+ );
272
+ this.hasPanes.set(hasPanes);
273
+ }
274
+
275
+ cleanNode(nodeId: string) {
276
+ const node = this.allNodes.get().get(nodeId);
277
+ if (!node) return;
278
+ const newNodes = new Map(this.allNodes.get());
279
+ const cleanedNode = cloneDeep(node);
280
+ if (cleanedNode.isChanged) delete cleanedNode.isChanged;
281
+ newNodes.set(nodeId, cleanedNode);
282
+ this.allNodes.set(newNodes);
283
+ }
284
+
285
+ getDirtyNodes(): BaseNode[] {
286
+ const allNodes = Array.from(this.allNodes.get().values());
287
+ return allNodes.filter(
288
+ (node): node is BaseNode => 'isChanged' in node && node.isChanged === true
289
+ );
290
+ }
291
+
292
+ clearUndoHistory() {
293
+ this.history.clearHistory();
294
+ }
295
+
296
+ getChildNodeIDs(parentNodeId: string): string[] {
297
+ const returnVal = this.parentNodes.get()?.get(parentNodeId) || [];
298
+ return returnVal;
299
+ }
300
+
301
+ setClickedParentLayer(layer: number | null) {
302
+ this.clickedParentLayer.set(layer);
303
+ }
304
+
305
+ handleEraseEvent(nodeId: string) {
306
+ const node = this.allNodes.get().get(nodeId) as FlatNode;
307
+ if (!node) return;
308
+ switch (node.nodeType) {
309
+ case `Pane`: {
310
+ const storyfragmentNodeId = this.getClosestNodeTypeFromId(
311
+ nodeId,
312
+ 'StoryFragment'
313
+ );
314
+ const storyfragmentNode = cloneDeep(
315
+ this.allNodes.get().get(storyfragmentNodeId)
316
+ ) as StoryFragmentNode;
317
+ this.modifyNodes([{ ...storyfragmentNode, isChanged: true }]);
318
+ break;
319
+ }
320
+ case `TagElement`: {
321
+ const paneNodeId = this.getClosestNodeTypeFromId(nodeId, 'Pane');
322
+ const paneNode = cloneDeep(
323
+ this.allNodes.get().get(paneNodeId)
324
+ ) as PaneNode;
325
+ this.modifyNodes([{ ...paneNode, isChanged: true }]);
326
+ break;
327
+ }
328
+ default:
329
+ }
330
+ }
331
+
332
+ handleClickEvent(dblClick: boolean = false) {
333
+ const toolModeVal = this.toolModeValStore.get().value;
334
+ const node = this.allNodes.get().get(this.clickedNodeId.get()) as FlatNode;
335
+ if (!node) return;
336
+
337
+ // click handler based on toolModeVal
338
+ switch (toolModeVal) {
339
+ case `styles`:
340
+ handleClickEventDefault(node, dblClick, this.clickedParentLayer.get());
341
+ break;
342
+ case `text`:
343
+ if (dblClick && ![`Markdown`].includes(node.nodeType)) {
344
+ this.toolModeValStore.set({ value: 'styles' });
345
+ handleClickEventDefault(
346
+ node,
347
+ dblClick,
348
+ this.clickedParentLayer.get()
349
+ );
350
+ } else {
351
+ handleClickEventDefault(
352
+ node,
353
+ dblClick,
354
+ this.clickedParentLayer.get()
355
+ );
356
+ }
357
+ break;
358
+ case `eraser`:
359
+ this.handleEraseEvent(node.id);
360
+ this.deleteNode(node.id);
361
+ break;
362
+ default:
363
+ }
364
+ // reset on parentLayer
365
+ this.setClickedParentLayer(null);
366
+ }
367
+
368
+ private clickTimer: number | null = null;
369
+ private DOUBLE_CLICK_DELAY = 300;
370
+ private isProcessingDoubleClick = false;
371
+ private lastProcessedTime = 0;
372
+
373
+ setClickedNodeId(nodeId: string, dblClick: boolean = false) {
374
+ //settingsPanelStore.set(null);
375
+ const now = Date.now();
376
+ // Prevent processing if we're too close to the last event
377
+ if (now - this.lastProcessedTime < 50 || this.isProcessingDoubleClick)
378
+ return;
379
+ let node = this.allNodes.get().get(nodeId) as FlatNode;
380
+ if (node && 'tagName' in node) {
381
+ while (node.parentId !== null && blockedClickNodes.has(node.tagName)) {
382
+ node = this.allNodes.get().get(node.parentId) as FlatNode;
383
+ }
384
+ if (!node) return;
385
+ }
386
+
387
+ // Handle double click
388
+ if (dblClick) {
389
+ if (this.clickTimer) {
390
+ window.clearTimeout(this.clickTimer);
391
+ this.clickTimer = null;
392
+ }
393
+ this.isProcessingDoubleClick = true;
394
+ this.clickedNodeId.set(node.id);
395
+ this.lastProcessedTime = now;
396
+ window.setTimeout(() => {
397
+ this.isProcessingDoubleClick = false;
398
+ }, 100);
399
+ this.handleClickEvent(true);
400
+ return;
401
+ }
402
+
403
+ // Handle single click with delay for potential double click
404
+ if (this.clickTimer) {
405
+ window.clearTimeout(this.clickTimer);
406
+ }
407
+ this.clickTimer = window.setTimeout(() => {
408
+ if (!this.isProcessingDoubleClick) {
409
+ this.clickTimer = null;
410
+ this.clickedNodeId.set(node.id);
411
+ this.lastProcessedTime = Date.now();
412
+ this.handleClickEvent(false);
413
+ }
414
+ }, this.DOUBLE_CLICK_DELAY);
415
+ }
416
+
417
+ clearAll() {
418
+ this.allNodes.get().clear();
419
+ this.parentNodes.get().clear();
420
+ this.impressionNodes.get().clear();
421
+ this.rootNodeId.set('');
422
+ this.notifications.clear();
423
+ }
424
+
425
+ buildNodesTreeFromRowDataMadeNodes(nodes: LoadData | null) {
426
+ if (nodes !== null) {
427
+ this.clearAll();
428
+ //if (nodes?.fileNodes) this.addNodes(nodes.fileNodes);
429
+ if (nodes?.menuNodes) this.addNodes(nodes.menuNodes);
430
+ //if (nodes?.resourceNodes) this.addNodes(nodes.resourceNodes);
431
+ if (nodes?.tractstackNodes) this.addNodes(nodes.tractstackNodes);
432
+ // IMPORTANT!
433
+ // pane nodes have to be added BEFORE StoryFragment nodes so they can register in this.allNodes
434
+ if (nodes?.paneNodes) this.addNodes(nodes.paneNodes);
435
+ // add childNodes after panes
436
+ if (nodes?.childNodes) this.addNodes(nodes.childNodes);
437
+ // then storyfragment nodes will link pane nodes from above
438
+ // then add storyfragmentNodes
439
+ if (nodes?.storyfragmentNodes) this.addNodes(nodes.storyfragmentNodes);
440
+
441
+ this.updateHasPanesStatus();
442
+ }
443
+ }
444
+
445
+ linkChildToParent(
446
+ nodeId: string,
447
+ parentId: string,
448
+ specificIndex: number = -1
449
+ ) {
450
+ const parentNode = this.parentNodes.get();
451
+ if (parentNode.has(parentId)) {
452
+ if (specificIndex === -1) {
453
+ parentNode.get(parentId)?.push(nodeId);
454
+ } else {
455
+ parentNode.get(parentId)?.splice(Math.max(0, specificIndex), 0, nodeId);
456
+ }
457
+ this.parentNodes.set(new Map<string, string[]>(parentNode));
458
+ } else {
459
+ parentNode.set(parentId, [nodeId]);
460
+ }
461
+ }
462
+
463
+ addNode(data: BaseNode) {
464
+ this.allNodes.get().set(data.id, data);
465
+
466
+ // root node
467
+ if (data.parentId === null && this.rootNodeId.get().length === 0) {
468
+ this.rootNodeId.set(data.id);
469
+ return;
470
+ }
471
+ const parentNode = this.parentNodes.get();
472
+ if (!parentNode) return;
473
+
474
+ if (data.parentId !== null) {
475
+ // if storyfragment then iterate over its paneIDs
476
+ if (data.nodeType === 'StoryFragment') {
477
+ const storyFragment = data as StoryFragmentNode;
478
+ this.linkChildToParent(data.id, data.parentId);
479
+
480
+ storyFragment.paneIds.forEach((paneId: string) => {
481
+ // pane should already exist by now, tell it where it belongs to
482
+ const pane = this.allNodes.get().get(paneId);
483
+ if (pane) {
484
+ pane.parentId = data.id;
485
+ }
486
+ this.linkChildToParent(paneId, data.id);
487
+ });
488
+ // skip panes, they get linked along with story fragment
489
+ } else if (data.nodeType !== 'Pane') {
490
+ this.linkChildToParent(data.id, data.parentId);
491
+
492
+ if (data.nodeType === 'Impression') {
493
+ this.impressionNodes.get().add(data as ImpressionNode);
494
+ }
495
+ }
496
+ }
497
+ this.updateHasPanesStatus();
498
+ }
499
+
500
+ addNodes(nodes: BaseNode[]) {
501
+ for (const node of nodes) {
502
+ this.addNode(node);
503
+ }
504
+ }
505
+
506
+ allowInsert(
507
+ nodeId: string,
508
+ tagNameStr: string
509
+ ): {
510
+ allowInsertBefore: boolean;
511
+ allowInsertAfter: boolean;
512
+ } {
513
+ const node = this.allNodes.get().get(nodeId);
514
+ if (!isDefined(node) || !hasTagName(node)) {
515
+ return { allowInsertBefore: false, allowInsertAfter: false };
516
+ }
517
+ const markdownId = this.getClosestNodeTypeFromId(nodeId, 'Markdown');
518
+ const tagNameIds = this.getChildNodeIDs(markdownId);
519
+ const tagNames = tagNameIds
520
+ .map((id) => {
521
+ const name = this.getNodeTagName(id);
522
+ return toTag(name);
523
+ })
524
+ .filter((name): name is Tag => name !== null);
525
+
526
+ const offset = tagNameIds.indexOf(nodeId);
527
+ const tagName = toTag(tagNameStr);
528
+
529
+ if (!tagName || !isValidTag(node.tagName)) {
530
+ return { allowInsertBefore: false, allowInsertAfter: false };
531
+ }
532
+
533
+ const allowInsertBefore =
534
+ offset > -1
535
+ ? allowInsert(
536
+ node,
537
+ node.tagName as Tag,
538
+ tagName,
539
+ offset ? tagNames[offset - 1] : undefined
540
+ )
541
+ : allowInsert(node, node.tagName as Tag, tagName);
542
+
543
+ const allowInsertAfter =
544
+ tagNames.length > offset
545
+ ? allowInsert(node, node.tagName as Tag, tagName, tagNames[offset + 1])
546
+ : allowInsert(node, node.tagName as Tag, tagName);
547
+
548
+ return { allowInsertBefore, allowInsertAfter };
549
+ }
550
+
551
+ allowInsertLi(
552
+ nodeId: string,
553
+ tagNameStr: string
554
+ ): {
555
+ allowInsertBefore: boolean;
556
+ allowInsertAfter: boolean;
557
+ } {
558
+ const node = this.allNodes.get().get(nodeId);
559
+ if (!isDefined(node) || !hasTagName(node) || !node.parentId) {
560
+ return { allowInsertBefore: false, allowInsertAfter: false };
561
+ }
562
+
563
+ const tagNameIds = this.getChildNodeIDs(node.parentId);
564
+ const tagNames = tagNameIds
565
+ .map((id) => {
566
+ const name = this.getNodeTagName(id);
567
+ return toTag(name);
568
+ })
569
+ .filter((name): name is Tag => name !== null);
570
+
571
+ const offset = tagNameIds.indexOf(nodeId);
572
+ const tagName = toTag(tagNameStr);
573
+
574
+ if (!tagName || !isValidTag(node.tagName)) {
575
+ return { allowInsertBefore: false, allowInsertAfter: false };
576
+ }
577
+
578
+ const allowInsertBefore =
579
+ offset > 0
580
+ ? allowInsert(node, node.tagName as Tag, tagName, tagNames[offset - 1])
581
+ : allowInsert(node, node.tagName as Tag, tagName);
582
+
583
+ const allowInsertAfter =
584
+ tagNames.length < offset
585
+ ? allowInsert(node, node.tagName as Tag, tagName, tagNames[offset + 1])
586
+ : allowInsert(node, node.tagName as Tag, tagName);
587
+
588
+ return { allowInsertBefore, allowInsertAfter };
589
+ }
590
+
591
+ getClosestNodeTypeFromId(startNodeId: string, nodeType: NodeType): string {
592
+ const node = this.allNodes.get().get(startNodeId);
593
+ if (!node || node.nodeType === 'Root') return '';
594
+
595
+ const parentId = node.parentId || '';
596
+ const parentNode = this.allNodes.get().get(parentId);
597
+ if (parentNode && parentNode.nodeType === nodeType) {
598
+ return parentId;
599
+ } else {
600
+ return this.getClosestNodeTypeFromId(parentId, nodeType);
601
+ }
602
+ }
603
+
604
+ getChildNodeByTagNames(startNodeId: string, tagNames: string[]): string {
605
+ const node = this.allNodes.get().get(startNodeId);
606
+ if (!node || node.nodeType === 'Root') return '';
607
+
608
+ let firstChildId = '';
609
+ if ('tagName' in node && tagNames.includes(node.tagName as string)) {
610
+ firstChildId = node.id;
611
+ return firstChildId;
612
+ }
613
+ this.getChildNodeIDs(node.id).forEach((childId) => {
614
+ const foundId = this.getChildNodeByTagNames(childId, tagNames);
615
+ if (foundId.length > 0 && firstChildId.length === 0) {
616
+ firstChildId = foundId;
617
+ }
618
+ });
619
+ return firstChildId;
620
+ }
621
+
622
+ getParentNodeByTagNames(startNodeId: string, tagNames: string[]): string {
623
+ const node = this.allNodes.get().get(startNodeId);
624
+ if (!node || node.nodeType === 'Root') return '';
625
+
626
+ const parentId = node.parentId || '';
627
+ const parentNode = this.allNodes.get().get(parentId);
628
+ if (
629
+ parentNode &&
630
+ 'tagName' in parentNode &&
631
+ tagNames.includes(parentNode.tagName as string)
632
+ ) {
633
+ return parentId;
634
+ } else {
635
+ return this.getParentNodeByTagNames(parentId, tagNames);
636
+ }
637
+ }
638
+
639
+ //getStyleByViewport(
640
+ // defaultClasses:
641
+ // | {
642
+ // mobile?: Record<string, string> | undefined;
643
+ // tablet?: Record<string, string> | undefined;
644
+ // desktop?: Record<string, string> | undefined;
645
+ // }
646
+ // | undefined,
647
+ // viewport: ViewportKey
648
+ //): Record<string, string> {
649
+ // switch (viewport) {
650
+ // case "desktop":
651
+ // return defaultClasses?.desktop || {};
652
+ // case "tablet":
653
+ // return defaultClasses?.tablet || {};
654
+ // default:
655
+ // case "mobile":
656
+ // return defaultClasses?.mobile || {};
657
+ // }
658
+ //}
659
+
660
+ getNodeSlug(nodeId: string): string {
661
+ const node = this.allNodes.get().get(nodeId);
662
+ if (!node || !(`slug` in node) || typeof node.slug !== `string`) return '';
663
+ return node.slug;
664
+ }
665
+
666
+ getNodeTagName(nodeId: string): string {
667
+ const node = this.allNodes.get().get(nodeId);
668
+ if (!node || !(`tagName` in node) || typeof node.tagName !== `string`)
669
+ return '';
670
+ return node.tagName;
671
+ }
672
+
673
+ getIsContextPane(nodeId: string): boolean {
674
+ const node = this.allNodes.get().get(nodeId);
675
+ if (!node || !(`isContextPane` in node)) return false;
676
+ return !!node.isContextPane;
677
+ }
678
+
679
+ getMenuNodeById(id: string): MenuNode | null {
680
+ const node = this.allNodes.get().get(id);
681
+ return node?.nodeType === 'Menu' ? (node as MenuNode) : null;
682
+ }
683
+
684
+ getTractStackNodeById(id: string): TractStackNode | null {
685
+ const node = this.allNodes.get().get(id);
686
+ return node?.nodeType === 'TractStack' ? (node as TractStackNode) : null;
687
+ }
688
+
689
+ getStoryFragmentNodeBySlug(slug: string): StoryFragmentNode | null {
690
+ const nodes = Array.from(this.allNodes.get().values());
691
+ return (
692
+ nodes.find(
693
+ (node): node is StoryFragmentNode =>
694
+ node.nodeType === 'StoryFragment' &&
695
+ 'slug' in node &&
696
+ node.slug === slug
697
+ ) || null
698
+ );
699
+ }
700
+
701
+ getContextPaneNodeBySlug(slug: string): PaneNode | null {
702
+ const nodes = Array.from(this.allNodes.get().values());
703
+ return (
704
+ nodes.find(
705
+ (node): node is PaneNode =>
706
+ node.nodeType === 'Pane' &&
707
+ 'slug' in node &&
708
+ node.slug === slug &&
709
+ 'isContextPane' in node &&
710
+ node.isContextPane === true
711
+ ) || null
712
+ );
713
+ }
714
+
715
+ getImpressionNodesForPanes(paneIds: string[]): ImpressionNode[] {
716
+ const nodes = Array.from(this.impressionNodes.get().values());
717
+ return nodes.filter(
718
+ (node): node is ImpressionNode =>
719
+ node.nodeType === 'Impression' &&
720
+ typeof node.parentId === `string` &&
721
+ paneIds.includes(node.parentId)
722
+ );
723
+ }
724
+
725
+ getPaneSlug(nodeId: string): string | null {
726
+ const node = this.allNodes.get().get(nodeId);
727
+ if (!node || node.nodeType !== 'Pane') {
728
+ return null;
729
+ }
730
+ if (!('slug' in node) || typeof node.slug !== 'string') {
731
+ return null;
732
+ }
733
+ return node.slug;
734
+ }
735
+
736
+ getNodeCodeHookPayload(
737
+ nodeId: string
738
+ ): { target: string; params?: Record<string, string> } | null {
739
+ const node = this.allNodes.get().get(nodeId);
740
+ const target =
741
+ node && 'codeHookTarget' in node
742
+ ? (node.codeHookTarget as string)
743
+ : undefined;
744
+ const payload =
745
+ node && 'codeHookPayload' in node
746
+ ? (node.codeHookPayload as Record<string, string>)
747
+ : undefined;
748
+
749
+ if (target) {
750
+ return {
751
+ target: target,
752
+ ...(payload && { params: payload }),
753
+ };
754
+ }
755
+ return null;
756
+ }
757
+
758
+ getPaneIsDecorative(nodeId: string): boolean {
759
+ const paneNode = this.allNodes.get().get(nodeId) as PaneNode;
760
+ if (paneNode.nodeType !== 'Pane') {
761
+ return false;
762
+ }
763
+ if (paneNode.isDecorative) return true;
764
+ return false;
765
+ }
766
+
767
+ getPaneBeliefs(
768
+ nodeId: string
769
+ ): { heldBeliefs: BeliefDatum; withheldBeliefs: BeliefDatum } | null {
770
+ const paneNode = this.allNodes.get().get(nodeId) as PaneNode;
771
+ if (paneNode.nodeType !== 'Pane') {
772
+ return null;
773
+ }
774
+
775
+ const beliefs: { heldBeliefs: BeliefDatum; withheldBeliefs: BeliefDatum } =
776
+ {
777
+ heldBeliefs: {},
778
+ withheldBeliefs: {},
779
+ };
780
+ let anyBeliefs = false;
781
+ if ('heldBeliefs' in paneNode) {
782
+ beliefs.heldBeliefs = paneNode.heldBeliefs as BeliefDatum;
783
+ anyBeliefs = true;
784
+ }
785
+ if ('withheldBeliefs' in paneNode) {
786
+ beliefs.withheldBeliefs = paneNode.withheldBeliefs as BeliefDatum;
787
+ anyBeliefs = true;
788
+ }
789
+
790
+ return anyBeliefs ? beliefs : null;
791
+ }
792
+
793
+ getNodeClasses(
794
+ nodeId: string,
795
+ viewport: ViewportKey,
796
+ depth: number = 0
797
+ ): string {
798
+ const isPreview = this.rootNodeId.get() === `tmp`;
799
+ const node = this.allNodes.get().get(nodeId);
800
+ if (!node) return '';
801
+
802
+ switch (node.nodeType) {
803
+ case 'Markdown':
804
+ {
805
+ const markdownFragment = node as MarkdownPaneFragmentNode;
806
+ if (markdownFragment.parentClasses) {
807
+ const [all, mobile, tablet, desktop] = processClassesForViewports(
808
+ markdownFragment.parentClasses[depth],
809
+ {}, // No override classes for Markdown parent case
810
+ 1
811
+ );
812
+
813
+ if (isPreview) return desktop[0];
814
+ switch (viewport) {
815
+ case 'desktop':
816
+ return desktop[0];
817
+ case 'tablet':
818
+ return tablet[0];
819
+ case 'mobile':
820
+ return mobile[0];
821
+ default:
822
+ return all[0];
823
+ }
824
+ }
825
+ // Fallback to existing parentCss if needed
826
+ if ('parentCss' in markdownFragment) {
827
+ return (<string[]>markdownFragment.parentCss)[depth];
828
+ }
829
+ }
830
+ break;
831
+
832
+ case 'TagElement':
833
+ {
834
+ const getButtonClasses = (node: FlatNode) => {
835
+ return {
836
+ mobile: strippedStyles(node.buttonPayload?.buttonClasses || {}),
837
+ tablet: {},
838
+ desktop: {},
839
+ };
840
+ };
841
+
842
+ const getHoverClasses = (node: FlatNode) => {
843
+ return {
844
+ mobile: strippedStyles(
845
+ node.buttonPayload?.buttonHoverClasses || {}
846
+ ),
847
+ tablet: {},
848
+ desktop: {},
849
+ };
850
+ };
851
+
852
+ if (hasButtonPayload(node)) {
853
+ const [classesPayload] = processClassesForViewports(
854
+ getButtonClasses(node),
855
+ {},
856
+ 1
857
+ );
858
+ const [classesHoverPayload] = processClassesForViewports(
859
+ getHoverClasses(node),
860
+ {},
861
+ 1
862
+ );
863
+ return `${classesPayload?.length ? classesPayload[0] : ``} ${
864
+ classesHoverPayload?.length
865
+ ? addHoverPrefix(classesHoverPayload[0])
866
+ : ``
867
+ }`;
868
+ }
869
+ const closestPaneId = this.getClosestNodeTypeFromId(
870
+ nodeId,
871
+ 'Markdown'
872
+ );
873
+ const paneNode = this.allNodes
874
+ .get()
875
+ .get(closestPaneId) as MarkdownPaneFragmentNode;
876
+ if (paneNode && 'tagName' in node) {
877
+ const tagNameStr = node.tagName as string;
878
+ const styles = paneNode.defaultClasses![tagNameStr];
879
+ if (styles && styles.mobile) {
880
+ const [all, mobile, tablet, desktop] = processClassesForViewports(
881
+ styles,
882
+ (node as FlatNode)?.overrideClasses || {},
883
+ 1
884
+ );
885
+ if (isPreview) return desktop[0];
886
+ switch (viewport) {
887
+ case 'desktop':
888
+ return desktop[0];
889
+ case 'tablet':
890
+ return tablet[0];
891
+ case 'mobile':
892
+ return mobile[0];
893
+ default:
894
+ return all[0];
895
+ }
896
+ }
897
+ }
898
+ }
899
+ break;
900
+
901
+ case 'StoryFragment': {
902
+ const storyFragment = node as StoryFragmentNode;
903
+ return typeof storyFragment?.tailwindBgColour === `string`
904
+ ? `bg-${storyFragment?.tailwindBgColour}`
905
+ : ``;
906
+ }
907
+ }
908
+ return '';
909
+ }
910
+
911
+ nodeToNotify(nodeId: string, nodeType: string) {
912
+ switch (nodeType) {
913
+ case `StoryFragment`:
914
+ return `root`;
915
+ case `Pane`:
916
+ if (this.getIsContextPane(nodeId)) return `root`;
917
+ return this.getClosestNodeTypeFromId(nodeId, 'StoryFragment');
918
+ case `TagElement`:
919
+ case `BgPane`:
920
+ case `Markdown`:
921
+ case `Impression`:
922
+ return this.getClosestNodeTypeFromId(nodeId, 'Pane');
923
+ case `Menu`:
924
+ // do nothing
925
+ break;
926
+ default:
927
+ console.warn(`nodeToNotify missed on`, nodeType);
928
+ }
929
+ }
930
+
931
+ modifyNodes(
932
+ newData: BaseNode[],
933
+ options?: {
934
+ notify?: boolean;
935
+ recordHistory?: boolean;
936
+ }
937
+ ) {
938
+ const undoList: ((ctx: NodesContext) => void)[] = [];
939
+ const redoList: ((ctx: NodesContext) => void)[] = [];
940
+ const shouldNotify = options?.notify ?? true;
941
+ const shouldRecordHistory = options?.recordHistory ?? true;
942
+
943
+ for (let i = 0; i < newData.length; i++) {
944
+ const node = newData[i];
945
+ const currentNodeData = this.allNodes.get().get(node.id) as BaseNode;
946
+ if (!currentNodeData) {
947
+ continue;
948
+ }
949
+
950
+ if (isDeepEqual(currentNodeData, node)) {
951
+ continue;
952
+ }
953
+
954
+ const newNodes = new Map(this.allNodes.get());
955
+ newNodes.set(node.id, node);
956
+ this.allNodes.set(newNodes);
957
+
958
+ const deepEqualWithExclusions = isDeepEqual(currentNodeData, node, [
959
+ 'isChanged',
960
+ ]);
961
+
962
+ if (deepEqualWithExclusions) {
963
+ if (shouldNotify) this.notifyNode(node.id);
964
+ continue;
965
+ }
966
+
967
+ switch (node.nodeType) {
968
+ case `TagElement`:
969
+ case `BgPane`:
970
+ case `Markdown`: {
971
+ const paneNodeId = this.getClosestNodeTypeFromId(node.id, 'Pane');
972
+ const paneNode = cloneDeep(
973
+ this.allNodes.get().get(paneNodeId)
974
+ ) as PaneNode;
975
+ this.modifyNodes([{ ...paneNode, isChanged: true }], {
976
+ notify: false,
977
+ });
978
+ this.notifyNode(ROOT_NODE_NAME);
979
+ break;
980
+ }
981
+ case `Menu`:
982
+ case `Pane`:
983
+ case `StoryFragment`:
984
+ break;
985
+
986
+ default:
987
+ console.warn(`must dirty check missed on `, node.nodeType);
988
+ }
989
+
990
+ undoList.push((ctx: NodesContext) => {
991
+ const newNodes = new Map(ctx.allNodes.get());
992
+ newNodes.set(node.id, currentNodeData);
993
+ ctx.allNodes.set(newNodes);
994
+ if (shouldNotify) {
995
+ const parentNode = this.nodeToNotify(node.id, node.nodeType);
996
+ if (parentNode) this.notifyNode(parentNode);
997
+ }
998
+ });
999
+ redoList.push((ctx: NodesContext) => {
1000
+ const newNodes = new Map(ctx.allNodes.get());
1001
+ newNodes.set(node.id, node);
1002
+ ctx.allNodes.set(newNodes);
1003
+ if (shouldNotify) {
1004
+ const parentNode = this.nodeToNotify(node.id, node.nodeType);
1005
+ if (parentNode) this.notifyNode(parentNode);
1006
+ }
1007
+ });
1008
+
1009
+ if (shouldNotify) {
1010
+ if ([`Menu`, `StoryFragment`].includes(node.nodeType))
1011
+ this.notifyNode(ROOT_NODE_NAME);
1012
+ this.notifyNode(node.id);
1013
+ }
1014
+ }
1015
+
1016
+ if (undoList.length > 0 && shouldRecordHistory) {
1017
+ this.history.addPatch({
1018
+ op: PatchOp.REPLACE,
1019
+ undo: (ctx) => {
1020
+ undoList.forEach((fn) => fn(ctx));
1021
+ },
1022
+ redo: (ctx) => {
1023
+ redoList.forEach((fn) => fn(ctx));
1024
+ },
1025
+ });
1026
+ }
1027
+ }
1028
+
1029
+ getNodeStringStyles(nodeId: string): string {
1030
+ const node = this.allNodes.get().get(nodeId);
1031
+ return this.getStringBgColorStyle(node);
1032
+ }
1033
+
1034
+ getNodeCSSPropertiesStyles(nodeId: string): CSSProperties {
1035
+ const node = this.allNodes.get().get(nodeId);
1036
+ return this.getPaneBgColorStyle(node);
1037
+ }
1038
+
1039
+ getPaneBgColorStyle(node: BaseNode | undefined): CSSProperties {
1040
+ if (!node) return {};
1041
+
1042
+ switch (node?.nodeType) {
1043
+ case 'Pane': {
1044
+ const pane = node as PaneFragmentNode;
1045
+ if ('bgColour' in pane) {
1046
+ return { backgroundColor: <string>pane.bgColour };
1047
+ }
1048
+ }
1049
+ }
1050
+ return {};
1051
+ }
1052
+
1053
+ getStringBgColorStyle(node: BaseNode | undefined): string {
1054
+ if (!node) return '';
1055
+ switch (node?.nodeType) {
1056
+ case 'Pane': {
1057
+ const pane = node as PaneFragmentNode;
1058
+ if ('bgColour' in pane) {
1059
+ return `background-color: ${<string>pane.bgColour}`;
1060
+ }
1061
+ }
1062
+ }
1063
+ return '';
1064
+ }
1065
+
1066
+ //addPaneToStoryFragment(
1067
+ // nodeId: string,
1068
+ // pane: PaneNode,
1069
+ // location: 'before' | 'after'
1070
+ ///) {
1071
+ // const node = this.allNodes.get().get(nodeId) as BaseNode;
1072
+ // if (
1073
+ // !node ||
1074
+ // (node.nodeType !== 'StoryFragment' && node.nodeType !== 'Pane')
1075
+ // ) {
1076
+ // return;
1077
+ // }
1078
+
1079
+ // pane.id = ulid();
1080
+ // this.addNode(pane);
1081
+
1082
+ // if (node.nodeType === 'Pane') {
1083
+ // const storyFragmentId = this.getClosestNodeTypeFromId(
1084
+ // nodeId,
1085
+ // 'StoryFragment'
1086
+ // );
1087
+ // const storyFragment = this.allNodes
1088
+ // .get()
1089
+ // .get(storyFragmentId) as StoryFragmentNode;
1090
+ // if (storyFragment) {
1091
+ // pane.parentId = storyFragmentId;
1092
+ // const originalPaneIndex = storyFragment.paneIds.indexOf(pane.parentId);
1093
+ // let insertIdx = -1;
1094
+ // if (location === 'before')
1095
+ // insertIdx = Math.max(0, originalPaneIndex - 1);
1096
+ // else
1097
+ // insertIdx = Math.min(
1098
+ // storyFragment.paneIds.length - 1,
1099
+ // originalPaneIndex + 1
1100
+ // );
1101
+ // storyFragment.paneIds.splice(insertIdx, 0, pane.id);
1102
+ // }
1103
+ // } else if (node.nodeType !== 'StoryFragment') {
1104
+ // const storyFragment = node as StoryFragmentNode;
1105
+ // if (storyFragment) {
1106
+ // pane.parentId = node.id;
1107
+ // if (location === 'after') {
1108
+ // storyFragment.paneIds.push(pane.id);
1109
+ // } else {
1110
+ // storyFragment.paneIds.unshift(pane.id);
1111
+ // }
1112
+ // }
1113
+ // }
1114
+ //}
1115
+
1116
+ addContextTemplatePane(ownerId: string, pane: TemplatePane) {
1117
+ const ownerNode = this.allNodes.get().get(ownerId);
1118
+ if (ownerNode?.nodeType === 'Pane') {
1119
+ const pane = ownerNode as PaneNode;
1120
+ if (!pane.isContextPane) {
1121
+ return;
1122
+ }
1123
+ }
1124
+ const duplicatedPane = cloneDeep(pane) as TemplatePane;
1125
+ duplicatedPane.id = ownerId;
1126
+ if (
1127
+ ownerNode &&
1128
+ 'title' in ownerNode &&
1129
+ typeof ownerNode.title === `string`
1130
+ )
1131
+ duplicatedPane.title = ownerNode.title;
1132
+ if (ownerNode && 'slug' in ownerNode && typeof ownerNode.slug === `string`)
1133
+ duplicatedPane.slug = ownerNode.slug;
1134
+ duplicatedPane.isChanged = true;
1135
+
1136
+ // Track all nodes that need to be added
1137
+ let allNodes: BaseNode[] = [];
1138
+
1139
+ // must generate nodes from markdown
1140
+ if (duplicatedPane.markdown) {
1141
+ duplicatedPane.markdown = cloneDeep(pane.markdown) as TemplateMarkdown;
1142
+ duplicatedPane.markdown.id = pane?.markdown?.id || ulid();
1143
+ duplicatedPane.markdown.markdownId = pane?.markdown?.markdownId || ulid();
1144
+ duplicatedPane.markdown.parentId = ownerId;
1145
+
1146
+ let markdownNodes: TemplateNode[] = [];
1147
+ if (duplicatedPane.markdown.markdownBody) {
1148
+ const markdownGen = new MarkdownGenerator(this);
1149
+ markdownNodes = markdownGen.markdownToFlatNodes(
1150
+ duplicatedPane.markdown.markdownBody,
1151
+ duplicatedPane.markdown.id
1152
+ ) as TemplateNode[];
1153
+ allNodes = [...allNodes, duplicatedPane.markdown, ...markdownNodes];
1154
+ }
1155
+
1156
+ // Markdown already as nodes
1157
+ else if (
1158
+ typeof duplicatedPane.markdown !== `undefined` &&
1159
+ typeof duplicatedPane.markdown.id === `string`
1160
+ ) {
1161
+ duplicatedPane?.markdown.nodes?.forEach((node) => {
1162
+ const childrenNodes = this.setupTemplateNodeRecursively(
1163
+ node,
1164
+ duplicatedPane?.markdown?.id || ''
1165
+ );
1166
+ markdownNodes.push(...childrenNodes);
1167
+ });
1168
+ allNodes = [...allNodes, duplicatedPane.markdown, ...markdownNodes];
1169
+ }
1170
+ }
1171
+
1172
+ this.addNode(duplicatedPane as PaneNode);
1173
+ this.addNodes(allNodes);
1174
+ this.notifyNode(ownerId);
1175
+
1176
+ return ownerId;
1177
+ }
1178
+
1179
+ addTemplatePane(
1180
+ ownerId: string,
1181
+ pane: TemplatePane,
1182
+ insertPaneId?: string,
1183
+ location?: 'before' | 'after'
1184
+ ) {
1185
+ const ownerNode = this.allNodes.get().get(ownerId);
1186
+ if (
1187
+ ownerNode?.nodeType !== 'StoryFragment' &&
1188
+ ownerNode?.nodeType !== 'Root' &&
1189
+ ownerNode?.nodeType !== 'File' &&
1190
+ ownerNode?.nodeType !== 'TractStack'
1191
+ ) {
1192
+ return;
1193
+ }
1194
+ const duplicatedPane = cloneDeep(pane) as TemplatePane;
1195
+ const duplicatedPaneId = pane?.id || ulid();
1196
+ duplicatedPane.id = duplicatedPaneId;
1197
+ duplicatedPane.parentId = ownerNode.id;
1198
+ duplicatedPane.isChanged = true;
1199
+
1200
+ if (this.rootNodeId.get() !== 'tmp') {
1201
+ if (
1202
+ ownerNode.nodeType === 'StoryFragment' &&
1203
+ 'slug' in ownerNode &&
1204
+ 'title' in ownerNode &&
1205
+ typeof ownerNode.title === `string` &&
1206
+ duplicatedPane.slug === '' &&
1207
+ duplicatedPane.title === ''
1208
+ ) {
1209
+ duplicatedPane.slug = `${ownerNode.slug}-${duplicatedPaneId.slice(-4)}`;
1210
+ duplicatedPane.title = `${ownerNode.title.slice(0, 20)}-${duplicatedPaneId.slice(-4)}`;
1211
+ }
1212
+ }
1213
+
1214
+ let allNodes: BaseNode[] = [];
1215
+
1216
+ if (duplicatedPane.markdown) {
1217
+ duplicatedPane.markdown = cloneDeep(pane.markdown) as TemplateMarkdown;
1218
+ duplicatedPane.markdown.id = pane?.markdown?.id || ulid();
1219
+ duplicatedPane.markdown.markdownId = pane?.markdown?.markdownId || ulid();
1220
+ duplicatedPane.markdown.parentId = duplicatedPaneId;
1221
+
1222
+ let markdownNodes: TemplateNode[] = [];
1223
+ if (duplicatedPane.markdown.markdownBody) {
1224
+ const markdownGen = new MarkdownGenerator(this);
1225
+ markdownNodes = markdownGen.markdownToFlatNodes(
1226
+ duplicatedPane.markdown.markdownBody,
1227
+ duplicatedPane.markdown.id
1228
+ ) as TemplateNode[];
1229
+ allNodes = [...allNodes, duplicatedPane.markdown, ...markdownNodes];
1230
+ } else if (
1231
+ typeof duplicatedPane.markdown !== `undefined` &&
1232
+ typeof duplicatedPane.markdown.id === `string`
1233
+ ) {
1234
+ // Create a map to track the original node ID to its duplicated node ID
1235
+ const oldToNewIdMap = new Map<string, string>();
1236
+ // First pass: Clone nodes and generate new IDs
1237
+ const nodesClone =
1238
+ duplicatedPane?.markdown?.nodes?.map((originalNode) => {
1239
+ const newNode = cloneDeep(originalNode);
1240
+ newNode.id = ulid();
1241
+ oldToNewIdMap.set(originalNode.id, newNode.id);
1242
+ return newNode;
1243
+ }) || [];
1244
+ // Second pass: Update parent IDs using the mapping
1245
+ nodesClone.forEach((node) => {
1246
+ // Special case for direct children of markdown
1247
+ if (node.parentId === pane?.markdown?.id) {
1248
+ node.parentId = duplicatedPane?.markdown?.id || '';
1249
+ } else {
1250
+ // For all other nodes, use the mapping to find the new parent ID
1251
+ const newParentId = oldToNewIdMap.get(node.parentId || '');
1252
+ if (newParentId) {
1253
+ node.parentId = newParentId;
1254
+ }
1255
+ }
1256
+ markdownNodes.push(node);
1257
+ });
1258
+ allNodes = [...allNodes, duplicatedPane.markdown, ...markdownNodes];
1259
+ }
1260
+ }
1261
+
1262
+ if (duplicatedPane.bgPane) {
1263
+ const bgPaneId = ulid();
1264
+
1265
+ if (duplicatedPane.bgPane.type === 'visual-break') {
1266
+ const visualBreakPane = duplicatedPane.bgPane as VisualBreakNode;
1267
+ const bgPaneNode: VisualBreakNode = {
1268
+ id: bgPaneId,
1269
+ nodeType: 'BgPane',
1270
+ parentId: duplicatedPaneId,
1271
+ type: 'visual-break',
1272
+ breakDesktop: visualBreakPane.breakDesktop,
1273
+ breakTablet: visualBreakPane.breakTablet,
1274
+ breakMobile: visualBreakPane.breakMobile,
1275
+ };
1276
+ allNodes.push(bgPaneNode);
1277
+ } else if (duplicatedPane.bgPane.type === 'artpack-image') {
1278
+ const artpackBgPane = duplicatedPane.bgPane as ArtpackImageNode;
1279
+ const bgPaneNode: ArtpackImageNode = {
1280
+ id: bgPaneId,
1281
+ nodeType: 'BgPane',
1282
+ parentId: duplicatedPaneId,
1283
+ type: 'artpack-image',
1284
+ collection: artpackBgPane.collection,
1285
+ image: artpackBgPane.image,
1286
+ src: artpackBgPane.src,
1287
+ srcSet: artpackBgPane.srcSet,
1288
+ alt: artpackBgPane.alt || `Artpack image`,
1289
+ objectFit: artpackBgPane.objectFit || 'cover',
1290
+ };
1291
+ allNodes.push(bgPaneNode);
1292
+ }
1293
+ delete duplicatedPane.bgPane;
1294
+ }
1295
+
1296
+ const storyFragmentNode = ownerNode as StoryFragmentNode;
1297
+ let specificIdx = -1;
1298
+ let elIdx = -1;
1299
+ let storyFragmentWasChanged: boolean = false;
1300
+
1301
+ if (
1302
+ insertPaneId &&
1303
+ location &&
1304
+ storyFragmentNode?.nodeType === 'StoryFragment'
1305
+ ) {
1306
+ storyFragmentWasChanged = storyFragmentNode.isChanged || false;
1307
+ specificIdx = storyFragmentNode.paneIds.indexOf(insertPaneId);
1308
+ elIdx = specificIdx;
1309
+ if (elIdx === -1) {
1310
+ storyFragmentNode.paneIds.push(duplicatedPane.id);
1311
+ } else {
1312
+ if (location === 'before') {
1313
+ storyFragmentNode.paneIds.splice(elIdx, 0, duplicatedPane.id);
1314
+ specificIdx = Math.max(0, specificIdx - 1);
1315
+ } else {
1316
+ storyFragmentNode.paneIds.splice(elIdx + 1, 0, duplicatedPane.id);
1317
+ specificIdx = Math.min(
1318
+ specificIdx + 1,
1319
+ storyFragmentNode.paneIds.length
1320
+ );
1321
+ }
1322
+ }
1323
+ storyFragmentNode.isChanged = true;
1324
+ }
1325
+
1326
+ this.addNode(duplicatedPane as PaneNode);
1327
+ this.linkChildToParent(
1328
+ duplicatedPane.id,
1329
+ duplicatedPane.parentId,
1330
+ specificIdx
1331
+ );
1332
+ this.addNodes(allNodes);
1333
+ this.notifyNode(ownerId);
1334
+
1335
+ this.history.addPatch({
1336
+ op: PatchOp.ADD,
1337
+ undo: (ctx) => {
1338
+ ctx.deleteNodes(allNodes);
1339
+
1340
+ if (
1341
+ storyFragmentNode &&
1342
+ storyFragmentNode.nodeType === 'StoryFragment' &&
1343
+ Array.isArray(storyFragmentNode.paneIds)
1344
+ ) {
1345
+ storyFragmentNode.paneIds = storyFragmentNode.paneIds.filter(
1346
+ (id: string) => id !== duplicatedPane.id
1347
+ );
1348
+ storyFragmentNode.isChanged = storyFragmentWasChanged;
1349
+ }
1350
+
1351
+ ctx.deleteNodes([duplicatedPane]);
1352
+ },
1353
+ redo: (ctx) => {
1354
+ if (storyFragmentNode?.nodeType === 'StoryFragment') {
1355
+ if (elIdx === -1) {
1356
+ storyFragmentNode.paneIds.push(duplicatedPane.id);
1357
+ } else {
1358
+ if (location === 'before') {
1359
+ storyFragmentNode.paneIds.splice(elIdx, 0, duplicatedPane.id);
1360
+ } else {
1361
+ storyFragmentNode.paneIds.splice(elIdx + 1, 0, duplicatedPane.id);
1362
+ }
1363
+ }
1364
+ storyFragmentNode.isChanged = true;
1365
+ }
1366
+
1367
+ ctx.addNodes([duplicatedPane]);
1368
+ ctx.linkChildToParent(
1369
+ duplicatedPane.id,
1370
+ duplicatedPane.parentId,
1371
+ specificIdx
1372
+ );
1373
+ ctx.addNodes(allNodes);
1374
+ },
1375
+ });
1376
+
1377
+ return duplicatedPaneId;
1378
+ }
1379
+
1380
+ handleInsertSignal(tagName: string, nodeId: string) {
1381
+ switch (tagName) {
1382
+ case `a`:
1383
+ settingsPanelStore.set({
1384
+ action: `style-link`,
1385
+ nodeId: nodeId,
1386
+ expanded: true,
1387
+ });
1388
+ break;
1389
+ case `img`:
1390
+ settingsPanelStore.set({
1391
+ action: `style-image`,
1392
+ nodeId: nodeId,
1393
+ expanded: true,
1394
+ });
1395
+ break;
1396
+ case `code`:
1397
+ settingsPanelStore.set({
1398
+ action: `style-widget`,
1399
+ nodeId: nodeId,
1400
+ expanded: true,
1401
+ });
1402
+ break;
1403
+ }
1404
+ this.toolModeValStore.set({ value: 'text' });
1405
+ }
1406
+
1407
+ addTemplateImpressionNode(targetId: string, node: ImpressionNode) {
1408
+ const targetNode = this.allNodes.get().get(targetId) as BaseNode;
1409
+ if (!targetNode || targetNode.nodeType !== 'Pane') {
1410
+ return;
1411
+ }
1412
+ const duplicatedNodes = cloneDeep(node) as TemplateNode;
1413
+ const flattenedNodes = this.setupTemplateNodeRecursively(
1414
+ duplicatedNodes,
1415
+ targetId
1416
+ );
1417
+ this.addNodes(flattenedNodes);
1418
+ this.history.addPatch({
1419
+ op: PatchOp.ADD,
1420
+ undo: (ctx) => ctx.deleteNodes(flattenedNodes),
1421
+ redo: (ctx) => ctx.addNodes(flattenedNodes),
1422
+ });
1423
+ }
1424
+
1425
+ addTemplateNode(
1426
+ targetId: string,
1427
+ node: TemplateNode,
1428
+ insertNodeId?: string,
1429
+ location?: 'before' | 'after'
1430
+ ): string | null {
1431
+ let targetNode = this.allNodes.get().get(targetId) as BaseNode;
1432
+
1433
+ // 1. VALIDATE TARGET NODE
1434
+ // Allow Pane, Markdown, or TagElement as valid targets.
1435
+ if (
1436
+ !targetNode ||
1437
+ (targetNode.nodeType !== 'Pane' &&
1438
+ targetNode.nodeType !== 'Markdown' &&
1439
+ targetNode.nodeType !== 'TagElement')
1440
+ ) {
1441
+ console.error('addTemplateNode received an invalid targetId.');
1442
+ return null;
1443
+ }
1444
+
1445
+ // 2. PREPARE PARENT AND STATE VARIABLES
1446
+ let parentId =
1447
+ targetNode.nodeType === 'Markdown' || targetNode.nodeType === 'Pane'
1448
+ ? targetId
1449
+ : this.getClosestNodeTypeFromId(targetId, 'Markdown');
1450
+ const paneNodeId = this.getClosestNodeTypeFromId(targetId, 'Pane');
1451
+ const originalPaneNode = this.allNodes.get().get(paneNodeId)
1452
+ ? cloneDeep(this.allNodes.get().get(paneNodeId) as PaneNode)
1453
+ : null;
1454
+
1455
+ let autoCreatedMarkdownNode: MarkdownPaneFragmentNode | null = null;
1456
+
1457
+ // 3. HANDLE EMPTY PANE BY AUTO-CREATING A MARKDOWN NODE
1458
+ if (targetNode.nodeType === 'Pane') {
1459
+ // Create a minimal markdown node to act as the container
1460
+ const newMarkdownNode: MarkdownPaneFragmentNode = {
1461
+ id: ulid(),
1462
+ nodeType: 'Markdown',
1463
+ parentId: targetId,
1464
+ type: 'markdown',
1465
+ markdownId: ulid(),
1466
+ defaultClasses: {},
1467
+ };
1468
+
1469
+ autoCreatedMarkdownNode = newMarkdownNode;
1470
+
1471
+ // Add the new markdown node to the state
1472
+ this.addNode(newMarkdownNode);
1473
+
1474
+ // Update the parentId to be this new markdown node for the next step
1475
+ parentId = newMarkdownNode.id;
1476
+ }
1477
+
1478
+ // 4. PREPARE THE NEW ELEMENT NODES
1479
+ const duplicatedNodes = cloneDeep(node) as TemplateNode;
1480
+ let flattenedNodes: TemplateNode[] = [];
1481
+
1482
+ if (['img', 'code'].includes(duplicatedNodes.tagName)) {
1483
+ let closestListNode = '';
1484
+ if (
1485
+ 'tagName' in targetNode &&
1486
+ ['ol', 'ul'].includes(targetNode.tagName as string)
1487
+ ) {
1488
+ closestListNode = targetId;
1489
+ } else {
1490
+ closestListNode = this.getParentNodeByTagNames(targetId, ['ol', 'ul']);
1491
+ }
1492
+
1493
+ if (!closestListNode) {
1494
+ const ulNode: TemplateNode = {
1495
+ id: ulid(),
1496
+ nodeType: 'TagElement',
1497
+ tagName: 'ul',
1498
+ parentId: parentId,
1499
+ };
1500
+ const liNode: TemplateNode = {
1501
+ id: ulid(),
1502
+ nodeType: 'TagElement',
1503
+ tagName: 'li',
1504
+ tagNameCustom: duplicatedNodes.tagName,
1505
+ parentId: ulNode.id,
1506
+ };
1507
+ duplicatedNodes.parentId = liNode.id;
1508
+ flattenedNodes = [
1509
+ ulNode,
1510
+ liNode,
1511
+ ...this.setupTemplateNodeRecursively(duplicatedNodes, liNode.id),
1512
+ ];
1513
+ } else {
1514
+ const liNode: TemplateNode = {
1515
+ id: ulid(),
1516
+ nodeType: 'TagElement',
1517
+ tagName: 'li',
1518
+ tagNameCustom: duplicatedNodes.tagName,
1519
+ parentId: closestListNode,
1520
+ };
1521
+ duplicatedNodes.parentId = liNode.id;
1522
+ flattenedNodes = [
1523
+ liNode,
1524
+ ...this.setupTemplateNodeRecursively(duplicatedNodes, liNode.id),
1525
+ ];
1526
+ }
1527
+ } else {
1528
+ flattenedNodes = this.setupTemplateNodeRecursively(
1529
+ duplicatedNodes,
1530
+ parentId
1531
+ );
1532
+ }
1533
+
1534
+ // 5. PERFORM REMAINING STATE MUTATIONS
1535
+ if (originalPaneNode) {
1536
+ this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
1537
+ notify: false,
1538
+ recordHistory: false,
1539
+ });
1540
+ }
1541
+
1542
+ this.addNodes(flattenedNodes);
1543
+
1544
+ const newTopLevelNodes = flattenedNodes.filter(
1545
+ (n) => n.parentId === parentId
1546
+ );
1547
+ const newTopLevelIds = newTopLevelNodes.map((n) => n.id);
1548
+
1549
+ const parentNodesMap = this.parentNodes.get();
1550
+ const parentChildren = parentNodesMap.get(parentId);
1551
+
1552
+ if (insertNodeId && location && parentChildren) {
1553
+ const insertIndex = parentChildren.indexOf(insertNodeId);
1554
+ if (insertIndex !== -1) {
1555
+ const currentChildren = parentChildren.filter(
1556
+ (id) => !newTopLevelIds.includes(id)
1557
+ );
1558
+ if (location === 'before') {
1559
+ currentChildren.splice(insertIndex, 0, ...newTopLevelIds);
1560
+ } else {
1561
+ currentChildren.splice(insertIndex + 1, 0, ...newTopLevelIds);
1562
+ }
1563
+ parentNodesMap.set(parentId, currentChildren);
1564
+ }
1565
+ }
1566
+
1567
+ // 6. RECORD THE ENTIRE ATOMIC OPERATION in a single history patch.
1568
+ this.history.addPatch({
1569
+ op: PatchOp.ADD,
1570
+ undo: (ctx) => {
1571
+ // Undo all changes: delete the element and the auto-created markdown node (if it exists)
1572
+ ctx.deleteNodes(flattenedNodes);
1573
+ if (autoCreatedMarkdownNode) {
1574
+ ctx.deleteNodes([autoCreatedMarkdownNode]);
1575
+ }
1576
+ if (originalPaneNode) {
1577
+ const newNodes = new Map(ctx.allNodes.get());
1578
+ newNodes.set(originalPaneNode.id, originalPaneNode);
1579
+ ctx.allNodes.set(newNodes);
1580
+ }
1581
+ if (paneNodeId) ctx.notifyNode(paneNodeId);
1582
+ },
1583
+ redo: (ctx) => {
1584
+ // Redo all changes in the correct order
1585
+ if (originalPaneNode) {
1586
+ ctx.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
1587
+ notify: false,
1588
+ recordHistory: false,
1589
+ });
1590
+ }
1591
+ if (autoCreatedMarkdownNode) {
1592
+ ctx.addNode(autoCreatedMarkdownNode);
1593
+ }
1594
+ ctx.addNodes(flattenedNodes);
1595
+
1596
+ // Re-apply insertion logic
1597
+ const parentNodesMap = ctx.parentNodes.get();
1598
+ const parentChildren = parentNodesMap.get(parentId);
1599
+ if (insertNodeId && location && parentChildren) {
1600
+ const insertIndex = parentChildren.indexOf(insertNodeId);
1601
+ if (insertIndex !== -1) {
1602
+ const currentChildren = parentChildren.filter(
1603
+ (id) => !newTopLevelIds.includes(id)
1604
+ );
1605
+ if (location === 'before') {
1606
+ currentChildren.splice(insertIndex, 0, ...newTopLevelIds);
1607
+ } else {
1608
+ currentChildren.splice(insertIndex + 1, 0, ...newTopLevelIds);
1609
+ }
1610
+ parentNodesMap.set(parentId, currentChildren);
1611
+ }
1612
+ }
1613
+ if (paneNodeId) ctx.notifyNode(paneNodeId);
1614
+ },
1615
+ });
1616
+
1617
+ // 7. SEND A SINGLE NOTIFICATION to update the UI.
1618
+ if (paneNodeId) {
1619
+ this.notifyNode(paneNodeId);
1620
+ }
1621
+
1622
+ return flattenedNodes.length > 0 ? flattenedNodes[0].id : null;
1623
+ }
1624
+
1625
+ setupTemplateNodeRecursively(node: TemplateNode, parentId: string) {
1626
+ let result: TemplateNode[] = [];
1627
+ if (!node) return result;
1628
+
1629
+ node.id = ulid();
1630
+ node.parentId = parentId;
1631
+ result.push(node);
1632
+ if ('nodes' in node && node.nodes) {
1633
+ for (let i = 0; i < node.nodes.length; ++i) {
1634
+ result = result.concat(
1635
+ this.setupTemplateNodeRecursively(node.nodes[i], node.id)
1636
+ );
1637
+ }
1638
+ }
1639
+ return result;
1640
+ }
1641
+
1642
+ deleteChildren(nodeId: string): BaseNode[] {
1643
+ const node = this.allNodes.get().get(nodeId);
1644
+ if (!node) return [];
1645
+
1646
+ const children = this.getNodesRecursively(node).reverse();
1647
+ children.shift();
1648
+ const deletedNodes = this.deleteNodes(children);
1649
+ this.notifyNode(node.id || '');
1650
+ return deletedNodes;
1651
+ }
1652
+
1653
+ deleteNode(nodeId: string) {
1654
+ // Get the original node
1655
+ const originalNode = this.allNodes.get().get(nodeId) as FlatNode;
1656
+ if (!originalNode) {
1657
+ return;
1658
+ }
1659
+
1660
+ // Track if we're redirecting deletion
1661
+ let targetNodeId = nodeId;
1662
+ let targetNode = originalNode;
1663
+
1664
+ // Case 1: Node is an LI - check if it's the last one in a list
1665
+ if (
1666
+ originalNode.nodeType === 'TagElement' &&
1667
+ 'tagName' in originalNode &&
1668
+ originalNode.tagName === 'li' &&
1669
+ originalNode.parentId
1670
+ ) {
1671
+ const listNode = this.allNodes
1672
+ .get()
1673
+ .get(originalNode.parentId) as FlatNode;
1674
+
1675
+ if (
1676
+ listNode &&
1677
+ 'tagName' in listNode &&
1678
+ (listNode.tagName === 'ul' || listNode.tagName === 'ol')
1679
+ ) {
1680
+ // Check if this LI is the last/only one
1681
+ const listChildren = this.getChildNodeIDs(listNode.id);
1682
+ const isLastLi =
1683
+ listChildren.length === 1 && listChildren[0] === nodeId;
1684
+
1685
+ if (isLastLi) {
1686
+ // Redirect deletion to the list
1687
+ targetNodeId = listNode.id;
1688
+ targetNode = listNode;
1689
+ }
1690
+ }
1691
+ }
1692
+
1693
+ // Case 2: Node is an image or code inside an LI
1694
+ else if (
1695
+ originalNode.nodeType === 'TagElement' &&
1696
+ 'tagName' in originalNode &&
1697
+ (originalNode.tagName === 'img' || originalNode.tagName === 'code')
1698
+ ) {
1699
+ // Find parent LI
1700
+ const liParentId = this.getParentNodeByTagNames(nodeId, ['li']);
1701
+
1702
+ if (liParentId) {
1703
+ const liNode = this.allNodes.get().get(liParentId) as FlatNode;
1704
+
1705
+ // Check if this is the only child of the LI
1706
+ const liChildren = this.getChildNodeIDs(liParentId);
1707
+
1708
+ // Calculate if content is the only significant child
1709
+ // (there might be text nodes with whitespace)
1710
+ const significantChildrenCount = liChildren.filter((childId) => {
1711
+ const child = this.allNodes.get().get(childId) as FlatNode;
1712
+ if (!child) return false;
1713
+
1714
+ // Skip text nodes with only whitespace
1715
+ if (
1716
+ child.tagName === 'text' &&
1717
+ (!child.copy || child.copy.trim() === '')
1718
+ ) {
1719
+ return false;
1720
+ }
1721
+ return true;
1722
+ }).length;
1723
+
1724
+ const isOnlySignificantChild = significantChildrenCount === 1;
1725
+
1726
+ if (isOnlySignificantChild && liNode?.parentId) {
1727
+ // Find list container (UL/OL)
1728
+ const listNode = this.allNodes.get().get(liNode.parentId) as FlatNode;
1729
+
1730
+ if (
1731
+ listNode &&
1732
+ 'tagName' in listNode &&
1733
+ (listNode.tagName === 'ul' || listNode.tagName === 'ol')
1734
+ ) {
1735
+ // Check if this LI is the last/only one
1736
+ const listChildren = this.getChildNodeIDs(listNode.id);
1737
+ const isLastLi =
1738
+ listChildren.length === 1 && listChildren[0] === liParentId;
1739
+
1740
+ if (isLastLi) {
1741
+ // Redirect deletion to the list
1742
+ targetNodeId = listNode.id;
1743
+ targetNode = listNode;
1744
+ } else {
1745
+ // Redirect to the LI instead
1746
+ targetNodeId = liParentId;
1747
+ targetNode = liNode;
1748
+ }
1749
+ }
1750
+ }
1751
+ }
1752
+ }
1753
+
1754
+ // Continue with normal deletion logic using the target node
1755
+ const parentId = targetNode.parentId;
1756
+ const toDelete = this.getNodesRecursively(targetNode).reverse();
1757
+ const closestMarkdownId = this.getClosestNodeTypeFromId(
1758
+ targetNode.id,
1759
+ 'Markdown'
1760
+ );
1761
+
1762
+ this.deleteNodes(toDelete);
1763
+ let paneIdx: number = -1;
1764
+
1765
+ // Process based on node type
1766
+ if (parentId !== null) {
1767
+ if (targetNode.nodeType === 'Pane') {
1768
+ const storyFragment = this.allNodes
1769
+ .get()
1770
+ .get(parentId) as StoryFragmentNode;
1771
+ if (storyFragment) {
1772
+ paneIdx = storyFragment.paneIds.indexOf(targetNodeId);
1773
+ storyFragment.paneIds.splice(paneIdx, 1);
1774
+ }
1775
+ } else if (targetNode.nodeType === 'TagElement') {
1776
+ // mark pane as changed
1777
+ const paneNodeId = this.getClosestNodeTypeFromId(
1778
+ closestMarkdownId,
1779
+ 'Pane'
1780
+ );
1781
+ if (paneNodeId) {
1782
+ const paneNode = cloneDeep(
1783
+ this.allNodes.get().get(paneNodeId)
1784
+ ) as PaneNode;
1785
+ if (paneNode) {
1786
+ this.modifyNodes([{ ...paneNode, isChanged: true }]);
1787
+ }
1788
+ }
1789
+ }
1790
+ } else {
1791
+ if (targetNodeId === this.rootNodeId.get()) {
1792
+ this.rootNodeId.set('');
1793
+ }
1794
+ }
1795
+
1796
+ this.notifyNode(ROOT_NODE_NAME);
1797
+
1798
+ // Add to history for undo/redo
1799
+ this.history.addPatch({
1800
+ op: PatchOp.REMOVE,
1801
+ undo: (ctx) => {
1802
+ ctx.addNodes(toDelete);
1803
+ if (targetNode.nodeType === 'Pane' && parentId !== null) {
1804
+ const storyFragment = this.allNodes
1805
+ .get()
1806
+ .get(parentId) as StoryFragmentNode;
1807
+ if (storyFragment) {
1808
+ storyFragment.paneIds.splice(paneIdx, 0, targetNodeId);
1809
+ this.linkChildToParent(targetNodeId, parentId, paneIdx);
1810
+ }
1811
+ }
1812
+ },
1813
+ redo: (ctx) => ctx.deleteNodes(toDelete),
1814
+ });
1815
+ }
1816
+
1817
+ getNodesRecursively(node: BaseNode | undefined): BaseNode[] {
1818
+ let nodes: BaseNode[] = [];
1819
+ if (!node) return nodes;
1820
+
1821
+ this.getChildNodeIDs(node.id).forEach((id) => {
1822
+ const collectedNodes = this.getNodesRecursively(
1823
+ this.allNodes.get().get(id)
1824
+ );
1825
+ nodes = collectedNodes.concat(nodes);
1826
+ });
1827
+
1828
+ nodes.push(node);
1829
+ return nodes;
1830
+ }
1831
+
1832
+ moveNode(nodeId: string, location: 'before' | 'after') {
1833
+ const node = this.allNodes.get().get(nodeId);
1834
+ if (!node || node.nodeType === 'Root') return;
1835
+
1836
+ if (node.parentId) {
1837
+ const children = this.getChildNodeIDs(node.parentId);
1838
+ const idx = children.indexOf(nodeId);
1839
+ if (idx !== -1) {
1840
+ const newPosNodeId = children.at(
1841
+ location === 'before'
1842
+ ? Math.max(idx - 1, 0)
1843
+ : Math.min(idx + 1, children.length - 1)
1844
+ );
1845
+ if (newPosNodeId) {
1846
+ this.moveNodeTo(nodeId, newPosNodeId, location);
1847
+ }
1848
+ }
1849
+ }
1850
+ }
1851
+
1852
+ moveNodeTo(
1853
+ nodeId: string,
1854
+ insertNodeId: string,
1855
+ location: 'before' | 'after'
1856
+ ) {
1857
+ const node = this.allNodes.get().get(nodeId);
1858
+ if (!node || node.nodeType === 'Root') return;
1859
+
1860
+ const newLocationNode = this.allNodes.get().get(insertNodeId);
1861
+ if (!newLocationNode) return;
1862
+
1863
+ if (nodeId === insertNodeId) return;
1864
+
1865
+ if (node.nodeType !== newLocationNode.nodeType) {
1866
+ console.warn(
1867
+ `Trying to move nodes ${nodeId} and ${insertNodeId} but they're belong to different types`
1868
+ );
1869
+ return;
1870
+ }
1871
+
1872
+ const oldParentId = node.parentId || '';
1873
+ const oldParentNodes = this.getChildNodeIDs(oldParentId);
1874
+ const originalIdx = oldParentNodes.indexOf(nodeId);
1875
+
1876
+ // Capture original state for history
1877
+ let originalPaneIds: string[] | null = null;
1878
+ if (node.nodeType === 'Pane') {
1879
+ const storyFragmentId = this.getClosestNodeTypeFromId(
1880
+ node.id,
1881
+ 'StoryFragment'
1882
+ );
1883
+ const storyFragment = this.allNodes
1884
+ .get()
1885
+ .get(storyFragmentId) as StoryFragmentNode;
1886
+ if (storyFragment) {
1887
+ originalPaneIds = [...storyFragment.paneIds];
1888
+ }
1889
+ }
1890
+
1891
+ moveNodeAtLocationInContext(
1892
+ oldParentNodes,
1893
+ originalIdx,
1894
+ newLocationNode,
1895
+ insertNodeId,
1896
+ nodeId,
1897
+ location,
1898
+ node,
1899
+ this
1900
+ );
1901
+
1902
+ if (node.nodeType === 'Pane') {
1903
+ const storyFragmentId = this.getClosestNodeTypeFromId(
1904
+ node.id,
1905
+ 'StoryFragment'
1906
+ );
1907
+ const storyFragment = cloneDeep(
1908
+ this.allNodes.get().get(storyFragmentId)
1909
+ ) as StoryFragmentNode;
1910
+ if (storyFragment) {
1911
+ this.modifyNodes([{ ...storyFragment, isChanged: true }]);
1912
+ }
1913
+ } else {
1914
+ const parentNode = this.nodeToNotify(
1915
+ newLocationNode?.parentId || '',
1916
+ newLocationNode.nodeType
1917
+ );
1918
+ this.notifyNode(parentNode || '');
1919
+ }
1920
+
1921
+ this.history.addPatch({
1922
+ op: PatchOp.REPLACE,
1923
+ undo: (ctx) => {
1924
+ const oldParentNodes = ctx.getChildNodeIDs(node.parentId || '');
1925
+ const newParentNodes = ctx.getChildNodeIDs(
1926
+ newLocationNode.parentId || ''
1927
+ );
1928
+ if (newParentNodes) {
1929
+ newParentNodes.splice(newParentNodes.indexOf(nodeId), 1);
1930
+ }
1931
+ if (oldParentNodes) {
1932
+ oldParentNodes.splice(originalIdx, 0, nodeId);
1933
+ }
1934
+ node.parentId = oldParentId;
1935
+
1936
+ if (node.nodeType === 'Pane' && originalPaneIds) {
1937
+ const storyFragmentId = ctx.getClosestNodeTypeFromId(
1938
+ node.id,
1939
+ 'StoryFragment'
1940
+ );
1941
+ const storyFragment = cloneDeep(
1942
+ ctx.allNodes.get().get(storyFragmentId)
1943
+ ) as StoryFragmentNode;
1944
+ if (storyFragment) {
1945
+ storyFragment.paneIds = [...originalPaneIds];
1946
+ this.modifyNodes([{ ...storyFragment, isChanged: true }]);
1947
+ }
1948
+ }
1949
+
1950
+ //const parentNode = ctx.nodeToNotify(node?.parentId || "", node.nodeType);
1951
+ ctx.notifyNode(node.id || '');
1952
+ },
1953
+ redo: (ctx) => {
1954
+ moveNodeAtLocationInContext(
1955
+ oldParentNodes,
1956
+ originalIdx,
1957
+ newLocationNode,
1958
+ insertNodeId,
1959
+ nodeId,
1960
+ location,
1961
+ node,
1962
+ ctx
1963
+ );
1964
+
1965
+ if (node.nodeType === 'Pane') {
1966
+ const storyFragmentId = ctx.getClosestNodeTypeFromId(
1967
+ node.id,
1968
+ 'StoryFragment'
1969
+ );
1970
+ const storyFragment = cloneDeep(
1971
+ ctx.allNodes.get().get(storyFragmentId)
1972
+ ) as StoryFragmentNode;
1973
+ if (storyFragment) {
1974
+ this.modifyNodes([{ ...storyFragment, isChanged: true }]);
1975
+ }
1976
+ }
1977
+ },
1978
+ });
1979
+ }
1980
+
1981
+ getPaneImageFileIds(paneId: string): string[] {
1982
+ const paneNode = this.allNodes.get().get(paneId);
1983
+ if (!paneNode || paneNode.nodeType !== 'Pane') return [];
1984
+
1985
+ const allNodes = this.getNodesRecursively(paneNode);
1986
+
1987
+ const embeddedFileIds = allNodes
1988
+ .filter(
1989
+ (node): node is FlatNode =>
1990
+ node.nodeType === 'TagElement' &&
1991
+ 'tagName' in node &&
1992
+ node.tagName === 'img' &&
1993
+ 'fileId' in node &&
1994
+ typeof node.fileId === 'string'
1995
+ )
1996
+ .map((node) => node.fileId)
1997
+ .filter((id): id is string => id !== undefined);
1998
+
1999
+ const bgFileIds = allNodes
2000
+ .filter(
2001
+ (node): node is any =>
2002
+ node.nodeType === 'BgPane' &&
2003
+ 'type' in node &&
2004
+ node.type === 'background-image' &&
2005
+ 'fileId' in node &&
2006
+ typeof node.fileId === 'string'
2007
+ )
2008
+ .map((node) => node.fileId)
2009
+ .filter((id): id is string => id !== undefined);
2010
+
2011
+ return [...embeddedFileIds, ...bgFileIds];
2012
+ }
2013
+
2014
+ getPaneImagesMap(): Record<string, string[]> {
2015
+ const paneNodes = Array.from(this.allNodes.get().values()).filter(
2016
+ (node): node is PaneNode => node.nodeType === 'Pane'
2017
+ );
2018
+ const result: Record<string, string[]> = {};
2019
+ paneNodes.forEach((pane) => {
2020
+ const fileIds = this.getPaneImageFileIds(pane.id);
2021
+ if (fileIds.length > 0) {
2022
+ result[pane.id] = fileIds;
2023
+ }
2024
+ });
2025
+ return result;
2026
+ }
2027
+
2028
+ insertPaneId(
2029
+ storyfragmentId: string,
2030
+ paneId: string,
2031
+ insertId?: string,
2032
+ location?: 'before' | 'after'
2033
+ ) {
2034
+ const storyfragment = this.allNodes
2035
+ .get()
2036
+ .get(storyfragmentId) as StoryFragmentNode;
2037
+ if (!storyfragment || storyfragment.nodeType !== 'StoryFragment') {
2038
+ console.warn('Invalid storyfragment ID in insertPaneId');
2039
+ return;
2040
+ }
2041
+
2042
+ const newPaneIds = [...storyfragment.paneIds];
2043
+
2044
+ if (!insertId) {
2045
+ newPaneIds.push(paneId);
2046
+ } else {
2047
+ const insertIdx = newPaneIds.indexOf(insertId);
2048
+ if (insertIdx === -1) {
2049
+ console.warn('Insert reference pane not found, adding to end.');
2050
+ newPaneIds.push(paneId);
2051
+ } else {
2052
+ const targetIdx = location === 'before' ? insertIdx : insertIdx + 1;
2053
+ newPaneIds.splice(targetIdx, 0, paneId);
2054
+ }
2055
+ }
2056
+
2057
+ // Create the updated node object with a clear type
2058
+ const updatedStoryFragment: StoryFragmentNode = {
2059
+ ...storyfragment,
2060
+ paneIds: newPaneIds,
2061
+ isChanged: true,
2062
+ };
2063
+
2064
+ // Pass the correctly typed object to modifyNodes
2065
+ this.modifyNodes([updatedStoryFragment]);
2066
+ }
2067
+
2068
+ isSlugValid(
2069
+ slug: string,
2070
+ currentNodeId?: string
2071
+ ): { isValid: boolean; error?: string } {
2072
+ // Early validation for empty slugs
2073
+ if (!slug || slug.length < 3) {
2074
+ return { isValid: false, error: 'Slug must be at least 3 characters' };
2075
+ }
2076
+ // Check against reserved slugs
2077
+ if (reservedSlugs.includes(slug)) {
2078
+ return {
2079
+ isValid: false,
2080
+ error: 'This URL is reserved and cannot be used',
2081
+ };
2082
+ }
2083
+ // Check if slug contains only valid characters (alphanumeric, hyphens)
2084
+ const validSlugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2085
+ if (!validSlugPattern.test(slug)) {
2086
+ return {
2087
+ isValid: false,
2088
+ error: 'Slug can only contain lowercase letters, numbers, and hyphens',
2089
+ };
2090
+ }
2091
+ // Check for duplicate slugs
2092
+ const nodes = Array.from(this.allNodes.get().values());
2093
+ const duplicateNode = nodes.find(
2094
+ (node) =>
2095
+ (node.nodeType === 'StoryFragment' || node.nodeType === 'Pane') &&
2096
+ 'slug' in node &&
2097
+ node.slug === slug &&
2098
+ node.id !== currentNodeId
2099
+ );
2100
+ if (duplicateNode) {
2101
+ return {
2102
+ isValid: false,
2103
+ error: `This URL is already in use by ${duplicateNode.nodeType === 'Pane' ? 'pane' : 'page'}: ${(duplicateNode as PaneNode).title}`,
2104
+ };
2105
+ }
2106
+ return { isValid: true };
2107
+ }
2108
+
2109
+ generateValidSlug(title: string, currentNodeId?: string): string {
2110
+ // Convert title to lowercase and replace spaces/special chars with hyphens
2111
+ const slug = title
2112
+ .toLowerCase()
2113
+ .replace(/[^a-z0-9]+/g, '-')
2114
+ .replace(/^-+|-+$/g, '');
2115
+ // If the base slug is already valid and unique, use it
2116
+ if (this.isSlugValid(slug, currentNodeId)) {
2117
+ return slug;
2118
+ }
2119
+ // Otherwise, append numbers until we find a unique slug
2120
+ let counter = 1;
2121
+ let newSlug = slug;
2122
+ while (!this.isSlugValid(newSlug, currentNodeId)) {
2123
+ newSlug = `${slug}-${counter}`;
2124
+ counter++;
2125
+ }
2126
+ return newSlug;
2127
+ }
2128
+
2129
+ isBunnyVideoNode(node: BaseNode): boolean {
2130
+ if (node.nodeType === 'Pane' && 'codeHookTarget' in node) {
2131
+ return (node as PaneNode).codeHookTarget === 'bunny-video';
2132
+ }
2133
+ if (node.nodeType === 'TagElement' && 'tagName' in node) {
2134
+ const flatNode = node as FlatNode;
2135
+ return (
2136
+ flatNode.tagName === 'code' &&
2137
+ 'codeHookParams' in flatNode &&
2138
+ Array.isArray(flatNode.codeHookParams) &&
2139
+ typeof flatNode.copy === 'string' &&
2140
+ flatNode.copy.includes('bunny(')
2141
+ );
2142
+ }
2143
+ return false;
2144
+ }
2145
+
2146
+ getBunnyVideoUrl(nodeId: string): string | string[] | null {
2147
+ const node = this.allNodes.get().get(nodeId);
2148
+ if (!node) return null;
2149
+
2150
+ if (node.nodeType === 'Pane' && 'codeHookPayload' in node) {
2151
+ const paneNode = node as PaneNode;
2152
+ try {
2153
+ if (
2154
+ paneNode.codeHookPayload &&
2155
+ typeof paneNode.codeHookPayload.options === 'string'
2156
+ ) {
2157
+ const options = JSON.parse(paneNode.codeHookPayload.options);
2158
+ return options.videoUrl || null;
2159
+ }
2160
+ } catch (error) {
2161
+ console.error('Error parsing Bunny video options:', error);
2162
+ }
2163
+ }
2164
+
2165
+ if (node.nodeType === 'TagElement' && 'codeHookParams' in node) {
2166
+ const flatNode = node as FlatNode;
2167
+ if (
2168
+ Array.isArray(flatNode.codeHookParams) &&
2169
+ flatNode.codeHookParams.length > 0
2170
+ ) {
2171
+ return flatNode.codeHookParams[0];
2172
+ }
2173
+ }
2174
+
2175
+ return null;
2176
+ }
2177
+
2178
+ getAllBunnyVideoInfo(): { url: string; title: string; videoId: string }[] {
2179
+ const results: { url: string; title: string; videoId: string }[] = [];
2180
+ const processedVideoIds = new Set<string>();
2181
+
2182
+ // Find panes with bunny-video code hook
2183
+ const allNodes = Array.from(this.allNodes.get().values());
2184
+ const paneNodes = allNodes.filter(
2185
+ (node) =>
2186
+ node.nodeType === 'Pane' &&
2187
+ 'codeHookTarget' in node &&
2188
+ node.codeHookTarget === 'bunny-video'
2189
+ ) as PaneNode[];
2190
+
2191
+ // Process pane-level bunny videos
2192
+ for (const paneNode of paneNodes) {
2193
+ try {
2194
+ if (
2195
+ paneNode.codeHookPayload &&
2196
+ typeof paneNode.codeHookPayload.options === 'string'
2197
+ ) {
2198
+ const options = JSON.parse(paneNode.codeHookPayload.options);
2199
+ const url = options.videoUrl || '';
2200
+ const title = options.title || 'Untitled Video';
2201
+
2202
+ if (url && typeof url === 'string') {
2203
+ let videoId = '';
2204
+ try {
2205
+ const urlObj = new URL(url);
2206
+ if (
2207
+ urlObj.hostname === 'iframe.mediadelivery.net' &&
2208
+ urlObj.pathname.startsWith('/embed/')
2209
+ ) {
2210
+ const pathParts = urlObj.pathname.split('/');
2211
+ if (pathParts.length >= 4) {
2212
+ videoId = `${pathParts[2]}/${pathParts[3]}`;
2213
+ }
2214
+ }
2215
+ } catch (error) {
2216
+ console.error('Error extracting video ID from URL:', error);
2217
+ }
2218
+
2219
+ if (videoId && !processedVideoIds.has(videoId)) {
2220
+ results.push({
2221
+ url: url,
2222
+ title: typeof title === 'string' ? title : 'Untitled Video',
2223
+ videoId,
2224
+ });
2225
+ processedVideoIds.add(videoId);
2226
+ }
2227
+ }
2228
+ }
2229
+ } catch (error) {
2230
+ console.error('Error parsing Bunny video options:', error);
2231
+ }
2232
+ }
2233
+
2234
+ // Find inline bunny widgets
2235
+ const codeNodes = allNodes.filter(
2236
+ (node) =>
2237
+ node.nodeType === 'TagElement' &&
2238
+ 'tagName' in node &&
2239
+ node.tagName === 'code' &&
2240
+ 'codeHookParams' in node &&
2241
+ 'copy' in node &&
2242
+ typeof node.copy === 'string' &&
2243
+ node.copy.includes('bunny(')
2244
+ ) as FlatNode[];
2245
+
2246
+ // Process inline widgets
2247
+ for (const codeNode of codeNodes) {
2248
+ if (
2249
+ Array.isArray(codeNode.codeHookParams) &&
2250
+ codeNode.codeHookParams.length >= 2
2251
+ ) {
2252
+ const urlParam = codeNode.codeHookParams[0];
2253
+ const titleParam = codeNode.codeHookParams[1];
2254
+
2255
+ const url = Array.isArray(urlParam)
2256
+ ? urlParam[0]
2257
+ : String(urlParam || '');
2258
+ const title = Array.isArray(titleParam)
2259
+ ? titleParam[0]
2260
+ : String(titleParam || 'Untitled Video');
2261
+
2262
+ if (url) {
2263
+ let videoId = '';
2264
+ try {
2265
+ const urlObj = new URL(url);
2266
+ if (
2267
+ urlObj.hostname === 'iframe.mediadelivery.net' &&
2268
+ urlObj.pathname.startsWith('/embed/')
2269
+ ) {
2270
+ const pathParts = urlObj.pathname.split('/');
2271
+ if (pathParts.length >= 4) {
2272
+ videoId = `${pathParts[2]}/${pathParts[3]}`;
2273
+ }
2274
+ }
2275
+ } catch (error) {
2276
+ console.error('Error extracting video ID from URL:', error);
2277
+ }
2278
+
2279
+ if (videoId && !processedVideoIds.has(videoId)) {
2280
+ results.push({ url, title, videoId });
2281
+ processedVideoIds.add(videoId);
2282
+ }
2283
+ }
2284
+ }
2285
+ }
2286
+
2287
+ return results;
2288
+ }
2289
+
2290
+ mapNodeHierarchy(nodes: TemplateNode[]) {
2291
+ const nodeMap: Record<string, any> = {};
2292
+
2293
+ // First pass - create entries for all nodes
2294
+ nodes.forEach((node) => {
2295
+ nodeMap[node.id] = {
2296
+ id: node.id,
2297
+ nodeType: node.nodeType,
2298
+ tagName: node.tagName || 'N/A',
2299
+ parentId: node.parentId,
2300
+ children: [],
2301
+ };
2302
+ });
2303
+
2304
+ // Second pass - build the hierarchy
2305
+ nodes.forEach((node) => {
2306
+ if (node.parentId && nodeMap[node.parentId]) {
2307
+ nodeMap[node.parentId].children.push(nodeMap[node.id]);
2308
+ }
2309
+ });
2310
+
2311
+ // Return only the root nodes (those whose parents aren't in our node set)
2312
+ return nodes
2313
+ .filter((node) => !node.parentId || !nodeMap[node.parentId])
2314
+ .map((node) => nodeMap[node.id]);
2315
+ }
2316
+
2317
+ getDirtyNodesClassData(): { dirtyPaneIds: string[]; classes: string[] } {
2318
+ const dirtyNodes = this.getDirtyNodes();
2319
+ const dirtyPaneIds = dirtyNodes
2320
+ .filter((node) => node.nodeType === 'Pane')
2321
+ .map((node) => node.id);
2322
+ const classes = extractClassesFromNodes(dirtyNodes);
2323
+
2324
+ return { dirtyPaneIds, classes };
2325
+ }
2326
+
2327
+ /**
2328
+ * Executes a series of updates on a temporary context and then applies the
2329
+ * results to the main context in a single operation, triggering one UI update.
2330
+ * @param work - An async function that receives the temporary context and performs modifications.
2331
+ */
2332
+ async applyAtomicUpdate(
2333
+ work: (tmpCtx: NodesContext) => Promise<void>
2334
+ ): Promise<void> {
2335
+ // 1. Create a temporary, "off-screen" context
2336
+ const tmpCtx = new NodesContext();
2337
+ // Prime the temp context with the same root ID and other relevant state
2338
+ tmpCtx.rootNodeId.set(this.rootNodeId.get());
2339
+ tmpCtx.allNodes.set(new Map(this.allNodes.get()));
2340
+ tmpCtx.parentNodes.set(new Map(this.parentNodes.get()));
2341
+
2342
+ // 2. Execute the long-running work on the temporary context
2343
+ await work(tmpCtx);
2344
+
2345
+ // 3. Get the results from the temporary context
2346
+ const newNodes = tmpCtx.allNodes.get();
2347
+ const newParentRelations = tmpCtx.parentNodes.get();
2348
+
2349
+ // 4. Swap/Merge the results into the main context
2350
+ this.allNodes.set(newNodes);
2351
+ this.parentNodes.set(newParentRelations);
2352
+
2353
+ // 5. Trigger a single notification to re-render the UI
2354
+ this.notifyNode('root');
2355
+ }
2356
+
2357
+ private deleteNodes(nodesList: BaseNode[]): BaseNode[] {
2358
+ const deletedNodes: BaseNode[] = [];
2359
+
2360
+ nodesList.forEach((node) => {
2361
+ if (!node) return;
2362
+
2363
+ // Remove node
2364
+ const allNodes = this.allNodes.get();
2365
+ if (allNodes.delete(node.id)) {
2366
+ deletedNodes.push(node);
2367
+ }
2368
+
2369
+ // Remove parent link
2370
+ if (node?.parentId !== null) {
2371
+ const parentNodes = this.parentNodes.get();
2372
+ const parentNode = parentNodes.get(node.parentId);
2373
+ if (parentNode) {
2374
+ parentNode.splice(parentNode.indexOf(node.id), 1);
2375
+ this.parentNodes.set(new Map<string, string[]>(parentNodes));
2376
+ }
2377
+ }
2378
+ });
2379
+
2380
+ return deletedNodes;
2381
+ }
2382
+ }
2383
+
2384
+ export const globalCtx: NodesContext = new NodesContext();
2385
+
2386
+ export const getCtx = (
2387
+ props?: NodeProps | CompositorProps | WidgetProps
2388
+ ): NodesContext => {
2389
+ return props?.ctx || globalCtx;
2390
+ };