@dxos/plugin-magazine 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (390) hide show
  1. package/LICENSE +105 -0
  2. package/PLUGIN.mdl +653 -0
  3. package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs +90 -0
  4. package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs.map +7 -0
  5. package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs +97 -0
  6. package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs.map +7 -0
  7. package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs +325 -0
  8. package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs.map +7 -0
  9. package/dist/lib/neutral/MagazinePlugin.mjs +62 -0
  10. package/dist/lib/neutral/MagazinePlugin.mjs.map +7 -0
  11. package/dist/lib/neutral/MagazinePlugin.workerd.mjs +12 -0
  12. package/dist/lib/neutral/MagazinePlugin.workerd.mjs.map +7 -0
  13. package/dist/lib/neutral/PostArticle-RERHXPXC.mjs +230 -0
  14. package/dist/lib/neutral/PostArticle-RERHXPXC.mjs.map +7 -0
  15. package/dist/lib/neutral/PostCard-VDGN7BBA.mjs +60 -0
  16. package/dist/lib/neutral/PostCard-VDGN7BBA.mjs.map +7 -0
  17. package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs +121 -0
  18. package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs.map +7 -0
  19. package/dist/lib/neutral/capabilities/index.mjs +21 -0
  20. package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
  21. package/dist/lib/neutral/chunk-3K4AQKBZ.mjs +12 -0
  22. package/dist/lib/neutral/chunk-3K4AQKBZ.mjs.map +7 -0
  23. package/dist/lib/neutral/chunk-AFPBWBTK.mjs +591 -0
  24. package/dist/lib/neutral/chunk-AFPBWBTK.mjs.map +7 -0
  25. package/dist/lib/neutral/chunk-APQRTYT3.mjs +57 -0
  26. package/dist/lib/neutral/chunk-APQRTYT3.mjs.map +7 -0
  27. package/dist/lib/neutral/chunk-GU725ZF4.mjs +11 -0
  28. package/dist/lib/neutral/chunk-GU725ZF4.mjs.map +7 -0
  29. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  30. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  31. package/dist/lib/neutral/chunk-JFPR6OV4.mjs +60 -0
  32. package/dist/lib/neutral/chunk-JFPR6OV4.mjs.map +7 -0
  33. package/dist/lib/neutral/chunk-KTYLUOUG.mjs +36 -0
  34. package/dist/lib/neutral/chunk-KTYLUOUG.mjs.map +7 -0
  35. package/dist/lib/neutral/chunk-MBPX7RPI.mjs +14 -0
  36. package/dist/lib/neutral/chunk-MBPX7RPI.mjs.map +7 -0
  37. package/dist/lib/neutral/chunk-MJ2AY2CK.mjs +8 -0
  38. package/dist/lib/neutral/chunk-MJ2AY2CK.mjs.map +7 -0
  39. package/dist/lib/neutral/chunk-OYCDRSWB.mjs +624 -0
  40. package/dist/lib/neutral/chunk-OYCDRSWB.mjs.map +7 -0
  41. package/dist/lib/neutral/chunk-SAWKLGJU.mjs +28 -0
  42. package/dist/lib/neutral/chunk-SAWKLGJU.mjs.map +7 -0
  43. package/dist/lib/neutral/chunk-XCNCK47N.mjs +15 -0
  44. package/dist/lib/neutral/chunk-XCNCK47N.mjs.map +7 -0
  45. package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs +59 -0
  46. package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs.map +7 -0
  47. package/dist/lib/neutral/components/index.mjs +338 -0
  48. package/dist/lib/neutral/components/index.mjs.map +7 -0
  49. package/dist/lib/neutral/containers/index.mjs +17 -0
  50. package/dist/lib/neutral/containers/index.mjs.map +7 -0
  51. package/dist/lib/neutral/create-object-SYZMRR4H.mjs +98 -0
  52. package/dist/lib/neutral/create-object-SYZMRR4H.mjs.map +7 -0
  53. package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs +184 -0
  54. package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs.map +7 -0
  55. package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs +31 -0
  56. package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs.map +7 -0
  57. package/dist/lib/neutral/index.mjs +18 -0
  58. package/dist/lib/neutral/index.mjs.map +7 -0
  59. package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs +65 -0
  60. package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs.map +7 -0
  61. package/dist/lib/neutral/meta.json +1 -0
  62. package/dist/lib/neutral/meta.mjs +8 -0
  63. package/dist/lib/neutral/meta.mjs.map +7 -0
  64. package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs +18 -0
  65. package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs.map +7 -0
  66. package/dist/lib/neutral/operation-handler-JE3743BU.mjs +8 -0
  67. package/dist/lib/neutral/operation-handler-JE3743BU.mjs.map +7 -0
  68. package/dist/lib/neutral/operations/index.mjs +8 -0
  69. package/dist/lib/neutral/operations/index.mjs.map +7 -0
  70. package/dist/lib/neutral/plugin.mjs +16 -0
  71. package/dist/lib/neutral/plugin.mjs.map +7 -0
  72. package/dist/lib/neutral/plugin.workerd.mjs +16 -0
  73. package/dist/lib/neutral/plugin.workerd.mjs.map +7 -0
  74. package/dist/lib/neutral/react-surface-TZESAMG2.mjs +60 -0
  75. package/dist/lib/neutral/react-surface-TZESAMG2.mjs.map +7 -0
  76. package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs +52 -0
  77. package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs.map +7 -0
  78. package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs +8 -0
  79. package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs.map +7 -0
  80. package/dist/lib/neutral/skills/index.mjs +46 -0
  81. package/dist/lib/neutral/skills/index.mjs.map +7 -0
  82. package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs +99 -0
  83. package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs.map +7 -0
  84. package/dist/lib/neutral/translations.mjs +81 -0
  85. package/dist/lib/neutral/translations.mjs.map +7 -0
  86. package/dist/lib/neutral/types/index.mjs +14 -0
  87. package/dist/lib/neutral/types/index.mjs.map +7 -0
  88. package/dist/types/dx.config.d.ts +28 -0
  89. package/dist/types/dx.config.d.ts.map +1 -0
  90. package/dist/types/src/MagazinePlugin.d.ts +4 -0
  91. package/dist/types/src/MagazinePlugin.d.ts.map +1 -0
  92. package/dist/types/src/MagazinePlugin.test.d.ts +2 -0
  93. package/dist/types/src/MagazinePlugin.test.d.ts.map +1 -0
  94. package/dist/types/src/MagazinePlugin.workerd.d.ts +4 -0
  95. package/dist/types/src/MagazinePlugin.workerd.d.ts.map +1 -0
  96. package/dist/types/src/atoms/atoms.test.d.ts +2 -0
  97. package/dist/types/src/atoms/atoms.test.d.ts.map +1 -0
  98. package/dist/types/src/atoms/index.d.ts +7 -0
  99. package/dist/types/src/atoms/index.d.ts.map +1 -0
  100. package/dist/types/src/atoms/magazine-posts.d.ts +16 -0
  101. package/dist/types/src/atoms/magazine-posts.d.ts.map +1 -0
  102. package/dist/types/src/atoms/post-content.d.ts +7 -0
  103. package/dist/types/src/atoms/post-content.d.ts.map +1 -0
  104. package/dist/types/src/atoms/post-curation.d.ts +16 -0
  105. package/dist/types/src/atoms/post-curation.d.ts.map +1 -0
  106. package/dist/types/src/atoms/post-display.d.ts +22 -0
  107. package/dist/types/src/atoms/post-display.d.ts.map +1 -0
  108. package/dist/types/src/atoms/post-read.d.ts +14 -0
  109. package/dist/types/src/atoms/post-read.d.ts.map +1 -0
  110. package/dist/types/src/atoms/post-tags.d.ts +16 -0
  111. package/dist/types/src/atoms/post-tags.d.ts.map +1 -0
  112. package/dist/types/src/capabilities/app-graph-builder.d.ts +5 -0
  113. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  114. package/dist/types/src/capabilities/create-object.d.ts +5 -0
  115. package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
  116. package/dist/types/src/capabilities/index.d.ts +10 -0
  117. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  118. package/dist/types/src/capabilities/navigation-resolver.d.ts +5 -0
  119. package/dist/types/src/capabilities/navigation-resolver.d.ts.map +1 -0
  120. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  121. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  122. package/dist/types/src/capabilities/react-surface.d.ts +5 -0
  123. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  124. package/dist/types/src/capabilities/routine-templates.d.ts +5 -0
  125. package/dist/types/src/capabilities/routine-templates.d.ts.map +1 -0
  126. package/dist/types/src/capabilities/skill-definition.d.ts +5 -0
  127. package/dist/types/src/capabilities/skill-definition.d.ts.map +1 -0
  128. package/dist/types/src/components/PostContent/PostContent.d.ts +30 -0
  129. package/dist/types/src/components/PostContent/PostContent.d.ts.map +1 -0
  130. package/dist/types/src/components/PostContent/PostContent.test.d.ts +2 -0
  131. package/dist/types/src/components/PostContent/PostContent.test.d.ts.map +1 -0
  132. package/dist/types/src/components/PostContent/index.d.ts +2 -0
  133. package/dist/types/src/components/PostContent/index.d.ts.map +1 -0
  134. package/dist/types/src/components/PostStack/PostStack.d.ts +17 -0
  135. package/dist/types/src/components/PostStack/PostStack.d.ts.map +1 -0
  136. package/dist/types/src/components/PostStack/PostStack.stories.d.ts +10 -0
  137. package/dist/types/src/components/PostStack/PostStack.stories.d.ts.map +1 -0
  138. package/dist/types/src/components/PostStack/index.d.ts +2 -0
  139. package/dist/types/src/components/PostStack/index.d.ts.map +1 -0
  140. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts +23 -0
  141. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts.map +1 -0
  142. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts +8 -0
  143. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts.map +1 -0
  144. package/dist/types/src/components/SubscriptionStack/index.d.ts +2 -0
  145. package/dist/types/src/components/SubscriptionStack/index.d.ts.map +1 -0
  146. package/dist/types/src/components/index.d.ts +4 -0
  147. package/dist/types/src/components/index.d.ts.map +1 -0
  148. package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts +6 -0
  149. package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts.map +1 -0
  150. package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts +12 -0
  151. package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts.map +1 -0
  152. package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts +10 -0
  153. package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts.map +1 -0
  154. package/dist/types/src/containers/FeedArticle/index.d.ts +2 -0
  155. package/dist/types/src/containers/FeedArticle/index.d.ts.map +1 -0
  156. package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts +6 -0
  157. package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts.map +1 -0
  158. package/dist/types/src/containers/FeedProperties/index.d.ts +2 -0
  159. package/dist/types/src/containers/FeedProperties/index.d.ts.map +1 -0
  160. package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts +6 -0
  161. package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts.map +1 -0
  162. package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts +9 -0
  163. package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts.map +1 -0
  164. package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts +11 -0
  165. package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts.map +1 -0
  166. package/dist/types/src/containers/MagazineArticle/index.d.ts +2 -0
  167. package/dist/types/src/containers/MagazineArticle/index.d.ts.map +1 -0
  168. package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts +18 -0
  169. package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts.map +1 -0
  170. package/dist/types/src/containers/PostArticle/PostArticle.d.ts +6 -0
  171. package/dist/types/src/containers/PostArticle/PostArticle.d.ts.map +1 -0
  172. package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts +8 -0
  173. package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts.map +1 -0
  174. package/dist/types/src/containers/PostArticle/PostToolbar.d.ts +19 -0
  175. package/dist/types/src/containers/PostArticle/PostToolbar.d.ts.map +1 -0
  176. package/dist/types/src/containers/PostArticle/index.d.ts +2 -0
  177. package/dist/types/src/containers/PostArticle/index.d.ts.map +1 -0
  178. package/dist/types/src/containers/PostCard/PostCard.d.ts +11 -0
  179. package/dist/types/src/containers/PostCard/PostCard.d.ts.map +1 -0
  180. package/dist/types/src/containers/PostCard/index.d.ts +2 -0
  181. package/dist/types/src/containers/PostCard/index.d.ts.map +1 -0
  182. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts +5 -0
  183. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts.map +1 -0
  184. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts +8 -0
  185. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts.map +1 -0
  186. package/dist/types/src/containers/SubscriptionsArticle/index.d.ts +2 -0
  187. package/dist/types/src/containers/SubscriptionsArticle/index.d.ts.map +1 -0
  188. package/dist/types/src/containers/index.d.ts +7 -0
  189. package/dist/types/src/containers/index.d.ts.map +1 -0
  190. package/dist/types/src/index.d.ts +3 -0
  191. package/dist/types/src/index.d.ts.map +1 -0
  192. package/dist/types/src/meta.d.ts +33 -0
  193. package/dist/types/src/meta.d.ts.map +1 -0
  194. package/dist/types/src/operations/clear-magazine.d.ts +10 -0
  195. package/dist/types/src/operations/clear-magazine.d.ts.map +1 -0
  196. package/dist/types/src/operations/curate-magazine.d.ts +25 -0
  197. package/dist/types/src/operations/curate-magazine.d.ts.map +1 -0
  198. package/dist/types/src/operations/curate-magazine.skill.test.d.ts +2 -0
  199. package/dist/types/src/operations/curate-magazine.skill.test.d.ts.map +1 -0
  200. package/dist/types/src/operations/curate-magazine.test.d.ts +2 -0
  201. package/dist/types/src/operations/curate-magazine.test.d.ts.map +1 -0
  202. package/dist/types/src/operations/extraction/article.d.ts +37 -0
  203. package/dist/types/src/operations/extraction/article.d.ts.map +1 -0
  204. package/dist/types/src/operations/extraction/article.test.d.ts +2 -0
  205. package/dist/types/src/operations/extraction/article.test.d.ts.map +1 -0
  206. package/dist/types/src/operations/extraction/index.d.ts +2 -0
  207. package/dist/types/src/operations/extraction/index.d.ts.map +1 -0
  208. package/dist/types/src/operations/fetch-article-content.d.ts +5 -0
  209. package/dist/types/src/operations/fetch-article-content.d.ts.map +1 -0
  210. package/dist/types/src/operations/index.d.ts +3 -0
  211. package/dist/types/src/operations/index.d.ts.map +1 -0
  212. package/dist/types/src/operations/load-post-content.d.ts +8 -0
  213. package/dist/types/src/operations/load-post-content.d.ts.map +1 -0
  214. package/dist/types/src/operations/sources/article.d.ts +24 -0
  215. package/dist/types/src/operations/sources/article.d.ts.map +1 -0
  216. package/dist/types/src/operations/sources/cors.d.ts +12 -0
  217. package/dist/types/src/operations/sources/cors.d.ts.map +1 -0
  218. package/dist/types/src/operations/sources/feed-fetcher.d.ts +22 -0
  219. package/dist/types/src/operations/sources/feed-fetcher.d.ts.map +1 -0
  220. package/dist/types/src/operations/sources/feed-fetcher.test.d.ts +2 -0
  221. package/dist/types/src/operations/sources/feed-fetcher.test.d.ts.map +1 -0
  222. package/dist/types/src/operations/sources/http.d.ts +9 -0
  223. package/dist/types/src/operations/sources/http.d.ts.map +1 -0
  224. package/dist/types/src/operations/sources/index.d.ts +6 -0
  225. package/dist/types/src/operations/sources/index.d.ts.map +1 -0
  226. package/dist/types/src/operations/sources/rss.d.ts +4 -0
  227. package/dist/types/src/operations/sources/rss.d.ts.map +1 -0
  228. package/dist/types/src/operations/sources/rss.test.d.ts +2 -0
  229. package/dist/types/src/operations/sources/rss.test.d.ts.map +1 -0
  230. package/dist/types/src/operations/sources/standard-site.d.ts +33 -0
  231. package/dist/types/src/operations/sources/standard-site.d.ts.map +1 -0
  232. package/dist/types/src/operations/sync-feed.d.ts +5 -0
  233. package/dist/types/src/operations/sync-feed.d.ts.map +1 -0
  234. package/dist/types/src/operations/sync-feed.test.d.ts +2 -0
  235. package/dist/types/src/operations/sync-feed.test.d.ts.map +1 -0
  236. package/dist/types/src/operations/util.d.ts +25 -0
  237. package/dist/types/src/operations/util.d.ts.map +1 -0
  238. package/dist/types/src/operations/util.test.d.ts +2 -0
  239. package/dist/types/src/operations/util.test.d.ts.map +1 -0
  240. package/dist/types/src/paths.d.ts +3 -0
  241. package/dist/types/src/paths.d.ts.map +1 -0
  242. package/dist/types/src/plugin.d.ts +4 -0
  243. package/dist/types/src/plugin.d.ts.map +1 -0
  244. package/dist/types/src/plugin.workerd.d.ts +3 -0
  245. package/dist/types/src/plugin.workerd.d.ts.map +1 -0
  246. package/dist/types/src/skills/index.d.ts +2 -0
  247. package/dist/types/src/skills/index.d.ts.map +1 -0
  248. package/dist/types/src/skills/magazine-skill.d.ts +4 -0
  249. package/dist/types/src/skills/magazine-skill.d.ts.map +1 -0
  250. package/dist/types/src/stories/ArticleExtractor.stories.d.ts +267 -0
  251. package/dist/types/src/stories/ArticleExtractor.stories.d.ts.map +1 -0
  252. package/dist/types/src/stories/MagazineCurate.stories.d.ts +32 -0
  253. package/dist/types/src/stories/MagazineCurate.stories.d.ts.map +1 -0
  254. package/dist/types/src/stories/theregister-fixture.test.d.ts +2 -0
  255. package/dist/types/src/stories/theregister-fixture.test.d.ts.map +1 -0
  256. package/dist/types/src/templates/magazine-curation.d.ts +7 -0
  257. package/dist/types/src/templates/magazine-curation.d.ts.map +1 -0
  258. package/dist/types/src/testing/builder.d.ts +28 -0
  259. package/dist/types/src/testing/builder.d.ts.map +1 -0
  260. package/dist/types/src/testing/index.d.ts +2 -0
  261. package/dist/types/src/testing/index.d.ts.map +1 -0
  262. package/dist/types/src/translations.d.ts +247 -0
  263. package/dist/types/src/translations.d.ts.map +1 -0
  264. package/dist/types/src/types/CreateSubscription.d.ts +29 -0
  265. package/dist/types/src/types/CreateSubscription.d.ts.map +1 -0
  266. package/dist/types/src/types/FeedOperation.d.ts +52 -0
  267. package/dist/types/src/types/FeedOperation.d.ts.map +1 -0
  268. package/dist/types/src/types/Magazine.d.ts +87 -0
  269. package/dist/types/src/types/Magazine.d.ts.map +1 -0
  270. package/dist/types/src/types/Magazine.test.d.ts +2 -0
  271. package/dist/types/src/types/Magazine.test.d.ts.map +1 -0
  272. package/dist/types/src/types/Subscription.d.ts +144 -0
  273. package/dist/types/src/types/Subscription.d.ts.map +1 -0
  274. package/dist/types/src/types/Subscription.test.d.ts +2 -0
  275. package/dist/types/src/types/Subscription.test.d.ts.map +1 -0
  276. package/dist/types/src/types/index.d.ts +5 -0
  277. package/dist/types/src/types/index.d.ts.map +1 -0
  278. package/dist/types/src/util/date.d.ts +8 -0
  279. package/dist/types/src/util/date.d.ts.map +1 -0
  280. package/dist/types/src/util/index.d.ts +4 -0
  281. package/dist/types/src/util/index.d.ts.map +1 -0
  282. package/dist/types/src/util/post-content.d.ts +14 -0
  283. package/dist/types/src/util/post-content.d.ts.map +1 -0
  284. package/dist/types/src/util/text.d.ts +18 -0
  285. package/dist/types/src/util/text.d.ts.map +1 -0
  286. package/dist/types/tsconfig.tsbuildinfo +1 -0
  287. package/dx.config.ts +48 -0
  288. package/package.json +164 -0
  289. package/src/MagazinePlugin.test.ts +36 -0
  290. package/src/MagazinePlugin.tsx +61 -0
  291. package/src/MagazinePlugin.workerd.ts +31 -0
  292. package/src/atoms/atoms.test.ts +323 -0
  293. package/src/atoms/index.ts +10 -0
  294. package/src/atoms/magazine-posts.ts +70 -0
  295. package/src/atoms/post-content.ts +34 -0
  296. package/src/atoms/post-curation.ts +36 -0
  297. package/src/atoms/post-display.ts +59 -0
  298. package/src/atoms/post-read.ts +40 -0
  299. package/src/atoms/post-tags.ts +66 -0
  300. package/src/capabilities/app-graph-builder.ts +125 -0
  301. package/src/capabilities/create-object.ts +127 -0
  302. package/src/capabilities/index.ts +17 -0
  303. package/src/capabilities/navigation-resolver.ts +21 -0
  304. package/src/capabilities/operation-handler.ts +16 -0
  305. package/src/capabilities/react-surface.tsx +53 -0
  306. package/src/capabilities/routine-templates.ts +16 -0
  307. package/src/capabilities/skill-definition.ts +14 -0
  308. package/src/components/PostContent/PostContent.test.ts +81 -0
  309. package/src/components/PostContent/PostContent.tsx +111 -0
  310. package/src/components/PostContent/index.ts +5 -0
  311. package/src/components/PostStack/PostStack.stories.tsx +39 -0
  312. package/src/components/PostStack/PostStack.tsx +151 -0
  313. package/src/components/PostStack/index.ts +5 -0
  314. package/src/components/SubscriptionStack/SubscriptionStack.stories.tsx +52 -0
  315. package/src/components/SubscriptionStack/SubscriptionStack.tsx +161 -0
  316. package/src/components/SubscriptionStack/index.ts +5 -0
  317. package/src/components/index.ts +7 -0
  318. package/src/containers/FeedArticle/FeedArticle.stories.tsx +102 -0
  319. package/src/containers/FeedArticle/FeedArticle.tsx +68 -0
  320. package/src/containers/FeedArticle/FeedToolbar.tsx +47 -0
  321. package/src/containers/FeedArticle/index.ts +5 -0
  322. package/src/containers/FeedProperties/FeedProperties.tsx +107 -0
  323. package/src/containers/FeedProperties/index.ts +5 -0
  324. package/src/containers/MagazineArticle/MagazineArticle.stories.tsx +244 -0
  325. package/src/containers/MagazineArticle/MagazineArticle.tsx +146 -0
  326. package/src/containers/MagazineArticle/MagazineTile.tsx +88 -0
  327. package/src/containers/MagazineArticle/index.ts +5 -0
  328. package/src/containers/MagazineArticle/useToolbar.tsx +145 -0
  329. package/src/containers/PostArticle/PostArticle.stories.tsx +117 -0
  330. package/src/containers/PostArticle/PostArticle.tsx +140 -0
  331. package/src/containers/PostArticle/PostToolbar.tsx +120 -0
  332. package/src/containers/PostArticle/index.ts +5 -0
  333. package/src/containers/PostCard/PostCard.tsx +70 -0
  334. package/src/containers/PostCard/index.ts +5 -0
  335. package/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.tsx +80 -0
  336. package/src/containers/SubscriptionsArticle/SubscriptionsArticle.tsx +99 -0
  337. package/src/containers/SubscriptionsArticle/index.ts +5 -0
  338. package/src/containers/index.ts +11 -0
  339. package/src/index.ts +6 -0
  340. package/src/meta.ts +9 -0
  341. package/src/operations/clear-magazine.ts +83 -0
  342. package/src/operations/curate-magazine.skill.conversations.json +1 -0
  343. package/src/operations/curate-magazine.skill.test.ts +167 -0
  344. package/src/operations/curate-magazine.test.ts +91 -0
  345. package/src/operations/curate-magazine.ts +178 -0
  346. package/src/operations/extraction/article.test.ts +154 -0
  347. package/src/operations/extraction/article.ts +270 -0
  348. package/src/operations/extraction/index.ts +5 -0
  349. package/src/operations/fetch-article-content.ts +27 -0
  350. package/src/operations/index.ts +13 -0
  351. package/src/operations/load-post-content.ts +76 -0
  352. package/src/operations/sources/article.ts +122 -0
  353. package/src/operations/sources/cors.ts +19 -0
  354. package/src/operations/sources/feed-fetcher.test.ts +200 -0
  355. package/src/operations/sources/feed-fetcher.ts +26 -0
  356. package/src/operations/sources/http.ts +42 -0
  357. package/src/operations/sources/index.ts +9 -0
  358. package/src/operations/sources/rss.test.ts +201 -0
  359. package/src/operations/sources/rss.ts +138 -0
  360. package/src/operations/sources/standard-site.ts +381 -0
  361. package/src/operations/sources/testing/feed.xml +477 -0
  362. package/src/operations/sync-feed.test.ts +146 -0
  363. package/src/operations/sync-feed.ts +140 -0
  364. package/src/operations/util.test.ts +67 -0
  365. package/src/operations/util.ts +65 -0
  366. package/src/paths.ts +14 -0
  367. package/src/plugin.ts +11 -0
  368. package/src/plugin.workerd.ts +6 -0
  369. package/src/skills/index.ts +5 -0
  370. package/src/skills/magazine-skill.ts +45 -0
  371. package/src/stories/ArticleExtractor.stories.tsx +204 -0
  372. package/src/stories/MagazineCurate.stories.tsx +217 -0
  373. package/src/stories/fixtures/theregister-ai.xml +45 -0
  374. package/src/stories/theregister-fixture.test.ts +35 -0
  375. package/src/templates/magazine-curation.ts +47 -0
  376. package/src/testing/builder.ts +77 -0
  377. package/src/testing/index.ts +5 -0
  378. package/src/translations.ts +81 -0
  379. package/src/types/CreateSubscription.ts +61 -0
  380. package/src/types/FeedOperation.ts +134 -0
  381. package/src/types/Magazine.test.ts +69 -0
  382. package/src/types/Magazine.ts +214 -0
  383. package/src/types/Subscription.test.ts +171 -0
  384. package/src/types/Subscription.ts +366 -0
  385. package/src/types/index.ts +8 -0
  386. package/src/util/date.ts +27 -0
  387. package/src/util/index.ts +7 -0
  388. package/src/util/post-content.ts +19 -0
  389. package/src/util/text.ts +98 -0
  390. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1,102 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React, { useEffect, useState } from 'react';
