@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,146 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { useAtomValue } from '@effect-atom/atom-react';
6
+ import React, { useCallback, useEffect, useMemo } from 'react';
7
+
8
+ import { useOperationInvoker } from '@dxos/app-framework/ui';
9
+ import { LayoutOperation, Paths } from '@dxos/app-toolkit';
10
+ import { type AppSurface, useShowItem } from '@dxos/app-toolkit/ui';
11
+ import { Obj, Ref } from '@dxos/echo';
12
+ import { log } from '@dxos/log';
13
+ import { useObject } from '@dxos/react-client/echo';
14
+ import { Panel, useTranslation } from '@dxos/react-ui';
15
+ import { linkedSegment, useSelection } from '@dxos/react-ui-attention';
16
+ import { Masonry } from '@dxos/react-ui-masonry';
17
+ import { Menu } from '@dxos/react-ui-menu';
18
+
19
+ import { useVisibleMagazinePosts } from '#atoms';
20
+ import { meta } from '#meta';
21
+ import { FeedOperation, Magazine, Subscription } from '#types';
22
+
23
+ import { MagazineTile } from './MagazineTile';
24
+ import { useToolbar } from './useToolbar';
25
+
26
+ export type MagazineArticleProps = AppSurface.ObjectArticleProps<Magazine.Magazine>;
27
+
28
+ export const MagazineArticle = ({ role, subject, attendableId }: MagazineArticleProps) => {
29
+ const { t } = useTranslation(meta.profile.key);
30
+ const invoker = useOperationInvoker();
31
+ const [magazine] = useObject(subject);
32
+
33
+ // The toolbar owns the view-filter atom and the curate/clear handlers; the article reads `view` to
34
+ // filter the visible posts.
35
+ const { menu, viewAtom } = useToolbar({ magazine: subject });
36
+ const view = useAtomValue(viewAtom);
37
+
38
+ const showItem = useShowItem();
39
+ const id = attendableId ?? Obj.getURI(magazine);
40
+ const currentId = useSelection(id, 'single');
41
+ const db = Obj.getDatabase(magazine);
42
+ const posts = useVisibleMagazinePosts(subject, view);
43
+
44
+ const handleToggleStar = useCallback(
45
+ async (post: Subscription.Post, starred: boolean) => {
46
+ const subscription = await post.source?.load();
47
+ if (db && subscription) {
48
+ void Subscription.setTag(subscription, post.id, db, 'starred', !starred);
49
+ }
50
+ },
51
+ [db],
52
+ );
53
+
54
+ const handleOpen = useCallback(
55
+ async (post: Subscription.Post) => {
56
+ const subscription = await post.source?.load();
57
+ if (subscription && !Subscription.getReadAt(subscription, post.id)) {
58
+ void Subscription.setReadAt(subscription, post.id, new Date().toISOString());
59
+ }
60
+ if (post.link) {
61
+ void invoker
62
+ .invokePromise(FeedOperation.LoadPostContent, { post: Ref.make(post) })
63
+ .catch((err) => log.catch(err, { postLink: post.link }));
64
+ }
65
+ void showItem({
66
+ contextId: id,
67
+ selectionId: post.id,
68
+ companion: linkedSegment('post'),
69
+ path: Paths.getObjectPathFromObject(subject),
70
+ });
71
+ },
72
+ [id, showItem, invoker, subject],
73
+ );
74
+
75
+ const noPosts = posts.length === 0;
76
+ useEffect(() => {
77
+ if (noPosts) {
78
+ void invoker.invokePromise(LayoutOperation.UpdateCompanion, {
79
+ subject: linkedSegment('settings'),
80
+ });
81
+ }
82
+ }, [noPosts, invoker]);
83
+
84
+ const tileItems = useMemo<TileData[]>(
85
+ () =>
86
+ posts.map((post) => ({
87
+ id: post.id,
88
+ post,
89
+ magazine: subject,
90
+ current: currentId === post.id,
91
+ onToggleStar: handleToggleStar,
92
+ onOpen: handleOpen,
93
+ })),
94
+ [posts, currentId, handleOpen, handleToggleStar],
95
+ );
96
+
97
+ return (
98
+ <Panel.Root role={role}>
99
+ <Menu.Root {...menu} attendableId={attendableId}>
100
+ <Panel.Toolbar asChild>
101
+ <Menu.Toolbar />
102
+ </Panel.Toolbar>
103
+ </Menu.Root>
104
+ <Panel.Content>
105
+ {noPosts ? (
106
+ // TODO(burdon): Factor out common EmptyState component; of push into Masonry, List, etc.
107
+ <div className='h-full flex items-center justify-center text-subdued text-sm'>
108
+ {t('empty-magazine.message')}
109
+ </div>
110
+ ) : (
111
+ <Masonry.Root Tile={TileAdapter} minColumnWidth={20} maxColumnWidth={25}>
112
+ <Masonry.Content thin centered padding>
113
+ {/* TODO(burdon): Move items into Root. */}
114
+ <Masonry.Viewport classNames='py-2' items={tileItems} />
115
+ </Masonry.Content>
116
+ </Masonry.Root>
117
+ )}
118
+ </Panel.Content>
119
+ </Panel.Root>
120
+ );
121
+ };
122
+
123
+ type TileData = {
124
+ id: string;
125
+ post: Subscription.Post;
126
+ magazine: Magazine.Magazine;
127
+ current: boolean;
128
+ onToggleStar?: (post: Subscription.Post, starred: boolean) => void;
129
+ onOpen?: (post: Subscription.Post) => void;
130
+ };
131
+
132
+ const TileAdapter = ({ data }: { data: TileData | undefined; index: number }) => {
133
+ if (!data?.post) {
134
+ return null;
135
+ }
136
+
137
+ return (
138
+ <MagazineTile
139
+ post={data.post}
140
+ magazine={data.magazine}
141
+ current={data.current}
142
+ onOpen={data.onOpen}
143
+ onToggleStar={data.onToggleStar}
144
+ />
145
+ );
146
+ };
@@ -0,0 +1,88 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React, { type MouseEvent, useCallback } from 'react';
6
+
7
+ import { Obj } from '@dxos/echo';
8
+ import { Card, Focus, SystemIconButton } from '@dxos/react-ui';
9
+ import { mx } from '@dxos/ui-theme';
10
+
11
+ import { useMagazinePostData } from '#atoms';
12
+ import { type Magazine, type Subscription } from '#types';
13
+
14
+ import { formatDate } from '../../util/date';
15
+
16
+ export type MagazineTileProps = {
17
+ post: Subscription.Post;
18
+ magazine: Magazine.Magazine;
19
+ current?: boolean;
20
+ onOpen?: (post: Subscription.Post) => void;
21
+ onToggleStar?: (post: Subscription.Post, starred: boolean) => void;
22
+ };
23
+
24
+ export const MagazineTile = ({ post, magazine, current, onToggleStar, onOpen }: MagazineTileProps) => {
25
+ // All per-Post derivation (snapshot, read, starred, snippet, image, feed name) happens in
26
+ // atom-land; this tile re-renders only when THIS post's slice changes, not when a sibling does.
27
+ const { post: snapshot, feedName, read, starred, snippet, imageUrl } = useMagazinePostData(post, magazine);
28
+
29
+ // `Focus.Item` calls `onCurrentChange` on click and on Enter.
30
+ const handleCurrentChange = useCallback(() => {
31
+ onOpen?.(post);
32
+ }, [onOpen, post]);
33
+
34
+ const handleToggleStar = useCallback(
35
+ (event: MouseEvent<HTMLButtonElement>) => {
36
+ // Prevent Focus.Item's onClick from firing the open action — clicks on the star toggle should not open the post.
37
+ event.stopPropagation();
38
+ onToggleStar?.(post, starred);
39
+ },
40
+ [onToggleStar, post, starred],
41
+ );
42
+
43
+ return (
44
+ <Focus.Item asChild current={current} onCurrentChange={handleCurrentChange}>
45
+ <Card.Root
46
+ fullWidth
47
+ classNames={mx('dx-hover dx-current cursor-pointer transition-opacity', read && !current && 'opacity-60')}
48
+ >
49
+ {imageUrl && (
50
+ <Card.Poster alt={snapshot.title ?? 'Article'} image={imageUrl} fit='cover' classNames='rounded-t-xs' />
51
+ )}
52
+ <Card.Header>
53
+ <Card.Block>
54
+ <SystemIconButton.Star
55
+ variant='ghost'
56
+ iconOnly
57
+ square
58
+ size={4}
59
+ active={starred}
60
+ onClick={handleToggleStar}
61
+ />
62
+ </Card.Block>
63
+ {snapshot.title ? <Card.Title classNames='line-clamp-2'>{snapshot.title}</Card.Title> : <div />}
64
+ <Card.Block end />
65
+ </Card.Header>
66
+ <Card.Body>
67
+ {snippet && (
68
+ <Card.Row>
69
+ <Card.Text variant='description' classNames='line-clamp-3'>
70
+ {snippet}
71
+ </Card.Text>
72
+ </Card.Row>
73
+ )}
74
+ <Card.Row>
75
+ <div className='grid grid-cols-[minmax(0,1fr)_auto] items-center gap-2 py-1.5 text-sm text-description overflow-hidden'>
76
+ <span className='truncate'>{feedName ?? ''}</span>
77
+ <span className='text-end shrink-0'>{formatPublished(snapshot) ?? ''}</span>
78
+ </div>
79
+ </Card.Row>
80
+ </Card.Body>
81
+ </Card.Root>
82
+ </Focus.Item>
83
+ );
84
+ };
85
+
86
+ /** Convenience: format a Post's published date the way the magazine view shows it. */
87
+ const formatPublished = (post: Obj.Snapshot<Subscription.Post>): string | undefined =>
88
+ post.published ? formatDate(post.published) : undefined;
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export { MagazineArticle as default } from './MagazineArticle';
@@ -0,0 +1,145 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { Atom, RegistryContext, useAtomSet } from '@effect-atom/atom-react';
6
+ import * as Effect from 'effect/Effect';
7
+ import { useCallback, useContext, useMemo } from 'react';
8
+
9
+ import { useOperationInvoker } from '@dxos/app-framework/ui';
10
+ import { Obj, Ref } from '@dxos/echo';
11
+ import { EffectEx } from '@dxos/effect';
12
+ import { MenuBuilder, useMenuBuilder } from '@dxos/react-ui-menu';
13
+
14
+ import { type MagazineView } from '#atoms';
15
+ import { meta } from '#meta';
16
+ import { FeedOperation, type Magazine } from '#types';
17
+
18
+ export type UseToolbarProps = {
19
+ magazine: Magazine.Magazine;
20
+ };
21
+
22
+ /**
23
+ * Owns the magazine toolbar: the view-filter and curate-busy atoms, the Curate/Clear operation
24
+ * handlers, and the reactive menu action graph. The action graph reads its state — view filter,
25
+ * curate-busy flag, and whether the magazine has feeds — via `get` inside the builder, so it
26
+ * subscribes to those atoms and rebuilds whenever they change. Returns the menu props (spread into
27
+ * `Menu.Root`) plus `viewAtom`, which the article also reads to filter the visible posts.
28
+ */
29
+ export const useToolbar = ({ magazine }: UseToolbarProps) => {
30
+ const invoker = useOperationInvoker();
31
+ const registry = useContext(RegistryContext);
32
+ const db = Obj.getDatabase(magazine);
33
+ const viewAtom = useMemo(() => Atom.make<MagazineView>('default'), []);
34
+ const busyAtom = useMemo(() => Atom.make(false), []);
35
+ const setView = useAtomSet(viewAtom);
36
+
37
+ // Curate and Clear share the busy guard: ignore re-entry while a run is in flight, flip the busy flag
38
+ // for the duration, then run the supplied invocation (failures surface via its own notify config).
39
+ const runExclusive = useCallback(
40
+ <A, E>(effect: Effect.Effect<A, E>) => {
41
+ if (registry.get(busyAtom)) {
42
+ return;
43
+ }
44
+ registry.set(busyAtom, true);
45
+ void EffectEx.runAndForwardErrors(effect.pipe(Effect.ensuring(Effect.sync(() => registry.set(busyAtom, false)))));
46
+ },
47
+ [registry, busyAtom],
48
+ );
49
+
50
+ const handleCurate = useCallback(
51
+ () =>
52
+ runExclusive(
53
+ invoker.invoke(
54
+ FeedOperation.CurateMagazine,
55
+ { magazine: Ref.make(magazine) },
56
+ { spaceId: db?.spaceId, notify: { error: ['curate-error.message', { ns: meta.profile.key }] } },
57
+ ),
58
+ ),
59
+ [runExclusive, invoker, magazine, db],
60
+ );
61
+
62
+ const handleClear = useCallback(
63
+ () =>
64
+ runExclusive(
65
+ invoker.invoke(
66
+ FeedOperation.ClearMagazine,
67
+ { magazine: Ref.make(magazine) },
68
+ { spaceId: db?.spaceId, notify: { error: ['clear-error.message', { ns: meta.profile.key }] } },
69
+ ),
70
+ ),
71
+ [runExclusive, invoker, magazine, db],
72
+ );
73
+
74
+ const menu = useMenuBuilder(
75
+ (get) => {
76
+ const view = get(viewAtom);
77
+ const busy = get(busyAtom);
78
+ const hasFeeds = (get(Obj.atomProperty(magazine, 'feeds')) ?? []).length > 0;
79
+ // Curate runs the magazine's Routine, so it is unavailable until that Routine exists.
80
+ const hasRoutine = Boolean(get(Obj.atomProperty(magazine, 'instructions')));
81
+ // Curate is disabled while busy or until there is at least one feed; the tooltip explains why.
82
+ const curateLabel = !hasFeeds ? 'no-feeds.label' : busy ? 'refreshing-magazine.label' : 'curate.label';
83
+
84
+ return (
85
+ MenuBuilder.make()
86
+ // Single-select filter: `value` is the active child id; `'default'` matches neither, so both
87
+ // toggles read as off. Re-clicking the active toggle returns to `'default'`.
88
+ .group(
89
+ 'view',
90
+ {
91
+ label: ['view-filter.label', { ns: meta.profile.key }],
92
+ variant: 'toggleGroup',
93
+ selectCardinality: 'single',
94
+ value: view,
95
+ },
96
+ (group) => {
97
+ group.action(
98
+ 'starred',
99
+ {
100
+ label: ['only-starred.label', { ns: meta.profile.key }],
101
+ icon: view === 'starred' ? 'ph--star--fill' : 'ph--star--regular',
102
+ },
103
+ () => setView(view === 'starred' ? 'default' : 'starred'),
104
+ );
105
+ group.action(
106
+ 'archived',
107
+ {
108
+ label: ['show-archived.label', { ns: meta.profile.key }],
109
+ icon: 'ph--archive--regular',
110
+ },
111
+ () => setView(view === 'archived' ? 'default' : 'archived'),
112
+ );
113
+ },
114
+ )
115
+ // `gap` is a flexible spacer that pushes Clear + Curate to the trailing edge.
116
+ .separator('gap')
117
+ .action(
118
+ 'clear',
119
+ {
120
+ label: ['clear-magazine.label', { ns: meta.profile.key }],
121
+ icon: 'ph--broom--regular',
122
+ iconOnly: true,
123
+ disabled: busy,
124
+ },
125
+ handleClear,
126
+ )
127
+ .action(
128
+ 'curate',
129
+ {
130
+ label: [curateLabel, { ns: meta.profile.key }],
131
+ icon: busy ? 'ph--circle-notch--regular' : 'ph--sparkle--regular',
132
+ iconClassNames: busy ? 'animate-spin' : undefined,
133
+ iconOnly: true,
134
+ disabled: busy || !hasFeeds || !hasRoutine,
135
+ },
136
+ handleCurate,
137
+ )
138
+ .build()
139
+ );
140
+ },
141
+ [magazine, viewAtom, busyAtom, setView, handleClear, handleCurate],
142
+ );
143
+
144
+ return { menu, viewAtom };
145
+ };
@@ -0,0 +1,117 @@
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
+
9
+ import { withPluginManager } from '@dxos/app-framework/testing';
10
+ import { type Client } from '@dxos/client';
11
+ import { type Space } from '@dxos/client/echo';
12
+ import { Filter, Tag } from '@dxos/echo';
13
+ import { ClientPlugin, initializeIdentity } from '@dxos/plugin-client/testing';
14
+ import { SpacePlugin } from '@dxos/plugin-space/testing';
15
+ import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
16
+ import { useQuery, useSpaces } from '@dxos/react-client/echo';
17
+ import { Loading, withLayout } from '@dxos/react-ui/testing';
18
+ import { Text } from '@dxos/schema';
19
+
20
+ import { translations } from '#translations';
21
+ import { Subscription } from '#types';
22
+
23
+ import { MagazinePlugin } from '../../MagazinePlugin';
24
+ import { PostArticle } from './PostArticle';
25
+
26
+ const SAMPLE_MARKDOWN = `# Local-first software
27
+
28
+ > "You own your data, in spite of the cloud." — Ink & Switch
29
+
30
+ Local-first apps put the user's data on their own device, syncing through a
31
+ decentralized substrate rather than a central server. That shift unlocks several
32
+ properties that cloud apps struggle to deliver:
33
+
34
+ ## Why it matters
35
+
36
+ - **No spinners.** Reads and writes hit local storage, so latency is sub-millisecond.
37
+ - **Offline by default.** The network is an enhancement, not a requirement.
38
+ - **Longevity.** Your data survives the vendor — no lock-in, no shutdown risk.
39
+
40
+ ## A small example
41
+
42
+ \`\`\`ts
43
+ const space = await client.spaces.create();
44
+ const note = space.db.add(Obj.make(Note, { title: 'Hello' }));
45
+ \`\`\`
46
+
47
+ See the [original essay](https://www.inkandswitch.com/local-first/) for the full
48
+ list of seven ideals, including real-time collaboration and end-to-end privacy.
49
+ `;
50
+
51
+ const DefaultStory = () => {
52
+ const spaces = useSpaces();
53
+ const space = spaces[spaces.length - 1];
54
+ const posts = useQuery(space?.db, Filter.type(Subscription.Post));
55
+ const post = posts[0];
56
+ if (!post) {
57
+ return <Loading />;
58
+ }
59
+
60
+ return <PostArticle role='article' subject={post} attendableId='story' />;
61
+ };
62
+
63
+ // `PostArticle` calls `useOperationInvoker` (for the star/archive/mark-unread toolbar actions), so
64
+ // the story must render under a plugin manager that provides the OperationInvoker capability — a
65
+ // bare `withClientProvider` throws "Missing PluginManagerContext".
66
+ const seedSpace = ({ client }: { client: Client }) =>
67
+ Effect.gen(function* () {
68
+ yield* initializeIdentity(client);
69
+ const space = (yield* Effect.promise(() => client.spaces.create())) as Space;
70
+ yield* Effect.promise(() => space.waitUntilReady());
71
+
72
+ // Content/imageUrl no longer live on the Post object — they belong on
73
+ // `Subscription.contentFeed` / `Subscription.postState`. PostContent falls
74
+ // back to `description` when no fetched body is available, so the sample
75
+ // markdown is stashed there for the story.
76
+ space.db.add(
77
+ Subscription.makePost({
78
+ title: 'Local-first software: a primer',
79
+ link: 'https://www.inkandswitch.com/local-first/',
80
+ author: 'Martin Kleppmann',
81
+ published: new Date().toISOString(),
82
+ guid: 'local-first-primer',
83
+ description: SAMPLE_MARKDOWN,
84
+ }),
85
+ );
86
+ yield* Effect.promise(() => space.db.flush());
87
+ });
88
+
89
+ const meta: Meta<typeof DefaultStory> = {
90
+ title: 'plugins/plugin-magazine/containers/PostArticle',
91
+ render: DefaultStory,
92
+ decorators: [
93
+ withLayout({ layout: 'fullscreen' }),
94
+ withPluginManager({
95
+ plugins: [
96
+ ...corePlugins(),
97
+ ClientPlugin({
98
+ types: [Subscription.Subscription, Subscription.Post, Tag.Tag, Text.Text],
99
+ onClientInitialized: seedSpace,
100
+ }),
101
+ SpacePlugin({}),
102
+ StorybookPlugin({}),
103
+ MagazinePlugin(),
104
+ ],
105
+ }),
106
+ ],
107
+ parameters: {
108
+ layout: 'fullscreen',
109
+ translations,
110
+ },
111
+ };
112
+
113
+ export default meta;
114
+
115
+ type Story = StoryObj<typeof meta>;
116
+
117
+ export const Default: Story = {};
@@ -0,0 +1,140 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { Atom, RegistryContext } from '@effect-atom/atom-react';
6
+ import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
7
+
8
+ import { useOperationInvoker } from '@dxos/app-framework/ui';
9
+ import { type AppSurface } from '@dxos/app-toolkit/ui';
10
+ import { Filter, Obj, Ref } from '@dxos/echo';
11
+ import { log } from '@dxos/log';
12
+ import { useObject, useQuery } from '@dxos/react-client/echo';
13
+ import { Panel } from '@dxos/react-ui';
14
+ import { getParentId, isLinkedSegment } from '@dxos/react-ui-attention';
15
+
16
+ import { usePostContentAtom } from '#atoms';
17
+ import { PostContent } from '#components';
18
+ import { meta } from '#meta';
19
+ import { FeedOperation } from '#types';
20
+ import { Subscription } from '#types';
21
+
22
+ import { PostToolbar } from './PostToolbar';
23
+
24
+ export type PostArticleProps = AppSurface.ObjectArticleProps<Subscription.Post>;
25
+
26
+ export const PostArticle = ({ role, subject, attendableId }: PostArticleProps) => {
27
+ // When shown as a companion the `attendableId` is a linked segment, but attention lives on the
28
+ // parent plank — resolve to the parent so the toolbar reads as attended (active).
29
+ const toolbarAttendableId = attendableId && isLinkedSegment(attendableId) ? getParentId(attendableId) : attendableId;
30
+ const { invokePromise } = useOperationInvoker();
31
+ const registry = useContext(RegistryContext);
32
+ const [post] = useObject(subject);
33
+ const db = Obj.getDatabase(post);
34
+ const subscription = post.source?.target;
35
+ const { id } = post;
36
+ const postContent = usePostContentAtom(subject);
37
+
38
+ // Lazily fetch full article content the first time this Post is shown — covers
39
+ // entry points that bypass MagazineArticle's `handleOpen` (deep-link, agent surface,
40
+ // restored attention). LoadPostContent is idempotent (no-op if `content` already
41
+ // populated or `link` missing), so a unique guard per Post id is enough to keep us
42
+ // from re-firing on every render.
43
+ const requestedContentFor = useRef<string | undefined>(undefined);
44
+ useEffect(() => {
45
+ if (requestedContentFor.current === id) {
46
+ return;
47
+ }
48
+ if (!post.link || postContent) {
49
+ return;
50
+ }
51
+ requestedContentFor.current = id;
52
+ void invokePromise(FeedOperation.LoadPostContent, { post: Ref.make(subject) }).catch((err) =>
53
+ log.catch(err, { postLink: post.link }),
54
+ );
55
+ }, [subject, post, post.link, postContent, invokePromise, id]);
56
+
57
+ // Reactive lookup of the source feed name. `post.feed?.target?.name` only renders
58
+ // synchronously when the ref is already resolved; querying feeds via useQuery means
59
+ // the meta line updates as soon as the feed object is loaded into the space.
60
+ const allFeeds = useQuery(db, Filter.type(Subscription.Subscription));
61
+ const feedName = useMemo(() => {
62
+ const dxn = post.source?.uri;
63
+ if (!dxn) {
64
+ return undefined;
65
+ }
66
+ return allFeeds.find((feed) => Obj.getURI(feed) === dxn)?.name;
67
+ }, [post.source, allFeeds]);
68
+
69
+ const handleOpenOriginal = useCallback(() => {
70
+ if (post.link) {
71
+ window.open(post.link, '_blank', 'noopener,noreferrer');
72
+ }
73
+ }, [post.link]);
74
+
75
+ const handleMarkUnread = useCallback(() => {
76
+ if (subscription) {
77
+ Subscription.setReadAt(subscription, id, undefined);
78
+ }
79
+ }, [subscription, id]);
80
+
81
+ // Value-setters (not toggles): the toolbar reads the current star/archive state reactively via
82
+ // `get` and passes the desired next value, so these don't need to subscribe to the current state.
83
+ const handleSetArchived = useCallback(
84
+ (value: boolean) => {
85
+ if (db && subscription) {
86
+ void Subscription.setTag(subscription, id, db, 'archived', value);
87
+ }
88
+ },
89
+ [db, subscription, id],
90
+ );
91
+
92
+ const handleSetStarred = useCallback(
93
+ (value: boolean) => {
94
+ if (db && subscription) {
95
+ void Subscription.setTag(subscription, id, db, 'starred', value);
96
+ }
97
+ },
98
+ [db, subscription, id],
99
+ );
100
+
101
+ // Re-fetch the article body from the source via the LoadPostContent operation with `force`, so the
102
+ // fetch/extraction/storage logic lives in one place (the operation) rather than inline here. Appends
103
+ // a fresh content entry to the subscription's contentFeed; the reverse-ref lookup picks the newest.
104
+ // `refreshing` is an atom so the toolbar subscribes to it via `get` rather than via React deps.
105
+ const refreshingAtom = useMemo(() => Atom.make(false), []);
106
+ const handleRefresh = useCallback(async () => {
107
+ if (!post.link || registry.get(refreshingAtom)) {
108
+ return;
109
+ }
110
+ registry.set(refreshingAtom, true);
111
+ try {
112
+ // Failures surface as a toast via `notify`; invokePromise resolves with `{ error }`, never throws.
113
+ await invokePromise(
114
+ FeedOperation.LoadPostContent,
115
+ { post: Ref.make(subject), force: true },
116
+ { notify: { error: ['refresh-content-error.message', { ns: meta.profile.key }] } },
117
+ );
118
+ } finally {
119
+ registry.set(refreshingAtom, false);
120
+ }
121
+ }, [registry, refreshingAtom, subject, post.link, invokePromise]);
122
+
123
+ return (
124
+ <Panel.Root role={role}>
125
+ <PostToolbar
126
+ post={subject}
127
+ refreshingAtom={refreshingAtom}
128
+ attendableId={toolbarAttendableId}
129
+ onSetStarred={handleSetStarred}
130
+ onSetArchived={handleSetArchived}
131
+ onMarkUnread={handleMarkUnread}
132
+ onRefresh={() => void handleRefresh()}
133
+ onOpenOriginal={handleOpenOriginal}
134
+ />
135
+ <Panel.Content asChild>
136
+ <PostContent post={subject} metadata={feedName ? [feedName] : undefined} />
137
+ </Panel.Content>
138
+ </Panel.Root>
139
+ );
140
+ };