@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,16 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { Capability } from '@dxos/app-framework';
8
+ import { RoutineCapabilities } from '@dxos/plugin-routine';
9
+
10
+ import { magazineCuration } from '../templates/magazine-curation';
11
+
12
+ export default Capability.makeModule(
13
+ Effect.fnUntraced(function* () {
14
+ return [Capability.contributes(RoutineCapabilities.Template, magazineCuration)];
15
+ }),
16
+ );
@@ -0,0 +1,14 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { Capability } from '@dxos/app-framework';
8
+ import { AppCapabilities } from '@dxos/app-toolkit';
9
+
10
+ import { MagazineSkill } from '#skills';
11
+
12
+ const skillDefinition = () => Effect.succeed([Capability.contributes(AppCapabilities.SkillDefinition, MagazineSkill)]);
13
+
14
+ export default skillDefinition;
@@ -0,0 +1,81 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, expect, test } from 'vitest';
6
+
7
+ import { dedupeImagesInMarkdown } from './PostContent';
8
+
9
+ describe('dedupeImagesInMarkdown', () => {
10
+ test('strips an image whose URL matches the hero imageUrl', () => {
11
+ const heroUrl = 'https://example.com/hero.jpg';
12
+ const md = `Some intro paragraph.
13
+
14
+ ![Photo](${heroUrl})
15
+
16
+ More body text.`;
17
+ const out = dedupeImagesInMarkdown(md, [heroUrl]);
18
+ expect(out).not.toContain(heroUrl);
19
+ expect(out).toContain('Some intro paragraph.');
20
+ expect(out).toContain('More body text.');
21
+ });
22
+
23
+ test('strips a second occurrence of the same image inside the body', () => {
24
+ const url = 'https://example.com/photo.jpg';
25
+ const md = `![A](${url})
26
+
27
+ Paragraph.
28
+
29
+ ![A again](${url})`;
30
+ const out = dedupeImagesInMarkdown(md, []);
31
+ // Only one image markdown tag should remain.
32
+ const matches = out.match(/!\[[^\]]*\]\([^)]*\)/g) ?? [];
33
+ expect(matches).toHaveLength(1);
34
+ expect(out).toContain('Paragraph.');
35
+ });
36
+
37
+ test('strips a duplicate <img> HTML tag inside the body', () => {
38
+ const url = 'https://example.com/photo.jpg';
39
+ const md = `<img src="${url}" alt="A" />
40
+
41
+ Body.
42
+
43
+ <img src="${url}" alt="B" />`;
44
+ const out = dedupeImagesInMarkdown(md, []);
45
+ const matches = out.match(/<img\b[^>]*>/gi) ?? [];
46
+ expect(matches).toHaveLength(1);
47
+ expect(out).toContain('Body.');
48
+ });
49
+
50
+ test('leaves distinct images alone', () => {
51
+ const md = `![A](https://example.com/a.jpg)
52
+
53
+ ![B](https://example.com/b.jpg)`;
54
+ const out = dedupeImagesInMarkdown(md, []);
55
+ expect(out).toContain('https://example.com/a.jpg');
56
+ expect(out).toContain('https://example.com/b.jpg');
57
+ });
58
+
59
+ test('handles markdown image with title attribute', () => {
60
+ const url = 'https://example.com/p.jpg';
61
+ const md = `![alt](${url} "Caption text")\n\n![alt2](${url})`;
62
+ const out = dedupeImagesInMarkdown(md, []);
63
+ const matches = out.match(/!\[[^\]]*\]\([^)]*\)/g) ?? [];
64
+ expect(matches).toHaveLength(1);
65
+ });
66
+
67
+ test('hero match dedupes against both Markdown and HTML occurrences', () => {
68
+ const heroUrl = 'https://example.com/hero.jpg';
69
+ const md = `<img src="${heroUrl}" />\n\n![also hero](${heroUrl})\n\nReal body.`;
70
+ const out = dedupeImagesInMarkdown(md, [heroUrl]);
71
+ expect(out).not.toContain(heroUrl);
72
+ expect(out).toContain('Real body.');
73
+ });
74
+
75
+ test('ignores undefined entries in `excluded`', () => {
76
+ const url = 'https://example.com/img.jpg';
77
+ const md = `![Only image](${url})`;
78
+ const out = dedupeImagesInMarkdown(md, [undefined]);
79
+ expect(out).toContain(url);
80
+ });
81
+ });
@@ -0,0 +1,111 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React, { useMemo } from 'react';
6
+
7
+ import { ScrollArea } from '@dxos/react-ui';
8
+ import { composable } from '@dxos/react-ui';
9
+ import { MarkdownView } from '@dxos/react-ui-markdown';
10
+
11
+ import { usePostContentAtom } from '#atoms';
12
+ import { Subscription } from '#types';
13
+
14
+ import { formatDate, getImageUrl } from '../../util';
15
+
16
+ export type PostContentProps = {
17
+ /** Post to render. */
18
+ post: Subscription.Post;
19
+ /** Additional metadata, such as feed or source domain, rendered between author and published date. */
20
+ metadata?: string[];
21
+ };
22
+
23
+ /**
24
+ * Strips images from a markdown string whose URL is already on the
25
+ * `excluded` list, AND drops any image that appears more than once. The
26
+ * hero `post.imageUrl` is added to `excluded` by the caller so the
27
+ * extracted article body doesn't re-render the same image immediately
28
+ * below the hero. Captures both Markdown image syntax (`![alt](url
29
+ * "title")`) and inline HTML `<img>` tags that defuddle sometimes
30
+ * leaves behind.
31
+ */
32
+ export const dedupeImagesInMarkdown = (markdown: string, excluded: ReadonlyArray<string | undefined>): string => {
33
+ const seen = new Set<string>();
34
+ for (const url of excluded) {
35
+ if (url) {
36
+ seen.add(url);
37
+ }
38
+ }
39
+
40
+ // Markdown image: `![alt](url)` or `![alt](url "title")`.
41
+ const markdownImg = /!\[[^\]]*\]\(\s*([^)\s]+)(?:\s+"[^"]*")?\s*\)/g;
42
+ // HTML image: `<img ... src="url" ...>`.
43
+ const htmlImg = /<img\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi;
44
+
45
+ const dedupe = (input: string, regex: RegExp): string =>
46
+ input.replace(regex, (match, url) => {
47
+ if (typeof url !== 'string') {
48
+ return match;
49
+ }
50
+ if (seen.has(url)) {
51
+ return '';
52
+ }
53
+ seen.add(url);
54
+ return match;
55
+ });
56
+
57
+ return dedupe(dedupe(markdown, markdownImg), htmlImg);
58
+ };
59
+
60
+ /** Returns true if the markdown contains at least one Markdown or inline HTML image. */
61
+ export const contentHasImage = (markdown: string): boolean =>
62
+ /!\[[^\]]*\]\(\s*[^)\s]+/.test(markdown) || /<img\b[^>]*\bsrc=["'][^"']+["']/i.test(markdown);
63
+
64
+ /**
65
+ * Shared presentational layout for an article-style post.
66
+ * Render order: title → hero image → Markdown body (subscription contentFeed
67
+ * entry for this Post id, falling back to `post.description`) → meta line
68
+ * (author · …extra · published).
69
+ */
70
+ export const PostContent = composable<HTMLDivElement, PostContentProps>(
71
+ ({ post, metadata = [], ...props }, forwardedRef) => {
72
+ const meta = [post.author, ...metadata, formatDate(post.published)].filter(Boolean).join(' · ');
73
+ const title = post.title;
74
+ const postContent = usePostContentAtom(post);
75
+ const imageUrl = getImageUrl(post, postContent);
76
+ const fetchedText = postContent?.text;
77
+
78
+ // Drop duplicate images from the article body — and remove any image
79
+ // that matches the hero `imageUrl` so it doesn't appear stacked immediately
80
+ // below the hero. Falls through to `post.description` when content
81
+ // hasn't been fetched yet.
82
+ const content = useMemo(() => {
83
+ const source = fetchedText || post.description || '';
84
+ if (!source) {
85
+ return '';
86
+ }
87
+ return dedupeImagesInMarkdown(source, [imageUrl]);
88
+ }, [fetchedText, post.description, imageUrl]);
89
+
90
+ // Suppress the hero when the article body already carries imagery — RSS feeds often
91
+ // duplicate the lead image inside `<content>` under a different URL than `imageUrl`,
92
+ // and rendering both stacks the same picture twice (see e.g. Guardian galleries).
93
+ const showHero = useMemo(
94
+ () => Boolean(imageUrl?.startsWith('http')) && !contentHasImage(content),
95
+ [imageUrl, content],
96
+ );
97
+
98
+ return (
99
+ <ScrollArea.Root {...props} orientation='vertical' thin ref={forwardedRef}>
100
+ <ScrollArea.Viewport classNames='flex flex-col gap-3 p-4'>
101
+ {title && <h1 className='text-xl font-semibold'>{title}</h1>}
102
+ {showHero && <img src={imageUrl} alt='' className='rounded w-full object-cover max-h-72' loading='lazy' />}
103
+ {content && <MarkdownView content={content} />}
104
+ {meta && <div className='text-xs text-subdued'>{meta}</div>}
105
+ </ScrollArea.Viewport>
106
+ </ScrollArea.Root>
107
+ );
108
+ },
109
+ );
110
+
111
+ PostContent.displayName = 'PostContent';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './PostContent';
@@ -0,0 +1,39 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React, { useMemo } from 'react';
7
+
8
+ import { withAttention } from '@dxos/react-ui-attention/testing';
9
+ import { withMosaic } from '@dxos/react-ui-mosaic/testing';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+
12
+ import { generatePosts } from '#testing';
13
+
14
+ import { PostStack, type PostStackProps } from './PostStack';
15
+
16
+ const DefaultStory = (props: Omit<PostStackProps, 'id' | 'posts'>) => {
17
+ const posts = useMemo(() => generatePosts(100), []);
18
+ return <PostStack id='story' posts={posts} {...props} />;
19
+ };
20
+
21
+ const meta: Meta<typeof DefaultStory> = {
22
+ title: 'plugins/plugin-magazine/components/PostStack',
23
+ component: DefaultStory,
24
+ parameters: {
25
+ layout: 'fullscreen',
26
+ },
27
+ };
28
+
29
+ export default meta;
30
+
31
+ type Story = StoryObj<typeof meta>;
32
+
33
+ export const Default: Story = {
34
+ decorators: [withTheme(), withLayout({ layout: 'column' }), withAttention(), withMosaic()],
35
+ };
36
+
37
+ export const Responsive: Story = {
38
+ decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'w-[30rem]' }), withAttention(), withMosaic()],
39
+ };
@@ -0,0 +1,151 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { type KeyboardEvent, forwardRef, useCallback, useMemo, useState } from 'react';
6
+
7
+ import { Type } from '@dxos/echo';
8
+ import { Card, Icon, ScrollArea, useTranslation } from '@dxos/react-ui';
9
+ import { composable, composableProps } from '@dxos/react-ui';
10
+ import { MarkdownView } from '@dxos/react-ui-markdown';
11
+ import { Focus, Mosaic, type MosaicTileProps, useMosaicContainer } from '@dxos/react-ui-mosaic';
12
+
13
+ import { Subscription } from '#types';
14
+
15
+ export type PostStackAction = { type: 'current'; postId: string };
16
+
17
+ export type PostStackActionHandler = (action: PostStackAction) => void;
18
+
19
+ export type PostStackProps = {
20
+ id: string;
21
+ posts?: Subscription.Post[];
22
+ currentId?: string;
23
+ onAction?: PostStackActionHandler;
24
+ };
25
+
26
+ export const PostStack = composable<HTMLDivElement, PostStackProps>(
27
+ ({ posts = [], currentId, onAction, ...props }, forwardedRef) => {
28
+ const [viewport, setViewport] = useState<HTMLElement | null>(null);
29
+ const items = useMemo(() => posts.map((post) => ({ post, onAction })), [posts, onAction]);
30
+
31
+ const handleCurrentChange = useCallback(
32
+ (id: string | undefined) => {
33
+ if (id) {
34
+ onAction?.({ type: 'current', postId: id });
35
+ }
36
+ },
37
+ [onAction],
38
+ );
39
+
40
+ const handleKeyDown = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
41
+ if (event.key === 'Enter') {
42
+ event.preventDefault();
43
+ (document.activeElement as HTMLElement | null)?.click();
44
+ }
45
+ }, []);
46
+
47
+ return (
48
+ <Focus.Group asChild {...composableProps(props)} onKeyDown={handleKeyDown} ref={forwardedRef}>
49
+ <Mosaic.Container
50
+ asChild
51
+ withFocus
52
+ autoScroll={viewport}
53
+ currentId={currentId}
54
+ onCurrentChange={handleCurrentChange}
55
+ >
56
+ <ScrollArea.Root orientation='vertical' padding centered>
57
+ <ScrollArea.Viewport ref={setViewport}>
58
+ <Mosaic.VirtualStack
59
+ Tile={PostTile}
60
+ classNames='my-2'
61
+ gap={8}
62
+ items={items}
63
+ draggable={false}
64
+ getId={(item) => item.post.id}
65
+ getScrollElement={() => viewport}
66
+ estimateSize={() => 120}
67
+ />
68
+ </ScrollArea.Viewport>
69
+ </ScrollArea.Root>
70
+ </Mosaic.Container>
71
+ </Focus.Group>
72
+ );
73
+ },
74
+ );
75
+
76
+ PostStack.displayName = 'PostStack';
77
+
78
+ type PostTileData = {
79
+ post: Subscription.Post;
80
+ onAction?: PostStackActionHandler;
81
+ };
82
+
83
+ type PostTileProps = Pick<MosaicTileProps<PostTileData>, 'data' | 'location' | 'current'>;
84
+
85
+ const PostTile = forwardRef<HTMLDivElement, PostTileProps>(({ data, location, current }, forwardedRef) => {
86
+ const post = data?.post;
87
+ const { setCurrentId } = useMosaicContainer('PostTile');
88
+ const { t } = useTranslation(Type.getTypename(Subscription.Post));
89
+
90
+ const handleCurrentChange = useCallback(() => {
91
+ if (post) {
92
+ setCurrentId(post.id);
93
+ }
94
+ }, [post, setCurrentId]);
95
+
96
+ if (!post) {
97
+ return null;
98
+ }
99
+
100
+ const published = post.published ? new Date(post.published).toLocaleDateString() : undefined;
101
+
102
+ return (
103
+ <Mosaic.Tile asChild classNames='dx-hover dx-current' id={post.id} data={data} location={location}>
104
+ <Focus.Item asChild current={current} onCurrentChange={handleCurrentChange}>
105
+ <Card.Root ref={forwardedRef} fullWidth>
106
+ <Card.Header>
107
+ <Card.Block>
108
+ <Icon icon='ph--dot-outline--regular' />
109
+ </Card.Block>
110
+ <Card.Text classNames='truncate'>{post.title ?? t('post-title.placeholder')}</Card.Text>
111
+ {post.link && (
112
+ <Card.Block end>
113
+ <a href={post.link} target='_blank' rel='noreferrer' className='shrink-0'>
114
+ <Icon icon='ph--arrow-square-out--regular' size={4} />
115
+ </a>
116
+ </Card.Block>
117
+ )}
118
+ </Card.Header>
119
+ <Card.Body>
120
+ {post.author && (
121
+ <Card.Row>
122
+ <Card.Block>
123
+ <Icon icon='ph--user--regular' />
124
+ </Card.Block>
125
+ <Card.Text variant='description'>{post.author}</Card.Text>
126
+ </Card.Row>
127
+ )}
128
+ {(post.description || post.content) && (
129
+ <Card.Row>
130
+ <MarkdownView
131
+ content={post.description ?? post.content}
132
+ classNames='line-clamp-5 text-sm text-description'
133
+ />
134
+ </Card.Row>
135
+ )}
136
+ {published && (
137
+ <Card.Row>
138
+ <Card.Block>
139
+ <Icon icon='ph--calendar--regular' />
140
+ </Card.Block>
141
+ <Card.Text variant='description'>{published}</Card.Text>
142
+ </Card.Row>
143
+ )}
144
+ </Card.Body>
145
+ </Card.Root>
146
+ </Focus.Item>
147
+ </Mosaic.Tile>
148
+ );
149
+ });
150
+
151
+ PostTile.displayName = 'PostTile';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './PostStack';
@@ -0,0 +1,52 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React, { useMemo, useState } from 'react';
7
+
8
+ import { random } from '@dxos/random';
9
+ import { withAttention } from '@dxos/react-ui-attention/testing';
10
+ import { withMosaic } from '@dxos/react-ui-mosaic/testing';
11
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
+
13
+ import { Subscription } from '#types';
14
+
15
+ import { SubscriptionStack, type SubscriptionStackAction } from './SubscriptionStack';
16
+
17
+ const generateFeeds = (count: number): Subscription.Subscription[] =>
18
+ Array.from({ length: count }, () =>
19
+ Subscription.makeSubscription({
20
+ name: random.company.name() + ' Blog',
21
+ url: random.internet.url(),
22
+ description: random.lorem.sentence(),
23
+ }),
24
+ );
25
+
26
+ const DefaultStory = () => {
27
+ const feeds = useMemo(() => generateFeeds(10), []);
28
+ const [currentId, setCurrentId] = useState<string>();
29
+
30
+ const handleAction = (action: SubscriptionStackAction) => {
31
+ if (action.type === 'current') {
32
+ setCurrentId(action.feedId);
33
+ }
34
+ };
35
+
36
+ return <SubscriptionStack id='story' feeds={feeds} currentId={currentId} onAction={handleAction} />;
37
+ };
38
+
39
+ const meta: Meta<typeof DefaultStory> = {
40
+ title: 'plugins/plugin-magazine/components/SubscriptionStack',
41
+ component: DefaultStory,
42
+ decorators: [withTheme(), withLayout({ layout: 'column' }), withAttention(), withMosaic()],
43
+ parameters: {
44
+ layout: 'fullscreen',
45
+ },
46
+ };
47
+
48
+ export default meta;
49
+
50
+ type Story = StoryObj<typeof meta>;
51
+
52
+ export const Default: Story = {};
@@ -0,0 +1,161 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { type KeyboardEvent, forwardRef, useCallback, useMemo, useState } from 'react';
6
+
7
+ import { Card, Icon, ScrollArea } from '@dxos/react-ui';
8
+ import { composable, composableProps } from '@dxos/react-ui';
9
+ import { Focus, Mosaic, type MosaicTileProps, useMosaicContainer } from '@dxos/react-ui-mosaic';
10
+
11
+ import { type Subscription } from '#types';
12
+
13
+ export type SubscriptionStackAction =
14
+ | { type: 'current'; feedId: string }
15
+ | { type: 'sync'; feedId: string }
16
+ | { type: 'delete'; feedId: string };
17
+
18
+ export type SubscriptionStackActionHandler = (action: SubscriptionStackAction) => void;
19
+
20
+ //
21
+ // SubscriptionStack
22
+ //
23
+
24
+ export type SubscriptionStackProps = {
25
+ id?: string;
26
+ feeds?: Subscription.Subscription[];
27
+ currentId?: string;
28
+ onAction?: SubscriptionStackActionHandler;
29
+ };
30
+
31
+ export const SubscriptionStack = composable<HTMLDivElement, SubscriptionStackProps>(
32
+ ({ feeds = [], currentId, onAction, ...props }, forwardedRef) => {
33
+ const [viewport, setViewport] = useState<HTMLElement | null>(null);
34
+
35
+ // TODO(burdon): Either sort or make draggable.
36
+ const items = useMemo(() => feeds.map((feed) => ({ feed, onAction })), [feeds, onAction]);
37
+
38
+ const handleCurrentChange = useCallback(
39
+ (id: string | undefined) => {
40
+ if (id) {
41
+ onAction?.({ type: 'current', feedId: id });
42
+ }
43
+ },
44
+ [onAction],
45
+ );
46
+
47
+ const handleKeyDown = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
48
+ if (event.key === 'Enter') {
49
+ event.preventDefault();
50
+ (document.activeElement as HTMLElement | null)?.click();
51
+ }
52
+ }, []);
53
+
54
+ return (
55
+ <Focus.Group asChild {...composableProps(props)} onKeyDown={handleKeyDown} ref={forwardedRef}>
56
+ <Mosaic.Container
57
+ asChild
58
+ withFocus
59
+ autoScroll={viewport}
60
+ currentId={currentId}
61
+ onCurrentChange={handleCurrentChange}
62
+ >
63
+ <ScrollArea.Root orientation='vertical' padding centered>
64
+ <ScrollArea.Viewport ref={setViewport}>
65
+ <Mosaic.VirtualStack
66
+ Tile={SubscriptionTile}
67
+ classNames='my-2'
68
+ gap={8}
69
+ items={items}
70
+ draggable={false}
71
+ getId={(item) => item.feed.id}
72
+ getScrollElement={() => viewport}
73
+ estimateSize={() => 100}
74
+ />
75
+ </ScrollArea.Viewport>
76
+ </ScrollArea.Root>
77
+ </Mosaic.Container>
78
+ </Focus.Group>
79
+ );
80
+ },
81
+ );
82
+
83
+ SubscriptionStack.displayName = 'SubscriptionStack';
84
+
85
+ //
86
+ // SubscriptionTile
87
+ //
88
+
89
+ const icons: Record<Subscription.FeedType, { icon: string; className?: string }> = {
90
+ 'standard-site': {
91
+ icon: 'ph--article--regular',
92
+ className: 'text-sky-500',
93
+ },
94
+ 'rss': {
95
+ icon: 'ph--rss--regular',
96
+ className: 'text-green-500',
97
+ },
98
+ 'bluesky': {
99
+ icon: 'ph--butterfly--regular',
100
+ className: 'text-blue-500',
101
+ },
102
+ };
103
+
104
+ type SubscriptionTileData = {
105
+ feed: Subscription.Subscription;
106
+ onAction?: SubscriptionStackActionHandler;
107
+ };
108
+
109
+ type SubscriptionTileProps = Pick<MosaicTileProps<SubscriptionTileData>, 'data' | 'location' | 'current'>;
110
+
111
+ const SubscriptionTile = forwardRef<HTMLDivElement, SubscriptionTileProps>(
112
+ ({ data, location, current }, forwardedRef) => {
113
+ const { feed, onAction } = data;
114
+ const { setCurrentId } = useMosaicContainer('SubscriptionTile');
115
+ const { icon, className: iconClassName } = icons[feed.type ?? 'rss'] || icons.rss;
116
+
117
+ const handleCurrentChange = useCallback(() => {
118
+ setCurrentId(feed.id);
119
+ }, [feed.id, setCurrentId]);
120
+
121
+ const menuItems = useMemo(
122
+ () => [
123
+ { label: 'Sync', onClick: () => onAction?.({ type: 'sync', feedId: feed.id }) },
124
+ { label: 'Delete', onClick: () => onAction?.({ type: 'delete', feedId: feed.id }) },
125
+ ],
126
+ [feed.id, onAction],
127
+ );
128
+
129
+ return (
130
+ <Mosaic.Tile asChild classNames='dx-hover dx-current' id={feed.id} data={data} location={location}>
131
+ <Focus.Item asChild current={current} onCurrentChange={handleCurrentChange}>
132
+ <Card.Root ref={forwardedRef}>
133
+ <Card.Header>
134
+ <Card.Block>
135
+ <Icon icon={icon} classNames={iconClassName} />
136
+ </Card.Block>
137
+ <Card.Title>{feed.name ?? 'Untitled feed'}</Card.Title>
138
+ <Card.Menu items={menuItems} />
139
+ </Card.Header>
140
+ <Card.Body>
141
+ {/* {feed.url && (
142
+ <Card.Row>
143
+ <Card.Text classNames='truncate' variant='description'>
144
+ {feed.url}
145
+ </Card.Text>
146
+ </Card.Row>
147
+ )} */}
148
+ {feed.description && (
149
+ <Card.Row>
150
+ <Card.Text variant='description'>{feed.description}</Card.Text>
151
+ </Card.Row>
152
+ )}
153
+ </Card.Body>
154
+ </Card.Root>
155
+ </Focus.Item>
156
+ </Mosaic.Tile>
157
+ );
158
+ },
159
+ );
160
+
161
+ SubscriptionTile.displayName = 'SubscriptionTile';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './SubscriptionStack';
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './PostContent';
6
+ export * from './PostStack';
7
+ export * from './SubscriptionStack';