7
+
8
+ import { EffectEx } from '@dxos/effect';
9
+ import { Panel, Toolbar } from '@dxos/react-ui';
10
+ import { withAttention } from '@dxos/react-ui-attention/testing';
11
+ import { withMosaic } from '@dxos/react-ui-mosaic/testing';
12
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
13
+
14
+ import { PostStack, type PostStackAction } from '#components';
15
+ import { generateFeed, generatePosts } from '#testing';
16
+ import { type Subscription } from '#types';
17
+
18
+ type FeedArticleStoryProps = {
19
+ feedUrl?: string;
20
+ };
21
+
22
+ const useFeedData = (feedUrl?: string): { feed: Subscription.Subscription; posts: Subscription.Post[] } | undefined => {
23
+ const [data, setData] = useState<{ feed: Subscription.Subscription; posts: Subscription.Post[] }>();
24
+
25
+ useEffect(() => {
26
+ let cancelled = false;
27
+ const load = async () => {
28
+ if (feedUrl) {
29
+ const { fetchRss } = await import('../../operations/sources/rss');
30
+ const result = await EffectEx.runPromise(fetchRss(feedUrl, { corsProxy: '/api/rss?url=' }));
31
+ if (!cancelled) {
32
+ setData(result);
33
+ }
34
+ } else {
35
+ const feed = generateFeed();
36
+ const posts = generatePosts(50);
37
+ if (!cancelled) {
38
+ setData({ feed, posts });
39
+ }
40
+ }
41
+ };
42
+
43
+ void load().catch(() => {});
44
+
45
+ return () => {
46
+ cancelled = true;
47
+ };
48
+ }, [feedUrl]);
49
+
50
+ return data;
51
+ };
52
+
53
+ const DefaultStory = ({ feedUrl }: FeedArticleStoryProps) => {
54
+ const data = useFeedData(feedUrl);
55
+ const [currentPostId, setCurrentPostId] = useState<string>();
56
+
57
+ const handleAction = (action: PostStackAction) => {
58
+ if (action.type === 'current') {
59
+ setCurrentPostId(action.postId);
60
+ }
61
+ };
62
+
63
+ if (!data) {
64
+ return null;
65
+ }
66
+
67
+ const { feed, posts } = data;
68
+
69
+ return (
70
+ <Panel.Root role='article' className='dx-document'>
71
+ <Panel.Toolbar asChild>
72
+ <Toolbar.Root>
73
+ <Toolbar.Text>{feed.name}</Toolbar.Text>
74
+ </Toolbar.Root>
75
+ </Panel.Toolbar>
76
+ <Panel.Content>
77
+ <PostStack id='story-feed' posts={posts} currentId={currentPostId} onAction={handleAction} />
78
+ </Panel.Content>
79
+ </Panel.Root>
80
+ );
81
+ };
82
+
83
+ const meta: Meta<typeof DefaultStory> = {
84
+ title: 'plugins/plugin-magazine/containers/FeedArticle',
85
+ component: DefaultStory,
86
+ decorators: [withTheme(), withLayout({ layout: 'column' }), withAttention(), withMosaic()],
87
+ parameters: {
88
+ layout: 'fullscreen',
89
+ },
90
+ };
91
+
92
+ export default meta;
93
+
94
+ type Story = StoryObj<typeof meta>;
95
+
96
+ export const Default: Story = {};
97
+
98
+ export const FromRss: Story = {
99
+ args: {
100
+ feedUrl: 'https://hnrss.org/frontpage',
101
+ },
102
+ };
@@ -0,0 +1,68 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useState } from 'react';
6
+
7
+ import { useOperationInvoker } from '@dxos/app-framework/ui';
8
+ import { type AppSurface } from '@dxos/app-toolkit/ui';
9
+ import { Filter, Obj, Query, Ref, Scope } from '@dxos/echo';
10
+ import { useObject, useQuery } from '@dxos/react-client/echo';
11
+ import { Panel } from '@dxos/react-ui';
12
+
13
+ import { PostStack, type PostStackAction } from '#components';
14
+ import { meta } from '#meta';
15
+ import { FeedOperation } from '#types';
16
+ import { Subscription } from '#types';
17
+
18
+ import { FeedToolbar } from './FeedToolbar';
19
+
20
+ export type FeedArticleProps = AppSurface.ObjectArticleProps<Subscription.Subscription>;
21
+
22
+ export const FeedArticle = ({ role, subject, attendableId }: FeedArticleProps) => {
23
+ const { invokePromise } = useOperationInvoker();
24
+ const [currentPostId, setCurrentPostId] = useState<string>();
25
+ const [subscription] = useObject(subject);
26
+ // Subscribe to the backing queue via its Ref — `.target` alone does not re-render when the
27
+ // feed loads after navigation (same pitfall as plugin-inbox MailboxArticle).
28
+ const [postFeed] = useObject(subscription?.feed);
29
+ const db = Obj.getDatabase(subscription);
30
+ const posts = useQuery(
31
+ db,
32
+ postFeed
33
+ ? Query.select(Filter.type(Subscription.Post)).from(Scope.feed(Obj.getURI(postFeed)))
34
+ : Query.select(Filter.nothing()),
35
+ );
36
+
37
+ const handleAction = useCallback((action: PostStackAction) => {
38
+ if (action.type === 'current') {
39
+ setCurrentPostId(action.postId);
40
+ }
41
+ }, []);
42
+
43
+ const handleSync = useCallback(() => {
44
+ // Failures surface as a toast via `notify`; invokePromise resolves with `{ error }`, never throws.
45
+ void invokePromise(
46
+ FeedOperation.SyncFeed,
47
+ { feed: Ref.make(subject) },
48
+ {
49
+ spaceId: Obj.getDatabase(subject)?.spaceId,
50
+ notify: { error: ['sync-feed-error.title', { ns: meta.profile.key }] },
51
+ },
52
+ );
53
+ }, [subject, invokePromise]);
54
+
55
+ return (
56
+ <Panel.Root role={role} className='dx-document'>
57
+ <FeedToolbar attendableId={attendableId} onSync={handleSync} />
58
+ <Panel.Content asChild>
59
+ <PostStack
60
+ id={subscription?.id ?? subject.id}
61
+ posts={posts}
62
+ currentId={currentPostId}
63
+ onAction={handleAction}
64
+ />
65
+ </Panel.Content>
66
+ </Panel.Root>
67
+ );
68
+ };
@@ -0,0 +1,47 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Panel } from '@dxos/react-ui';
8
+ import { Menu, MenuBuilder, useMenuBuilder } from '@dxos/react-ui-menu';
9
+
10
+ import { meta } from '#meta';
11
+
12
+ export type FeedToolbarProps = {
13
+ attendableId?: string;
14
+ onSync: () => void;
15
+ };
16
+
17
+ /**
18
+ * Reactive feed toolbar built from the menu action-graph idiom: a single Sync action. Matches the
19
+ * magazine/post toolbars; the feed's name is surfaced by the plank chrome, not duplicated here.
20
+ */
21
+ export const FeedToolbar = ({ attendableId, onSync }: FeedToolbarProps) => {
22
+ const menuActions = useMenuBuilder(
23
+ () =>
24
+ MenuBuilder.make()
25
+ .action(
26
+ 'sync',
27
+ {
28
+ label: ['sync-feed.label', { ns: meta.profile.key }],
29
+ icon: 'ph--arrows-clockwise--regular',
30
+ iconOnly: true,
31
+ },
32
+ onSync,
33
+ )
34
+ .build(),
35
+ [onSync],
36
+ );
37
+
38
+ return (
39
+ <Menu.Root {...menuActions} attendableId={attendableId}>
40
+ <Panel.Toolbar asChild>
41
+ <Menu.Toolbar />
42
+ </Panel.Toolbar>
43
+ </Menu.Root>
44
+ );
45
+ };
46
+
47
+ FeedToolbar.displayName = 'FeedToolbar';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export { FeedArticle as default } from './FeedArticle';
@@ -0,0 +1,107 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useMemo, useState } from 'react';
6
+
7
+ import { useOperationInvoker } from '@dxos/app-framework/ui';
8
+ import { LayoutOperation, Paths } from '@dxos/app-toolkit';
9
+ import { type AppSurface } from '@dxos/app-toolkit/ui';
10
+ import { Operation, Trigger } from '@dxos/compute';
11
+ import { Filter, Obj, Query, Ref } from '@dxos/echo';
12
+ import { useObject, useQuery } from '@dxos/react-client/echo';
13
+ import { IconButton, Input, useTranslation } from '@dxos/react-ui';
14
+ import { Form } from '@dxos/react-ui-form';
15
+
16
+ import { meta } from '#meta';
17
+ import { FeedOperation, Subscription } from '#types';
18
+
19
+ export type FeedPropertiesProps = AppSurface.ObjectPropertiesProps<Subscription.Subscription>;
20
+
21
+ export const FeedProperties = ({ subject }: FeedPropertiesProps) => {
22
+ const { t } = useTranslation(meta.profile.key);
23
+ const { invokePromise } = useOperationInvoker();
24
+ const db = useMemo(() => Obj.getDatabase(subject), [subject]);
25
+ const [pending, setPending] = useState(false);
26
+
27
+ const childTriggers = useQuery(
28
+ db,
29
+ Query.select(Filter.and(Filter.type(Trigger.Trigger), Filter.childOf(subject, { transitive: false }))).debugLabel(
30
+ 'plugin-magazine.FeedProperties.syncTrigger',
31
+ ),
32
+ );
33
+
34
+ const syncTrigger = useMemo(() => childTriggers.find((trigger) => trigger.spec?.kind === 'timer'), [childTriggers]);
35
+
36
+ const [syncEnabled, setSyncEnabled] = useObject(syncTrigger, 'enabled');
37
+
38
+ const handleToggleSync = useCallback(async () => {
39
+ if (!db) {
40
+ return;
41
+ }
42
+
43
+ if (syncTrigger) {
44
+ setSyncEnabled((enabled) => !enabled);
45
+ return;
46
+ }
47
+
48
+ setPending(true);
49
+ try {
50
+ const operation = await ensureSyncFeedOperation(db);
51
+ db.add(
52
+ Trigger.make({
53
+ [Obj.Parent]: subject,
54
+ enabled: true,
55
+ spec: Trigger.specTimer('*/5 * * * *'),
56
+ runnable: Ref.make(operation),
57
+ input: { feed: db.makeRef(Obj.getURI(subject)) },
58
+ }),
59
+ );
60
+ } finally {
61
+ setPending(false);
62
+ }
63
+ }, [syncTrigger, db, subject, setSyncEnabled]);
64
+
65
+ const handleViewTrigger = useCallback(() => {
66
+ if (!db) {
67
+ return;
68
+ }
69
+
70
+ void invokePromise(LayoutOperation.Open, {
71
+ subject: [Paths.getSpacePath(db.spaceId, 'settings', 'org.dxos.plugin.routine.routines')],
72
+ workspace: Paths.getSpacePath(db.spaceId),
73
+ });
74
+ }, [invokePromise, db]);
75
+
76
+ return (
77
+ <Form.Section>
78
+ <Input.Root>
79
+ <Input.Label>{t('feed-sync.label')}</Input.Label>
80
+ <div className='flex flex-row items-center'>
81
+ <Input.Switch
82
+ checked={syncEnabled ?? false}
83
+ disabled={pending}
84
+ onCheckedChange={() => {
85
+ void handleToggleSync();
86
+ }}
87
+ />
88
+ {syncTrigger && (
89
+ <IconButton iconOnly icon='ph--gear--regular' label={t('view-trigger.label')} onClick={handleViewTrigger} />
90
+ )}
91
+ </div>
92
+ </Input.Root>
93
+ </Form.Section>
94
+ );
95
+ };
96
+
97
+ const ensureSyncFeedOperation = async (db: NonNullable<ReturnType<typeof Obj.getDatabase>>) => {
98
+ const existing = await db
99
+ .query(Filter.and(Filter.type(Operation.PersistentOperation), Filter.key(FeedOperation.SyncFeed.meta.key)))
100
+ .run();
101
+ const [existingOperation] = existing;
102
+ if (existingOperation) {
103
+ return existingOperation;
104
+ }
105
+
106
+ return db.add(Operation.serialize(FeedOperation.SyncFeed));
107
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export { FeedProperties as default } from './FeedProperties';
@@ -0,0 +1,244 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import * as Effect from 'effect/Effect';
7
+ import React from 'react';
8
+ import { expect, userEvent, waitFor, within } from 'storybook/test';
9
+
10
+ import { withPluginManager } from '@dxos/app-framework/testing';
11
+ import { type Client } from '@dxos/client';
12
+ import { type Space } from '@dxos/client/echo';
13
+ import { Feed, Filter, Obj, Ref } from '@dxos/echo';
14
+ import { ClientPlugin } from '@dxos/plugin-client/testing';
15
+ import { initializeIdentity } from '@dxos/plugin-client/testing';
16
+ import { SpacePlugin } from '@dxos/plugin-space/testing';
17
+ import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
18
+ import { useQuery, useSpaces } from '@dxos/react-client/echo';
19
+ import { Loading, withLayout } from '@dxos/react-ui/testing';
20
+
21
+ import { MagazineSkill } from '#skills';
22
+ import { generateCuratedPost, generateFeed, generateMagazine } from '#testing';
23
+ import { translations } from '#translations';
24
+ import { Magazine, Subscription } from '#types';
25
+
26
+ import { MagazinePlugin } from '../../MagazinePlugin';
27
+ import { MagazineArticle } from './MagazineArticle';
28
+
29
+ const DefaultStory = () => {
30
+ const spaces = useSpaces();
31
+ const space = spaces[spaces.length - 1];
32
+ const magazines = useQuery(space?.db, Filter.type(Magazine.Magazine));
33
+ const magazine = magazines[0];
34
+ if (!magazine) {
35
+ return <Loading />;
36
+ }
37
+
38
+ return <MagazineArticle role='article' subject={magazine} attendableId='story' />;
39
+ };
40
+
41
+ const seedSpace =
42
+ (options: { postCount: number; feedCount: number; readFraction: number }) =>
43
+ ({ client }: { client: Client }) =>
44
+ Effect.gen(function* () {
45
+ yield* initializeIdentity(client);
46
+ const space = (yield* Effect.promise(() => client.spaces.create())) as Space;
47
+ yield* Effect.promise(() => space.waitUntilReady());
48
+
49
+ const readCount = Math.floor(options.postCount * options.readFraction);
50
+ const posts: Subscription.Post[] = [];
51
+ for (let i = 0; i < options.postCount; i++) {
52
+ posts.push(space.db.add(generateCuratedPost({ read: i < readCount })));
53
+ }
54
+
55
+ const feeds: Subscription.Subscription[] = [];
56
+ for (let i = 0; i < options.feedCount; i++) {
57
+ feeds.push(space.db.add(generateFeed()));
58
+ }
59
+
60
+ space.db.add(generateMagazine({ name: 'Distributed Systems Reading', feeds, posts }));
61
+ });
62
+
63
+ const buildMeta = (options: {
64
+ postCount: number;
65
+ feedCount: number;
66
+ readFraction: number;
67
+ }): Meta<typeof DefaultStory> => ({
68
+ component: DefaultStory,
69
+ decorators: [
70
+ withLayout({ layout: 'fullscreen' }),
71
+ withPluginManager({
72
+ plugins: [
73
+ ...corePlugins(),
74
+ ClientPlugin({
75
+ types: [Subscription.Subscription, Subscription.Post, Magazine.Magazine],
76
+ onClientInitialized: seedSpace(options),
77
+ }),
78
+ SpacePlugin({}),
79
+ StorybookPlugin({}),
80
+ MagazinePlugin(),
81
+ ],
82
+ }),
83
+ ],
84
+ parameters: {
85
+ layout: 'fullscreen',
86
+ translations,
87
+ },
88
+ });
89
+
90
+ const meta: Meta<typeof DefaultStory> = {
91
+ title: 'plugins/plugin-magazine/containers/MagazineArticle',
92
+ ...buildMeta({ postCount: 12, feedCount: 2, readFraction: 0.25 }),
93
+ };
94
+
95
+ export default meta;
96
+
97
+ type Story = StoryObj<typeof meta>;
98
+
99
+ export const Default: Story = {};
100
+
101
+ //
102
+ // CurateFlow story
103
+ //
104
+ // Seeds a magazine + a feed whose queue is pre-populated with three Posts.
105
+ // Renders MagazineArticle and exercises the full Curate cycle:
106
+ // 1. Click Curate. CurateMagazine pulls queue items into magazine.posts;
107
+ // tiles appear.
108
+ // 2. Click Clear. magazine.posts empties; "No articles yet" placeholder
109
+ // appears.
110
+ // 3. Click Curate again. With the canonical-proxy reuse fix, this must NOT
111
+ // throw the `addCore` invariant — and the tiles must come back, proving
112
+ // magazine.posts re-renders reactively after a second Curate.
113
+ //
114
+
115
+ const seedSpaceWithQueueItems = ({ client }: { client: Client }) =>
116
+ Effect.gen(function* () {
117
+ yield* initializeIdentity(client);
118
+ const space = (yield* Effect.promise(() => client.spaces.create())) as Space;
119
+ yield* Effect.promise(() => space.waitUntilReady());
120
+
121
+ // No `url` set: handleCurate's `validFeeds` filter skips this feed during
122
+ // the SyncFeed phase, so the test doesn't depend on a real RSS endpoint.
123
+ // CurateMagazine still iterates `magazine.feeds` and reads the backing
124
+ // queue, which is what we want to exercise.
125
+ const subscriptionFeed = space.db.add(
126
+ Subscription.makeSubscription({
127
+ name: 'Curate Test Feed',
128
+ type: 'rss',
129
+ }),
130
+ );
131
+ yield* Effect.promise(() => space.db.flush());
132
+
133
+ // Push three Posts into the feed's backing queue via db.appendToFeed.
134
+ // Loading the ref is necessary post-`db.add` because the inline
135
+ // `savedTarget` from `Ref.make(echoFeed)` is dropped when the ref is
136
+ // encoded for persistence.
137
+ const echoFeed = yield* Effect.promise(() => subscriptionFeed.feed!.load());
138
+ const feedRef = Ref.make(subscriptionFeed);
139
+ const posts = [
140
+ Obj.make(Subscription.Post, {
141
+ source: feedRef,
142
+ title: 'Distributed systems are hard',
143
+ description: 'A long-form discussion of consistency, availability, and partition tolerance.',
144
+ author: 'Test Author',
145
+ published: '2026-04-01T00:00:00Z',
146
+ guid: 'post-a',
147
+ link: 'https://example.com/post-a',
148
+ }),
149
+ Obj.make(Subscription.Post, {
150
+ source: feedRef,
151
+ title: 'Local-first is the future',
152
+ description: 'How CRDTs and last-writer-wins reconcile collaborative state across peers.',
153
+ author: 'Test Author',
154
+ published: '2026-04-02T00:00:00Z',
155
+ guid: 'post-b',
156
+ link: 'https://example.com/post-b',
157
+ }),
158
+ Obj.make(Subscription.Post, {
159
+ source: feedRef,
160
+ title: 'P2P discovery patterns',
161
+ description: 'Comparing rendezvous, DHT-based, and broadcast strategies for peer discovery.',
162
+ author: 'Test Author',
163
+ published: '2026-04-03T00:00:00Z',
164
+ guid: 'post-c',
165
+ link: 'https://example.com/post-c',
166
+ }),
167
+ ];
168
+ yield* Effect.promise(() => space.db.appendToFeed(echoFeed, posts));
169
+
170
+ const mag = Magazine.make({
171
+ name: 'Curate Flow Test',
172
+ feeds: [Ref.make(subscriptionFeed)],
173
+ });
174
+ space.db.add(mag);
175
+ // Curation resolves the base methodology skill from the registry; register it here since the
176
+ // automation plugin (which normally syncs SkillDefinition capabilities) isn't in this story.
177
+ client.graph.registry.add([MagazineSkill.make()]);
178
+ yield* Effect.promise(() => space.db.flush());
179
+ });
180
+
181
+ // TODO(jdw): Re-enable on jdw/identifiers — the seed path now uses
182
+ // `space.db.appendToFeed(echoFeed, posts)` but the storybook story still loses
183
+ // queue contents between the seed promise resolving and `magazine.posts` being
184
+ // read inside the `CurateMagazine` operation. The unit tests drive
185
+ // `curateMagazine` directly against an `EchoTestBuilder` database and pass;
186
+ // the fully-wired plugin container in this story does not. Default story is
187
+ // unaffected (it never touches the feed path).
188
+ export const CurateFlow: Story = {
189
+ tags: ['!test'],
190
+ decorators: [
191
+ withLayout({ layout: 'fullscreen' }),
192
+ withPluginManager({
193
+ plugins: [
194
+ ...corePlugins(),
195
+ ClientPlugin({
196
+ types: [Feed.Feed, Subscription.Subscription, Subscription.Post, Magazine.Magazine],
197
+ onClientInitialized: seedSpaceWithQueueItems,
198
+ }),
199
+ SpacePlugin({}),
200
+ StorybookPlugin({}),
201
+ MagazinePlugin(),
202
+ ],
203
+ }),
204
+ ],
205
+ parameters: {
206
+ layout: 'fullscreen',
207
+ translations,
208
+ },
209
+ play: async ({ canvasElement }) => {
210
+ const canvas = within(canvasElement);
211
+
212
+ // Wait for the magazine to render (initially empty).
213
+ const empty = await canvas.findByText(/no articles yet/i, {}, { timeout: 10_000 });
214
+ await expect(empty).toBeVisible();
215
+
216
+ // First Curate.
217
+ const curateButton = await canvas.findByRole('button', { name: /^curate$/i });
218
+ await userEvent.click(curateButton);
219
+
220
+ await waitFor(
221
+ async () => {
222
+ const tile = await canvas.findByText('Distributed systems are hard');
223
+ await expect(tile).toBeVisible();
224
+ },
225
+ { timeout: 15_000 },
226
+ );
227
+
228
+ // Second Curate (back-to-back, no Clear). This is the user-reported
229
+ // failure mode: the second invocation previously tripped ECHO's
230
+ // `!_objects.has(core.id)` invariant in `addCore`. With the
231
+ // reuse-or-add fix it must NOT throw — the tiles already on screen
232
+ // must remain visible (curate is purely additive, dedups by id).
233
+ const curateAgain = await canvas.findByRole('button', { name: /^curate$/i });
234
+ await userEvent.click(curateAgain);
235
+
236
+ // Give the operation enough time to run.
237
+ await new Promise((resolve) => setTimeout(resolve, 2000));
238
+
239
+ // The previously-shown tiles must still be present; nothing should have crashed the plank.
240
+ await expect(await canvas.findByText('Distributed systems are hard')).toBeVisible();
241
+ await expect(await canvas.findByText('Local-first is the future')).toBeVisible();
242
+ await expect(await canvas.findByText('P2P discovery patterns')).toBeVisible();
243
+ },
244
+ };