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,888 @@
1
+ import {
2
+ useState,
3
+ useEffect,
4
+ useRef,
5
+ type ChangeEvent,
6
+ type MouseEventHandler,
7
+ } from 'react';
8
+ import { useStore } from '@nanostores/react';
9
+ import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
10
+ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
11
+ import TagIcon from '@heroicons/react/24/outline/TagIcon';
12
+ import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
13
+ import ArrowUpTrayIcon from '@heroicons/react/24/outline/ArrowUpTrayIcon';
14
+ import ExclamationTriangleIcon from '@heroicons/react/24/outline/ExclamationTriangleIcon';
15
+ import {
16
+ fullContentMapStore,
17
+ setPendingImageOperation,
18
+ getPendingImageOperation,
19
+ storyFragmentTopicsStore,
20
+ } from '@/stores/storykeep';
21
+ import { getCtx } from '@/stores/nodes';
22
+ import { cloneDeep, findUniqueSlug, titleToSlug } from '@/utils/helpers';
23
+ import { isStoryFragmentNode } from '@/utils/compositor/typeGuards';
24
+ import OgImagePreview from '@/components/compositor/preview/OgImagePreview';
25
+ import {
26
+ validateFile,
27
+ resizeAndCropImage,
28
+ getImageDimensions,
29
+ type FileValidationOptions,
30
+ } from '@/utils/api/fileHelpers';
31
+ import type {
32
+ FullContentMapItem,
33
+ BrandConfig,
34
+ Topic,
35
+ } from '@/types/tractstack';
36
+ import type { ImageDimensions } from '@/types/formTypes';
37
+ import {
38
+ StoryFragmentMode,
39
+ type StoryFragmentNode,
40
+ } from '@/types/compositorTypes';
41
+
42
+ const TARGET_WIDTH = 1200;
43
+ const TARGET_HEIGHT = 630;
44
+
45
+ interface StoryFragmentOpenGraphPanelProps {
46
+ nodeId: string;
47
+ setMode: (mode: StoryFragmentMode) => void;
48
+ config?: BrandConfig;
49
+ }
50
+
51
+ const hasSlug = (
52
+ item: FullContentMapItem
53
+ ): item is FullContentMapItem & { slug: string } =>
54
+ 'slug' in item &&
55
+ typeof item.slug === 'string' &&
56
+ (item.type === 'StoryFragment' || item.type === 'Pane');
57
+
58
+ const StoryFragmentOpenGraphPanel = ({
59
+ nodeId,
60
+ setMode,
61
+ config,
62
+ }: StoryFragmentOpenGraphPanelProps) => {
63
+ const $contentMap = useStore(fullContentMapStore);
64
+ const $storyFragmentTopics = useStore(storyFragmentTopicsStore);
65
+ const storedData = $storyFragmentTopics[nodeId];
66
+
67
+ // Local state for draft changes
68
+ const [draftTitle, setDraftTitle] = useState('');
69
+ const [draftTopics, setDraftTopics] = useState<Topic[]>([]);
70
+ const [draftDetails, setDraftDetails] = useState('');
71
+ const [draftImagePath, setDraftImagePath] = useState<string | null>(null);
72
+ const [draftImageData, setDraftImageData] = useState<string | null>(null);
73
+ const [isValid, setIsValid] = useState(false);
74
+ const [warning, setWarning] = useState(false);
75
+ const [charCount, setCharCount] = useState(0);
76
+ const [existingTopics, setExistingTopics] = useState<Topic[]>([]);
77
+ const [newTopicTitle, setNewTopicTitle] = useState('');
78
+ const [loading, setLoading] = useState(true);
79
+ const [error, setError] = useState<string | null>(null);
80
+ const [dataFetched, setDataFetched] = useState(false);
81
+ const [isProcessing, setIsProcessing] = useState(false);
82
+ const [imageError, setImageError] = useState<string | null>(null);
83
+ const [hasChanges, setHasChanges] = useState(false);
84
+ const fileInputRef = useRef<HTMLInputElement>(null);
85
+
86
+ // Track initial state for change detection
87
+ const initialState = useRef<{
88
+ title: string;
89
+ details: string;
90
+ topics: Topic[];
91
+ socialImagePath: string | null;
92
+ textColor: string;
93
+ bgColor: string;
94
+ } | null>(null);
95
+
96
+ const ctx = getCtx();
97
+ const allNodes = ctx.allNodes.get();
98
+ const thisNode = allNodes.get(nodeId);
99
+
100
+ if (!thisNode || !isStoryFragmentNode(thisNode)) {
101
+ return null;
102
+ }
103
+ const storyfragmentNode = thisNode as StoryFragmentNode;
104
+
105
+ const initialized = useRef(false);
106
+
107
+ // Initialize draft state and colors
108
+ // Initialize draft state and colors
109
+ useEffect(() => {
110
+ const ogParams = ctx.getOgImageParams(nodeId);
111
+
112
+ // Only set initial values if not already set
113
+ if (!initialized.current) {
114
+ setDraftTitle(storyfragmentNode.title);
115
+ setCharCount(storyfragmentNode.title.length);
116
+ setDraftImagePath(storyfragmentNode.socialImagePath || null);
117
+
118
+ initialState.current = {
119
+ title: storyfragmentNode.title,
120
+ details: '',
121
+ topics: [],
122
+ socialImagePath: storyfragmentNode.socialImagePath ?? null,
123
+ textColor: ogParams.textColor,
124
+ bgColor: ogParams.bgColor,
125
+ };
126
+ }
127
+
128
+ const pendingOp = getPendingImageOperation(nodeId);
129
+ if (pendingOp) {
130
+ if (pendingOp.type === 'upload' && pendingOp.data) {
131
+ setDraftImageData(pendingOp.data);
132
+ setDraftImagePath(pendingOp.path || null);
133
+ } else if (pendingOp.type === 'remove') {
134
+ setDraftImagePath(null);
135
+ setDraftImageData(null);
136
+ }
137
+ }
138
+ }, [nodeId]);
139
+
140
+ // Handle color changes from OgImagePreview
141
+ const handleColorChange = (newTextColor: string, newBgColor: string) => {
142
+ if (!initialState.current) return;
143
+
144
+ if (
145
+ (newTextColor !== initialState.current.textColor ||
146
+ newBgColor !== initialState.current.bgColor) &&
147
+ draftImagePath?.includes('--')
148
+ ) {
149
+ setPendingImageOperation(nodeId, {
150
+ type: 'remove',
151
+ path: draftImagePath,
152
+ });
153
+ setDraftImagePath(null);
154
+ setDraftImageData(null);
155
+ }
156
+ };
157
+
158
+ const handleTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
159
+ const newTitle = e.target.value;
160
+ if (newTitle.length <= 70) {
161
+ setDraftTitle(newTitle);
162
+ setCharCount(newTitle.length);
163
+ setIsValid(newTitle.length >= 35 && newTitle.length <= 60);
164
+ setWarning(newTitle.length > 60 && newTitle.length <= 70);
165
+
166
+ if (
167
+ draftImagePath?.includes('--') &&
168
+ newTitle !== initialState.current?.title
169
+ ) {
170
+ setPendingImageOperation(nodeId, {
171
+ type: 'remove',
172
+ path: draftImagePath,
173
+ });
174
+ setDraftImagePath(null);
175
+ setDraftImageData(null);
176
+ }
177
+ }
178
+ };
179
+
180
+ useEffect(() => {
181
+ if (initialized.current || dataFetched) return;
182
+
183
+ setLoading(true);
184
+ try {
185
+ // Get all topics from the special "all-topics" entry in content map
186
+ const topicsContent = $contentMap.find(
187
+ (item) => item.type === 'Topic' && item.id === 'all-topics'
188
+ );
189
+
190
+ // Convert topic strings to Topic objects with mock IDs (since V2 doesn't expose topic IDs in content map)
191
+ const allTopicsArray = topicsContent?.topics || [];
192
+ const topicsWithIds: Topic[] = allTopicsArray.map(
193
+ (topicTitle, index) => ({
194
+ id: index + 1, // Mock ID - in V2 we don't have access to actual topic IDs from content map
195
+ title: topicTitle,
196
+ })
197
+ );
198
+
199
+ setExistingTopics(topicsWithIds);
200
+
201
+ let initialTopics: Topic[] = [];
202
+ let initialDescription = '';
203
+
204
+ // Check stored draft data first
205
+ if (storedData) {
206
+ initialTopics = Array.isArray(storedData.topics)
207
+ ? storedData.topics.map((t) => ({
208
+ id: typeof t.id === 'string' ? parseInt(t.id, 10) : (t.id ?? -1),
209
+ title: t.title,
210
+ }))
211
+ : [];
212
+ initialDescription = storedData.description || '';
213
+ setDraftTopics(initialTopics);
214
+ setDraftDetails(initialDescription);
215
+ } else {
216
+ // Fall back to content map data
217
+ const sfContent = $contentMap.find(
218
+ (item) => item.type === 'StoryFragment' && item.id === nodeId
219
+ );
220
+
221
+ if (sfContent && sfContent.topics && sfContent.topics.length > 0) {
222
+ initialTopics = sfContent.topics.map((topicTitle) => {
223
+ const existingTopic = topicsWithIds.find(
224
+ (t) => t.title.toLowerCase() === topicTitle.toLowerCase()
225
+ );
226
+ return existingTopic || { id: -1, title: topicTitle };
227
+ });
228
+ initialDescription = sfContent.description || '';
229
+ setDraftTopics(initialTopics);
230
+ setDraftDetails(initialDescription);
231
+ } else {
232
+ setDraftTopics([]);
233
+ setDraftDetails('');
234
+ }
235
+ }
236
+
237
+ if (initialState.current) {
238
+ initialState.current.details = initialDescription;
239
+ initialState.current.topics = cloneDeep(initialTopics);
240
+ }
241
+
242
+ setDataFetched(true);
243
+ initialized.current = true;
244
+ } catch (err) {
245
+ setError(err instanceof Error ? err.message : 'Failed to load data');
246
+ } finally {
247
+ setLoading(false);
248
+ }
249
+ }, [nodeId, storedData, $contentMap, dataFetched]);
250
+
251
+ // Detect changes
252
+ useEffect(() => {
253
+ if (!loading && dataFetched && initialState.current) {
254
+ const initial = initialState.current;
255
+ const pendingOp = getPendingImageOperation(nodeId);
256
+
257
+ const topicsChanged =
258
+ draftTopics.length !== initial.topics.length ||
259
+ draftTopics.some(
260
+ (t, i) =>
261
+ t.title.toLowerCase() !== initial.topics[i]?.title.toLowerCase() ||
262
+ t.id !== initial.topics[i]?.id
263
+ );
264
+
265
+ const imageChanged =
266
+ pendingOp !== null || draftImagePath !== initial.socialImagePath;
267
+
268
+ const hasChangesDetected =
269
+ draftTitle !== initial.title ||
270
+ draftDetails !== initial.details ||
271
+ topicsChanged ||
272
+ imageChanged;
273
+
274
+ setHasChanges(hasChangesDetected);
275
+ }
276
+ }, [
277
+ draftTopics,
278
+ draftDetails,
279
+ draftTitle,
280
+ draftImagePath,
281
+ nodeId,
282
+ loading,
283
+ dataFetched,
284
+ ]);
285
+
286
+ const handleUploadClick = () => {
287
+ fileInputRef.current?.click();
288
+ };
289
+
290
+ const handleRemoveImage = () => {
291
+ setPendingImageOperation(nodeId, {
292
+ type: 'remove',
293
+ path: draftImagePath || undefined,
294
+ });
295
+ setDraftImagePath(null);
296
+ setDraftImageData(null);
297
+ setImageError(null);
298
+ };
299
+
300
+ const handleFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
301
+ const file = event.target.files?.[0];
302
+ if (!file) return;
303
+
304
+ setIsProcessing(true);
305
+ setImageError(null);
306
+
307
+ try {
308
+ // Basic validation first (format, size)
309
+ const validationOptions: FileValidationOptions = {
310
+ maxSizeKB: 2048,
311
+ allowedFormats: ['jpg', 'jpeg', 'png'],
312
+ allowAnyImageWithWarning: true,
313
+ };
314
+
315
+ const validation = await validateFile(file, validationOptions);
316
+ if (!validation.isValid) {
317
+ setImageError(validation.error || 'Invalid file');
318
+ setIsProcessing(false);
319
+ return;
320
+ }
321
+
322
+ // Check dimensions
323
+ const dimensions = await getImageDimensions(file);
324
+ const needsResize =
325
+ dimensions.width !== TARGET_WIDTH ||
326
+ dimensions.height !== TARGET_HEIGHT;
327
+
328
+ if (needsResize) {
329
+ // Auto-resize
330
+ const targetDimensions: ImageDimensions = {
331
+ width: TARGET_WIDTH,
332
+ height: TARGET_HEIGHT,
333
+ };
334
+
335
+ const processResult = await resizeAndCropImage(
336
+ file,
337
+ targetDimensions,
338
+ 0.9
339
+ );
340
+
341
+ const fileExtension =
342
+ file.name.split('.').pop()?.toLowerCase() || 'jpg';
343
+ const filename = `${nodeId}-${Date.now()}.${fileExtension}`;
344
+
345
+ setPendingImageOperation(nodeId, {
346
+ type: 'upload',
347
+ data: processResult.processedFile,
348
+ path: `/images/og/${filename}`,
349
+ filename,
350
+ });
351
+
352
+ setDraftImageData(processResult.processedFile);
353
+ setDraftImagePath(`/images/og/${filename}`);
354
+
355
+ // Show resize feedback
356
+ if (processResult.warnings.length > 0) {
357
+ setImageError(
358
+ `✓ Image automatically resized from ${processResult.originalDimensions.width}x${processResult.originalDimensions.height} to ${processResult.finalDimensions.width}x${processResult.finalDimensions.height} pixels. ${processResult.warnings.join(' ')}`
359
+ );
360
+ } else {
361
+ setImageError(
362
+ `✓ Image automatically resized from ${processResult.originalDimensions.width}x${processResult.originalDimensions.height} to ${processResult.finalDimensions.width}x${processResult.finalDimensions.height} pixels.`
363
+ );
364
+ }
365
+ } else {
366
+ // Perfect dimensions - use as-is
367
+ const reader = new FileReader();
368
+ const base64Promise = new Promise<string>((resolve) => {
369
+ reader.onload = (e) => resolve(e.target?.result as string);
370
+ });
371
+ reader.readAsDataURL(file);
372
+ const base64 = await base64Promise;
373
+
374
+ const fileExtension =
375
+ file.name.split('.').pop()?.toLowerCase() || 'jpg';
376
+ const filename = `${nodeId}-${Date.now()}.${fileExtension}`;
377
+
378
+ setPendingImageOperation(nodeId, {
379
+ type: 'upload',
380
+ data: base64,
381
+ path: `/images/og/${filename}`,
382
+ filename,
383
+ });
384
+
385
+ setDraftImageData(base64);
386
+ setDraftImagePath(`/images/og/${filename}`);
387
+
388
+ setImageError(
389
+ `✓ Perfect dimensions! Image uploaded at ${dimensions.width}x${dimensions.height} pixels.`
390
+ );
391
+ }
392
+ } catch (err) {
393
+ setImageError('Failed to process image');
394
+ console.error('Error processing image:', err);
395
+ } finally {
396
+ setIsProcessing(false);
397
+ }
398
+ };
399
+
400
+ const addTopic = (titleToAdd: string, sourceTopic?: Topic) => {
401
+ if (!titleToAdd) return;
402
+
403
+ const titleLower = titleToAdd.toLowerCase().trim();
404
+
405
+ if (draftTopics.some((topic) => topic.title.toLowerCase() === titleLower))
406
+ return;
407
+
408
+ const existingTopicsArray = Array.isArray(existingTopics)
409
+ ? existingTopics
410
+ : [];
411
+ const matchingTopic =
412
+ sourceTopic ||
413
+ existingTopicsArray.find(
414
+ (topic) => topic.title.toLowerCase() === titleLower
415
+ );
416
+
417
+ if (matchingTopic) {
418
+ setDraftTopics([
419
+ ...draftTopics,
420
+ { id: matchingTopic.id, title: matchingTopic.title },
421
+ ]);
422
+ } else {
423
+ setDraftTopics([...draftTopics, { id: -1, title: titleToAdd.trim() }]);
424
+ setExistingTopics([
425
+ ...existingTopicsArray,
426
+ { id: -1, title: titleToAdd.trim() },
427
+ ]);
428
+ }
429
+ };
430
+
431
+ const handleAddTopic: MouseEventHandler<HTMLButtonElement> = () => {
432
+ const titleToAdd = newTopicTitle.trim();
433
+ addTopic(titleToAdd);
434
+ setNewTopicTitle('');
435
+ };
436
+
437
+ const handleRemoveTopic = (topicToRemove: Topic) => {
438
+ setDraftTopics((prevTopics) =>
439
+ prevTopics.filter(
440
+ (topic) =>
441
+ topic.title.toLowerCase() !== topicToRemove.title.toLowerCase()
442
+ )
443
+ );
444
+ };
445
+
446
+ const handleDescriptionChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
447
+ setDraftDetails(e.target.value);
448
+ };
449
+
450
+ const handleApplyChanges = () => {
451
+ let currentNode = storyfragmentNode; // Track the current node state
452
+
453
+ // Update title if changed
454
+ if (draftTitle !== storyfragmentNode.title) {
455
+ const existingSlugs = $contentMap
456
+ .filter(hasSlug)
457
+ .map((item) => item.slug);
458
+ const newSlug =
459
+ storyfragmentNode.slug === ''
460
+ ? findUniqueSlug(titleToSlug(draftTitle), existingSlugs)
461
+ : null;
462
+
463
+ const updatedNode = cloneDeep({
464
+ ...storyfragmentNode,
465
+ title: draftTitle,
466
+ ...(newSlug ? { slug: newSlug } : {}),
467
+ isChanged: true,
468
+ });
469
+ ctx.modifyNodes([updatedNode]);
470
+
471
+ // Update our reference to the current node
472
+ currentNode = updatedNode;
473
+ }
474
+
475
+ // Save topics and description to store
476
+ const topicsArray = Array.isArray(draftTopics) ? draftTopics : [];
477
+ const storeTopics = topicsArray.map((topic) => ({
478
+ id:
479
+ topic.id !== undefined && topic.id !== -1
480
+ ? String(topic.id)
481
+ : undefined,
482
+ title: topic.title,
483
+ }));
484
+
485
+ storyFragmentTopicsStore.setKey(nodeId, {
486
+ topics: storeTopics,
487
+ description: draftDetails,
488
+ });
489
+
490
+ // FORCE node update to trigger undo history for ANY changes
491
+ // Use the CURRENT node state, not the original storyfragmentNode
492
+ if (hasChanges) {
493
+ const updatedNode = cloneDeep({
494
+ ...currentNode,
495
+ changed: new Date(),
496
+ isChanged: true,
497
+ });
498
+ ctx.modifyNodes([updatedNode]);
499
+ }
500
+
501
+ setMode(StoryFragmentMode.DEFAULT);
502
+ };
503
+
504
+ return (
505
+ <div className="group mb-4 w-full rounded-b-md bg-white px-1.5 py-6">
506
+ <div className="px-3.5">
507
+ <div className="mb-4 flex justify-between">
508
+ <h3 className="text-lg font-bold">Page SEO</h3>
509
+ <button
510
+ onClick={() => setMode(StoryFragmentMode.DEFAULT)}
511
+ className="text-cyan-600 hover:text-black"
512
+ >
513
+ ← Close Panel
514
+ </button>
515
+ </div>
516
+
517
+ {hasChanges && (
518
+ <div className="mt-6 flex justify-end">
519
+ <button
520
+ onClick={handleApplyChanges}
521
+ className="rounded-md bg-cyan-600 px-4 py-2 text-white hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-cyan-500"
522
+ >
523
+ Apply Changes
524
+ </button>
525
+ </div>
526
+ )}
527
+
528
+ {error && (
529
+ <div className="mb-4 rounded-md bg-red-100 p-3 text-red-700">
530
+ {error}
531
+ </div>
532
+ )}
533
+
534
+ <div className="space-y-6">
535
+ <div>
536
+ <h3 className="mb-1 block text-sm font-bold text-gray-700">
537
+ Page Title
538
+ </h3>
539
+ <div className="relative">
540
+ <input
541
+ type="text"
542
+ value={draftTitle}
543
+ onChange={handleTitleChange}
544
+ className={`w-full rounded-md border px-2 py-1 pr-16 ${
545
+ charCount < 5
546
+ ? 'border-red-500 bg-red-50'
547
+ : isValid
548
+ ? 'border-green-500 bg-green-50'
549
+ : warning
550
+ ? 'border-yellow-500 bg-yellow-50'
551
+ : 'border-gray-300'
552
+ }`}
553
+ placeholder="Enter story fragment title (50-60 characters recommended)"
554
+ />
555
+ <div className="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-2">
556
+ {charCount < 5 ? (
557
+ <ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
558
+ ) : isValid ? (
559
+ <CheckIcon className="h-5 w-5 text-green-500" />
560
+ ) : warning ? (
561
+ <ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
562
+ ) : null}
563
+ <span
564
+ className={`text-sm ${
565
+ charCount < 5
566
+ ? 'text-red-500'
567
+ : isValid
568
+ ? 'text-green-500'
569
+ : warning
570
+ ? 'text-yellow-500'
571
+ : 'text-gray-500'
572
+ }`}
573
+ >
574
+ {charCount}/70
575
+ </span>
576
+ </div>
577
+ </div>
578
+ <div className="mt-2 space-y-2 text-sm text-gray-600">
579
+ <p>
580
+ Write a clear, descriptive title that accurately represents your
581
+ page content.
582
+ </p>
583
+ <ul className="ml-4 space-y-1">
584
+ <li>
585
+ <CheckIcon className="mr-1 inline h-4 w-4" /> Include relevant
586
+ keywords
587
+ </li>
588
+ <li>
589
+ <CheckIcon className="mr-1 inline h-4 w-4" /> Avoid
590
+ unnecessary words like "welcome to" or "the"
591
+ </li>
592
+ <li>
593
+ <CheckIcon className="mr-1 inline h-4 w-4" /> Unique titles
594
+ across your website
595
+ </li>
596
+ </ul>
597
+ <div className="py-2">
598
+ {charCount < 5 && (
599
+ <span className="text-red-500">
600
+ Title must be at least 5 characters
601
+ </span>
602
+ )}
603
+ {charCount >= 5 && charCount < 35 && (
604
+ <span className="text-gray-500">
605
+ Add {35 - charCount} more characters for optimal length
606
+ </span>
607
+ )}
608
+ {warning && (
609
+ <span className="text-yellow-500">Title is getting long</span>
610
+ )}
611
+ {isValid && (
612
+ <span className="text-green-500">Perfect title length!</span>
613
+ )}
614
+ </div>
615
+ </div>
616
+ </div>
617
+
618
+ <div>
619
+ <label
620
+ htmlFor="description"
621
+ className="mb-1 block text-sm font-bold text-gray-700"
622
+ >
623
+ Page Description
624
+ </label>
625
+ <textarea
626
+ id="description"
627
+ rows={3}
628
+ className="w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-cyan-600 focus:ring-cyan-600"
629
+ placeholder="Add a description for this page..."
630
+ value={draftDetails}
631
+ onChange={handleDescriptionChange}
632
+ />
633
+ <p className="mt-1 text-sm text-gray-500">
634
+ This description helps with SEO and may appear in search results.
635
+ </p>
636
+ </div>
637
+
638
+ <div>
639
+ <h3 className="mb-2 block text-sm font-bold text-gray-700">
640
+ Social Share Image
641
+ </h3>
642
+
643
+ {draftImageData || draftImagePath ? (
644
+ <div className="flex items-start space-x-4">
645
+ <div
646
+ className="relative w-64 overflow-hidden rounded-md bg-gray-100"
647
+ style={{ aspectRatio: 1.91 / 1 }}
648
+ >
649
+ <img
650
+ src={draftImageData || `${draftImagePath}?v=${Date.now()}`}
651
+ alt="Open Graph preview"
652
+ className="h-full w-full object-cover"
653
+ />
654
+ <button
655
+ onClick={handleRemoveImage}
656
+ disabled={isProcessing}
657
+ title="Remove image"
658
+ className="absolute right-2 top-2 rounded-full bg-white p-1 shadow-md hover:bg-gray-200 disabled:opacity-50"
659
+ >
660
+ <XMarkIcon className="h-4 w-4 text-gray-800" />
661
+ </button>
662
+ </div>
663
+
664
+ <div className="flex-grow">
665
+ <button
666
+ onClick={handleUploadClick}
667
+ disabled={isProcessing}
668
+ title="Replace image"
669
+ className="flex items-center text-sm text-cyan-600 hover:text-orange-600 disabled:opacity-50"
670
+ >
671
+ <ArrowUpTrayIcon className="mr-1 h-4 w-4" />
672
+ {isProcessing ? 'Processing...' : 'Replace Image'}
673
+ </button>
674
+
675
+ <input
676
+ type="file"
677
+ ref={fileInputRef}
678
+ onChange={handleFileChange}
679
+ accept="image/jpeg,image/png"
680
+ className="hidden"
681
+ />
682
+
683
+ <p className="mt-2 break-all text-xs text-gray-800">
684
+ {draftImageData
685
+ ? 'New image (pending save)'
686
+ : draftImagePath}
687
+ </p>
688
+ {imageError && (
689
+ <p
690
+ className={`mt-2 text-sm ${imageError.startsWith('✓') ? 'text-green-600' : 'text-red-600'}`}
691
+ >
692
+ {imageError}
693
+ </p>
694
+ )}
695
+ </div>
696
+ </div>
697
+ ) : (
698
+ <>
699
+ {config && (
700
+ <OgImagePreview
701
+ nodeId={nodeId}
702
+ title={draftTitle}
703
+ socialImagePath={draftImagePath}
704
+ config={config}
705
+ onColorChange={handleColorChange}
706
+ />
707
+ )}
708
+ <div className="mt-4 flex space-x-4">
709
+ <div
710
+ className="relative w-64 overflow-hidden rounded-md bg-gray-100"
711
+ style={{ aspectRatio: 1.91 / 1 }}
712
+ >
713
+ <div className="flex h-full w-full items-center justify-center rounded-md border-2 border-dashed border-gray-400">
714
+ <span className="text-sm text-gray-600">
715
+ No image selected
716
+ </span>
717
+ </div>
718
+ </div>
719
+
720
+ <div className="flex-grow">
721
+ <button
722
+ onClick={handleUploadClick}
723
+ disabled={isProcessing}
724
+ title="Upload image"
725
+ className="flex items-center text-sm text-cyan-600 hover:text-orange-600 disabled:opacity-50"
726
+ >
727
+ <ArrowUpTrayIcon className="mr-1 h-4 w-4" />
728
+ {isProcessing ? 'Processing...' : 'Upload Image'}
729
+ </button>
730
+
731
+ <input
732
+ type="file"
733
+ ref={fileInputRef}
734
+ onChange={handleFileChange}
735
+ accept="image/jpeg,image/png"
736
+ className="hidden"
737
+ />
738
+
739
+ {imageError && (
740
+ <p
741
+ className={`mt-2 text-sm ${imageError.startsWith('✓') ? 'text-green-600' : 'text-red-600'}`}
742
+ >
743
+ {imageError}
744
+ </p>
745
+ )}
746
+ </div>
747
+ </div>
748
+
749
+ <div className="mt-2 space-y-2 text-sm text-gray-600">
750
+ <p>
751
+ This image will be used when your page is shared on social
752
+ media.
753
+ </p>
754
+ <p>Requirements:</p>
755
+ <ul className="ml-5 list-disc space-y-1">
756
+ <li>
757
+ <strong>Best results:</strong> Upload images at exactly{' '}
758
+ {TARGET_WIDTH}x{TARGET_HEIGHT} pixels
759
+ </li>
760
+ <li>
761
+ Other sizes will be automatically resized (may crop to
762
+ fit)
763
+ </li>
764
+ <li>Only JPG or PNG formats are accepted</li>
765
+ <li>Keep important content centered</li>
766
+ <li>Use clear, high-contrast imagery</li>
767
+ <li>Avoid small text</li>
768
+ </ul>
769
+ </div>
770
+ </>
771
+ )}
772
+ </div>
773
+
774
+ {draftDetails.trim().length > 0 && (
775
+ <div>
776
+ <label className="mb-2 block text-sm font-bold text-gray-700">
777
+ Topics
778
+ </label>
779
+
780
+ <div className="mb-3 flex space-x-1">
781
+ <input
782
+ type="text"
783
+ className="flex-grow rounded-l-md border border-gray-300 p-2 shadow-sm focus:border-cyan-600 focus:ring-cyan-600"
784
+ placeholder="Add a new tag..."
785
+ value={newTopicTitle}
786
+ onChange={(e) => setNewTopicTitle(e.target.value)}
787
+ onKeyDown={(e) => {
788
+ if (e.key === 'Enter') {
789
+ e.preventDefault();
790
+ addTopic(newTopicTitle.trim());
791
+ setNewTopicTitle('');
792
+ }
793
+ }}
794
+ />
795
+ <button
796
+ onClick={handleAddTopic}
797
+ disabled={!newTopicTitle.trim()}
798
+ className="rounded-r-md bg-cyan-600 px-3 py-2 text-white hover:bg-cyan-700 disabled:cursor-not-allowed disabled:bg-gray-300"
799
+ >
800
+ <PlusIcon className="h-5 w-5" />
801
+ </button>
802
+ </div>
803
+
804
+ <div className="flex flex-wrap gap-2">
805
+ {draftTopics.map((topic) => (
806
+ <div
807
+ key={`topic-${topic.title}`}
808
+ className="flex items-center rounded-full bg-gray-100 px-3 py-1"
809
+ >
810
+ <TagIcon className="mr-1 h-4 w-4 text-gray-500" />
811
+ <span className="text-sm">{topic.title}</span>
812
+ <button
813
+ onClick={() => handleRemoveTopic(topic)}
814
+ className="ml-1 text-gray-500 hover:text-gray-700"
815
+ >
816
+ <XMarkIcon className="h-4 w-4" />
817
+ </button>
818
+ </div>
819
+ ))}
820
+ {draftTopics.length === 0 && (
821
+ <p className="text-sm italic text-gray-500">
822
+ No topics added yet. Topics help organize and categorize
823
+ your content.
824
+ </p>
825
+ )}
826
+ </div>
827
+ <div className="mt-4">
828
+ <h4 className="mb-2 text-xs font-bold text-gray-700">
829
+ Available Tags
830
+ </h4>
831
+ <div className="flex flex-wrap gap-2">
832
+ {existingTopics
833
+ .filter(
834
+ (existingTopic) =>
835
+ !draftTopics.some(
836
+ (topic) =>
837
+ topic.title.toLowerCase() ===
838
+ existingTopic.title.toLowerCase()
839
+ )
840
+ )
841
+ .map((availableTopic) => (
842
+ <button
843
+ key={`available-${availableTopic.title}`}
844
+ onClick={() =>
845
+ addTopic(availableTopic.title, availableTopic)
846
+ }
847
+ className="flex items-center rounded-full border border-gray-200 bg-gray-50 px-3 py-1 transition-colors hover:bg-gray-100"
848
+ >
849
+ <TagIcon className="mr-1 h-3 w-3 text-gray-400" />
850
+ <span className="text-xs text-gray-600">
851
+ {availableTopic.title}
852
+ </span>
853
+ </button>
854
+ ))}
855
+ {existingTopics.filter(
856
+ (existingTopic) =>
857
+ !draftTopics.some(
858
+ (topic) =>
859
+ topic.title.toLowerCase() ===
860
+ existingTopic.title.toLowerCase()
861
+ )
862
+ ).length === 0 && (
863
+ <p className="text-xs italic text-gray-500">
864
+ No additional topics available.
865
+ </p>
866
+ )}
867
+ </div>
868
+ </div>
869
+ </div>
870
+ )}
871
+ </div>
872
+
873
+ {hasChanges && (
874
+ <div className="mt-6 flex justify-end">
875
+ <button
876
+ onClick={handleApplyChanges}
877
+ className="rounded-md bg-cyan-600 px-4 py-2 text-white hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-cyan-500"
878
+ >
879
+ Apply Changes
880
+ </button>
881
+ </div>
882
+ )}
883
+ </div>
884
+ </div>
885
+ );
886
+ };
887
+
888
+ export default StoryFragmentOpenGraphPanel;