@dxos/plugin-magazine 0.10.0 → 0.11.1

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 (242) hide show
  1. package/dist/lib/MagazinePlugin.mjs +36 -0
  2. package/dist/lib/MagazinePlugin.mjs.map +1 -0
  3. package/dist/lib/MagazinePlugin.workerd.mjs +26 -0
  4. package/dist/lib/MagazinePlugin.workerd.mjs.map +1 -0
  5. package/dist/lib/atoms.mjs +175 -0
  6. package/dist/lib/atoms.mjs.map +1 -0
  7. package/dist/lib/capabilities.mjs +12 -0
  8. package/dist/lib/capabilities.mjs.map +1 -0
  9. package/dist/lib/chunk-FeedArticle.mjs +70 -0
  10. package/dist/lib/chunk-FeedArticle.mjs.map +1 -0
  11. package/dist/lib/chunk-FeedProperties.mjs +78 -0
  12. package/dist/lib/chunk-FeedProperties.mjs.map +1 -0
  13. package/dist/lib/chunk-MagazineArticle.mjs +267 -0
  14. package/dist/lib/chunk-MagazineArticle.mjs.map +1 -0
  15. package/dist/lib/chunk-PostArticle.mjs +174 -0
  16. package/dist/lib/chunk-PostArticle.mjs.map +1 -0
  17. package/dist/lib/chunk-PostCard.mjs +62 -0
  18. package/dist/lib/chunk-PostCard.mjs.map +1 -0
  19. package/dist/lib/chunk-app-graph-builder.mjs +91 -0
  20. package/dist/lib/chunk-app-graph-builder.mjs.map +1 -0
  21. package/dist/lib/chunk-clear-magazine.mjs +37 -0
  22. package/dist/lib/chunk-clear-magazine.mjs.map +1 -0
  23. package/dist/lib/chunk-create-object.mjs +59 -0
  24. package/dist/lib/chunk-create-object.mjs.map +1 -0
  25. package/dist/lib/chunk-curate-magazine.mjs +164 -0
  26. package/dist/lib/chunk-curate-magazine.mjs.map +1 -0
  27. package/dist/lib/chunk-date.mjs +25 -0
  28. package/dist/lib/chunk-date.mjs.map +1 -0
  29. package/dist/lib/chunk-fetch-article-content.mjs +26 -0
  30. package/dist/lib/chunk-fetch-article-content.mjs.map +1 -0
  31. package/dist/lib/chunk-load-post-content.mjs +57 -0
  32. package/dist/lib/chunk-load-post-content.mjs.map +1 -0
  33. package/dist/lib/{neutral/chunk-JFPR6OV4.mjs → chunk-meta.mjs} +19 -26
  34. package/dist/lib/chunk-meta.mjs.map +1 -0
  35. package/dist/lib/chunk-operation-handler.mjs +13 -0
  36. package/dist/lib/chunk-operation-handler.mjs.map +1 -0
  37. package/dist/lib/chunk-paths.mjs +8 -0
  38. package/dist/lib/chunk-paths.mjs.map +1 -0
  39. package/dist/lib/chunk-post-content.mjs +14 -0
  40. package/dist/lib/chunk-post-content.mjs.map +1 -0
  41. package/dist/lib/chunk-react-surface.mjs +60 -0
  42. package/dist/lib/chunk-react-surface.mjs.map +1 -0
  43. package/dist/lib/chunk-routine-templates.mjs +52 -0
  44. package/dist/lib/chunk-routine-templates.mjs.map +1 -0
  45. package/dist/lib/chunk-skill-definition.mjs +12 -0
  46. package/dist/lib/chunk-skill-definition.mjs.map +1 -0
  47. package/dist/lib/chunk-sources.mjs +576 -0
  48. package/dist/lib/chunk-sources.mjs.map +1 -0
  49. package/dist/lib/chunk-sync-feed.mjs +86 -0
  50. package/dist/lib/chunk-sync-feed.mjs.map +1 -0
  51. package/dist/lib/chunk-text.mjs +52 -0
  52. package/dist/lib/chunk-text.mjs.map +1 -0
  53. package/dist/lib/chunk-types.mjs +607 -0
  54. package/dist/lib/chunk-types.mjs.map +1 -0
  55. package/dist/lib/components.mjs +323 -0
  56. package/dist/lib/components.mjs.map +1 -0
  57. package/dist/lib/containers.mjs +11 -0
  58. package/dist/lib/containers.mjs.map +1 -0
  59. package/dist/lib/index.mjs +3 -0
  60. package/dist/lib/meta.mjs +2 -0
  61. package/dist/lib/operations.mjs +7 -0
  62. package/dist/lib/operations.mjs.map +1 -0
  63. package/dist/lib/plugin.mjs +9 -0
  64. package/dist/lib/plugin.mjs.map +1 -0
  65. package/dist/lib/plugin.workerd.mjs +3 -0
  66. package/dist/lib/{neutral/skills/index.mjs → skills.mjs} +15 -28
  67. package/dist/lib/skills.mjs.map +1 -0
  68. package/dist/lib/testing.mjs +71 -0
  69. package/dist/lib/testing.mjs.map +1 -0
  70. package/dist/lib/translations.mjs +74 -0
  71. package/dist/lib/translations.mjs.map +1 -0
  72. package/dist/lib/types.mjs +2 -0
  73. package/dist/types/src/MagazinePlugin.d.ts.map +1 -1
  74. package/dist/types/src/capabilities/app-graph-builder.d.ts +1 -1
  75. package/dist/types/src/capabilities/create-object.d.ts +1 -1
  76. package/dist/types/src/capabilities/index.d.ts +0 -1
  77. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  78. package/dist/types/src/capabilities/operation-handler.d.ts +1 -1
  79. package/dist/types/src/capabilities/react-surface.d.ts +1 -1
  80. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  81. package/dist/types/src/capabilities/routine-templates.d.ts +1 -1
  82. package/dist/types/src/components/PostContent/PostContent.d.ts +1 -13
  83. package/dist/types/src/components/PostContent/PostContent.d.ts.map +1 -1
  84. package/dist/types/src/components/PostContent/dedupe-images.d.ts +13 -0
  85. package/dist/types/src/components/PostContent/dedupe-images.d.ts.map +1 -0
  86. package/dist/types/src/components/PostContent/index.d.ts +1 -0
  87. package/dist/types/src/components/PostContent/index.d.ts.map +1 -1
  88. package/dist/types/src/components/PostStack/PostStack.d.ts +1 -1
  89. package/dist/types/src/components/PostStack/PostStack.d.ts.map +1 -1
  90. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts +1 -1
  91. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts.map +1 -1
  92. package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts +4 -1
  93. package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts.map +1 -1
  94. package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts +4 -1
  95. package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts.map +1 -1
  96. package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts +4 -1
  97. package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts.map +1 -1
  98. package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts.map +1 -1
  99. package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts +4 -1
  100. package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts.map +1 -1
  101. package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts +4 -8
  102. package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts.map +1 -1
  103. package/dist/types/src/containers/PostArticle/PostArticle.d.ts +4 -1
  104. package/dist/types/src/containers/PostArticle/PostArticle.d.ts.map +1 -1
  105. package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts.map +1 -1
  106. package/dist/types/src/containers/PostArticle/PostToolbar.d.ts +1 -1
  107. package/dist/types/src/containers/PostArticle/PostToolbar.d.ts.map +1 -1
  108. package/dist/types/src/containers/PostCard/PostCard.d.ts +4 -6
  109. package/dist/types/src/containers/PostCard/PostCard.d.ts.map +1 -1
  110. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts +4 -1
  111. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts.map +1 -1
  112. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts.map +1 -1
  113. package/dist/types/src/stories/ArticleExtractor.stories.d.ts +1 -0
  114. package/dist/types/src/stories/ArticleExtractor.stories.d.ts.map +1 -1
  115. package/dist/types/src/stories/MagazineCurate.stories.d.ts.map +1 -1
  116. package/dist/types/src/templates/magazine-curation.d.ts.map +1 -1
  117. package/dist/types/src/translations.d.ts +1 -0
  118. package/dist/types/src/translations.d.ts.map +1 -1
  119. package/dist/types/tsconfig.tsbuildinfo +1 -1
  120. package/dx.config.ts +1 -1
  121. package/package.json +57 -60
  122. package/src/MagazinePlugin.tsx +0 -3
  123. package/src/atoms/atoms.test.ts +5 -4
  124. package/src/capabilities/app-graph-builder.ts +8 -6
  125. package/src/capabilities/index.ts +0 -1
  126. package/src/capabilities/{react-surface.tsx → react-surface.ts} +10 -12
  127. package/src/components/PostContent/PostContent.test.ts +1 -1
  128. package/src/components/PostContent/PostContent.tsx +1 -41
  129. package/src/components/PostContent/dedupe-images.ts +48 -0
  130. package/src/components/PostContent/index.ts +1 -0
  131. package/src/containers/FeedArticle/FeedArticle.stories.tsx +2 -2
  132. package/src/containers/FeedArticle/FeedArticle.tsx +4 -2
  133. package/src/containers/FeedProperties/FeedProperties.tsx +6 -4
  134. package/src/containers/MagazineArticle/MagazineArticle.stories.tsx +2 -1
  135. package/src/containers/MagazineArticle/MagazineArticle.tsx +8 -6
  136. package/src/containers/MagazineArticle/MagazineTile.tsx +2 -0
  137. package/src/containers/MagazineArticle/useToolbar.tsx +2 -0
  138. package/src/containers/PostArticle/PostArticle.stories.tsx +2 -1
  139. package/src/containers/PostArticle/PostArticle.tsx +6 -3
  140. package/src/containers/PostArticle/PostToolbar.tsx +1 -1
  141. package/src/containers/PostCard/PostCard.tsx +3 -1
  142. package/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.tsx +2 -1
  143. package/src/containers/SubscriptionsArticle/SubscriptionsArticle.tsx +5 -3
  144. package/src/operations/curate-magazine.test.ts +108 -4
  145. package/src/operations/sync-feed.test.ts +1 -1
  146. package/src/operations/sync-feed.ts +1 -1
  147. package/src/paths.ts +3 -3
  148. package/src/stories/ArticleExtractor.stories.tsx +1 -1
  149. package/src/stories/MagazineCurate.stories.tsx +3 -2
  150. package/src/templates/magazine-curation.ts +3 -2
  151. package/src/types/Magazine.test.ts +1 -1
  152. package/src/types/Subscription.ts +1 -1
  153. package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs +0 -90
  154. package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs.map +0 -7
  155. package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs +0 -97
  156. package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs.map +0 -7
  157. package/dist/lib/neutral/MagazineArticle-5T7RTIPS.mjs +0 -325
  158. package/dist/lib/neutral/MagazineArticle-5T7RTIPS.mjs.map +0 -7
  159. package/dist/lib/neutral/MagazinePlugin.mjs +0 -62
  160. package/dist/lib/neutral/MagazinePlugin.mjs.map +0 -7
  161. package/dist/lib/neutral/MagazinePlugin.workerd.mjs +0 -12
  162. package/dist/lib/neutral/MagazinePlugin.workerd.mjs.map +0 -7
  163. package/dist/lib/neutral/PostArticle-BGIPU7XA.mjs +0 -230
  164. package/dist/lib/neutral/PostArticle-BGIPU7XA.mjs.map +0 -7
  165. package/dist/lib/neutral/PostCard-VDGN7BBA.mjs +0 -60
  166. package/dist/lib/neutral/PostCard-VDGN7BBA.mjs.map +0 -7
  167. package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs +0 -121
  168. package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs.map +0 -7
  169. package/dist/lib/neutral/capabilities/index.mjs +0 -21
  170. package/dist/lib/neutral/capabilities/index.mjs.map +0 -7
  171. package/dist/lib/neutral/chunk-3K4AQKBZ.mjs +0 -12
  172. package/dist/lib/neutral/chunk-3K4AQKBZ.mjs.map +0 -7
  173. package/dist/lib/neutral/chunk-AFPBWBTK.mjs +0 -591
  174. package/dist/lib/neutral/chunk-AFPBWBTK.mjs.map +0 -7
  175. package/dist/lib/neutral/chunk-APQRTYT3.mjs +0 -57
  176. package/dist/lib/neutral/chunk-APQRTYT3.mjs.map +0 -7
  177. package/dist/lib/neutral/chunk-GU725ZF4.mjs +0 -11
  178. package/dist/lib/neutral/chunk-GU725ZF4.mjs.map +0 -7
  179. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
  180. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
  181. package/dist/lib/neutral/chunk-JFPR6OV4.mjs.map +0 -7
  182. package/dist/lib/neutral/chunk-KTYLUOUG.mjs +0 -36
  183. package/dist/lib/neutral/chunk-KTYLUOUG.mjs.map +0 -7
  184. package/dist/lib/neutral/chunk-LFSQMLPK.mjs +0 -8
  185. package/dist/lib/neutral/chunk-LFSQMLPK.mjs.map +0 -7
  186. package/dist/lib/neutral/chunk-MBPX7RPI.mjs +0 -14
  187. package/dist/lib/neutral/chunk-MBPX7RPI.mjs.map +0 -7
  188. package/dist/lib/neutral/chunk-OYCDRSWB.mjs +0 -624
  189. package/dist/lib/neutral/chunk-OYCDRSWB.mjs.map +0 -7
  190. package/dist/lib/neutral/chunk-SAWKLGJU.mjs +0 -28
  191. package/dist/lib/neutral/chunk-SAWKLGJU.mjs.map +0 -7
  192. package/dist/lib/neutral/chunk-XCNCK47N.mjs +0 -15
  193. package/dist/lib/neutral/chunk-XCNCK47N.mjs.map +0 -7
  194. package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs +0 -59
  195. package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs.map +0 -7
  196. package/dist/lib/neutral/components/index.mjs +0 -338
  197. package/dist/lib/neutral/components/index.mjs.map +0 -7
  198. package/dist/lib/neutral/containers/index.mjs +0 -17
  199. package/dist/lib/neutral/containers/index.mjs.map +0 -7
  200. package/dist/lib/neutral/create-object-SYZMRR4H.mjs +0 -98
  201. package/dist/lib/neutral/create-object-SYZMRR4H.mjs.map +0 -7
  202. package/dist/lib/neutral/curate-magazine-7RMKSJC5.mjs +0 -184
  203. package/dist/lib/neutral/curate-magazine-7RMKSJC5.mjs.map +0 -7
  204. package/dist/lib/neutral/fetch-article-content-CXPKXIMR.mjs +0 -31
  205. package/dist/lib/neutral/fetch-article-content-CXPKXIMR.mjs.map +0 -7
  206. package/dist/lib/neutral/index.mjs +0 -18
  207. package/dist/lib/neutral/index.mjs.map +0 -7
  208. package/dist/lib/neutral/load-post-content-H3S4TKVM.mjs +0 -65
  209. package/dist/lib/neutral/load-post-content-H3S4TKVM.mjs.map +0 -7
  210. package/dist/lib/neutral/meta.json +0 -1
  211. package/dist/lib/neutral/meta.mjs +0 -8
  212. package/dist/lib/neutral/meta.mjs.map +0 -7
  213. package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs +0 -18
  214. package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs.map +0 -7
  215. package/dist/lib/neutral/operation-handler-JE3743BU.mjs +0 -8
  216. package/dist/lib/neutral/operation-handler-JE3743BU.mjs.map +0 -7
  217. package/dist/lib/neutral/operations/index.mjs +0 -8
  218. package/dist/lib/neutral/operations/index.mjs.map +0 -7
  219. package/dist/lib/neutral/plugin.mjs +0 -16
  220. package/dist/lib/neutral/plugin.mjs.map +0 -7
  221. package/dist/lib/neutral/plugin.workerd.mjs +0 -16
  222. package/dist/lib/neutral/plugin.workerd.mjs.map +0 -7
  223. package/dist/lib/neutral/react-surface-TZESAMG2.mjs +0 -60
  224. package/dist/lib/neutral/react-surface-TZESAMG2.mjs.map +0 -7
  225. package/dist/lib/neutral/routine-templates-D5A2L4KL.mjs +0 -52
  226. package/dist/lib/neutral/routine-templates-D5A2L4KL.mjs.map +0 -7
  227. package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs +0 -8
  228. package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs.map +0 -7
  229. package/dist/lib/neutral/skills/index.mjs.map +0 -7
  230. package/dist/lib/neutral/sync-feed-SDXYF7HU.mjs +0 -99
  231. package/dist/lib/neutral/sync-feed-SDXYF7HU.mjs.map +0 -7
  232. package/dist/lib/neutral/translations.mjs +0 -81
  233. package/dist/lib/neutral/translations.mjs.map +0 -7
  234. package/dist/lib/neutral/types/index.mjs +0 -14
  235. package/dist/lib/neutral/types/index.mjs.map +0 -7
  236. package/dist/types/src/capabilities/navigation-resolver.d.ts +0 -5
  237. package/dist/types/src/capabilities/navigation-resolver.d.ts.map +0 -1
  238. package/dist/types/src/operations/curate-magazine.skill.test.d.ts +0 -2
  239. package/dist/types/src/operations/curate-magazine.skill.test.d.ts.map +0 -1
  240. package/src/capabilities/navigation-resolver.ts +0 -21
  241. package/src/operations/curate-magazine.skill.conversations.json +0 -1
  242. package/src/operations/curate-magazine.skill.test.ts +0 -167
@@ -0,0 +1,267 @@
1
+ import { t as formatDate } from "./chunk-date.mjs";
2
+ import { Obj, Ref } from "@dxos/echo";
3
+ import * as Effect from "effect/Effect";
4
+ import { EffectEx } from "@dxos/effect";
5
+ import { meta } from "#meta";
6
+ import { GraphPath, LayoutOperation } from "@dxos/app-toolkit";
7
+ import { FeedOperation, Subscription } from "#types";
8
+ import { useCallback, useContext, useEffect, useMemo } from "react";
9
+ import { Card, Focus, Panel, SystemIconButton, useTranslation } from "@dxos/react-ui";
10
+ import { useMagazinePostData, useVisibleMagazinePosts } from "#atoms";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ import { Atom, RegistryContext, useAtomSet, useAtomValue } from "@effect-atom/atom-react";
13
+ import { Attention, useSelection } from "@dxos/react-ui-attention";
14
+ import { useOperationInvoker } from "@dxos/app-framework/ui";
15
+ import { useShowItem } from "@dxos/app-toolkit/ui";
16
+ import { useObject } from "@dxos/echo-react";
17
+ import { Menu, MenuBuilder, useMenuBuilder } from "@dxos/react-ui-menu";
18
+ import { log } from "@dxos/log";
19
+ import { Masonry } from "@dxos/react-ui-masonry";
20
+ import { mx } from "@dxos/ui-theme";
21
+ //#region src/containers/MagazineArticle/MagazineTile.tsx
22
+ var MagazineTile = ({ post, magazine, current, onToggleStar, onOpen }) => {
23
+ const { post: snapshot, feedName, read, starred, snippet, imageUrl } = useMagazinePostData(post, magazine);
24
+ const handleCurrentChange = useCallback(() => {
25
+ onOpen?.(post);
26
+ }, [onOpen, post]);
27
+ const handleToggleStar = useCallback((event) => {
28
+ event.stopPropagation();
29
+ onToggleStar?.(post, starred);
30
+ }, [
31
+ onToggleStar,
32
+ post,
33
+ starred
34
+ ]);
35
+ return /* @__PURE__ */ jsx(Focus.Item, {
36
+ asChild: true,
37
+ current,
38
+ onCurrentChange: handleCurrentChange,
39
+ children: /* @__PURE__ */ jsxs(Card.Root, {
40
+ fullWidth: true,
41
+ classNames: mx("dx-hover dx-current cursor-pointer transition-opacity", read && !current && "opacity-60"),
42
+ children: [
43
+ imageUrl && /* @__PURE__ */ jsx(Card.Poster, {
44
+ alt: snapshot.title ?? "Article",
45
+ image: imageUrl,
46
+ fit: "cover",
47
+ classNames: "rounded-t-xs"
48
+ }),
49
+ /* @__PURE__ */ jsxs(Card.Header, { children: [
50
+ /* @__PURE__ */ jsx(Card.Block, { children: /* @__PURE__ */ jsx(SystemIconButton.Star, {
51
+ variant: "ghost",
52
+ iconOnly: true,
53
+ square: true,
54
+ size: 4,
55
+ active: starred,
56
+ onClick: handleToggleStar
57
+ }) }),
58
+ snapshot.title ? /* @__PURE__ */ jsx(Card.Title, {
59
+ classNames: "line-clamp-2",
60
+ children: snapshot.title
61
+ }) : /* @__PURE__ */ jsx("div", {}),
62
+ /* @__PURE__ */ jsx(Card.Block, { end: true })
63
+ ] }),
64
+ /* @__PURE__ */ jsxs(Card.Body, { children: [snippet && /* @__PURE__ */ jsx(Card.Row, { children: /* @__PURE__ */ jsx(Card.Text, {
65
+ variant: "description",
66
+ classNames: "line-clamp-3",
67
+ children: snippet
68
+ }) }), /* @__PURE__ */ jsx(Card.Row, { children: /* @__PURE__ */ jsxs("div", {
69
+ className: "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-2 py-1.5 text-sm text-description overflow-hidden",
70
+ children: [/* @__PURE__ */ jsx("span", {
71
+ className: "truncate",
72
+ children: feedName ?? ""
73
+ }), /* @__PURE__ */ jsx("span", {
74
+ className: "text-end shrink-0",
75
+ children: formatPublished(snapshot) ?? ""
76
+ })]
77
+ }) })] })
78
+ ]
79
+ })
80
+ });
81
+ };
82
+ /** Convenience: format a Post's published date the way the magazine view shows it. */
83
+ var formatPublished = (post) => post.published ? formatDate(post.published) : void 0;
84
+ MagazineTile.displayName = "MagazineTile";
85
+ //#endregion
86
+ //#region src/containers/MagazineArticle/useToolbar.tsx
87
+ /**
88
+ * Owns the magazine toolbar: the view-filter and curate-busy atoms, the Curate/Clear operation
89
+ * handlers, and the reactive menu action graph. The action graph reads its state — view filter,
90
+ * curate-busy flag, and whether the magazine has feeds — via `get` inside the builder, so it
91
+ * subscribes to those atoms and rebuilds whenever they change. Returns the menu props (spread into
92
+ * `Menu.Root`) plus `viewAtom`, which the article also reads to filter the visible posts.
93
+ */
94
+ var useToolbar = ({ magazine }) => {
95
+ const invoker = useOperationInvoker();
96
+ const registry = useContext(RegistryContext);
97
+ const db = Obj.getDatabase(magazine);
98
+ const viewAtom = useMemo(() => Atom.make("default"), []);
99
+ const busyAtom = useMemo(() => Atom.make(false), []);
100
+ const setView = useAtomSet(viewAtom);
101
+ const runExclusive = useCallback((effect) => {
102
+ if (registry.get(busyAtom)) return;
103
+ registry.set(busyAtom, true);
104
+ EffectEx.runAndForwardErrors(effect.pipe(Effect.ensuring(Effect.sync(() => registry.set(busyAtom, false)))));
105
+ }, [registry, busyAtom]);
106
+ const handleCurate = useCallback(() => runExclusive(invoker.invoke(FeedOperation.CurateMagazine, { magazine: Ref.make(magazine) }, {
107
+ spaceId: db?.spaceId,
108
+ notify: { error: ["curate-error.message", { ns: meta.profile.key }] }
109
+ })), [
110
+ runExclusive,
111
+ invoker,
112
+ magazine,
113
+ db
114
+ ]);
115
+ const handleClear = useCallback(() => runExclusive(invoker.invoke(FeedOperation.ClearMagazine, { magazine: Ref.make(magazine) }, {
116
+ spaceId: db?.spaceId,
117
+ notify: { error: ["clear-error.message", { ns: meta.profile.key }] }
118
+ })), [
119
+ runExclusive,
120
+ invoker,
121
+ magazine,
122
+ db
123
+ ]);
124
+ return {
125
+ menu: useMenuBuilder((get) => {
126
+ const view = get(viewAtom);
127
+ const busy = get(busyAtom);
128
+ const hasFeeds = (get(Obj.atomProperty(magazine, "feeds")) ?? []).length > 0;
129
+ const hasRoutine = Boolean(get(Obj.atomProperty(magazine, "instructions")));
130
+ const curateLabel = !hasFeeds ? "no-feeds.label" : busy ? "refreshing-magazine.label" : "curate.label";
131
+ return MenuBuilder.make().group("view", {
132
+ label: ["view-filter.label", { ns: meta.profile.key }],
133
+ variant: "toggleGroup",
134
+ selectCardinality: "single",
135
+ value: view
136
+ }, (group) => {
137
+ group.action("starred", {
138
+ label: ["only-starred.label", { ns: meta.profile.key }],
139
+ icon: view === "starred" ? "ph--star--fill" : "ph--star--regular"
140
+ }, () => setView(view === "starred" ? "default" : "starred"));
141
+ group.action("archived", {
142
+ label: ["show-archived.label", { ns: meta.profile.key }],
143
+ icon: "ph--archive--regular"
144
+ }, () => setView(view === "archived" ? "default" : "archived"));
145
+ }).separator("gap").action("clear", {
146
+ label: ["clear-magazine.label", { ns: meta.profile.key }],
147
+ icon: "ph--broom--regular",
148
+ iconOnly: true,
149
+ disabled: busy
150
+ }, handleClear).action("curate", {
151
+ label: [curateLabel, { ns: meta.profile.key }],
152
+ icon: busy ? "ph--circle-notch--regular" : "ph--sparkle--regular",
153
+ iconClassNames: busy ? "animate-spin" : void 0,
154
+ iconOnly: true,
155
+ disabled: busy || !hasFeeds || !hasRoutine
156
+ }, handleCurate).build();
157
+ }, [
158
+ magazine,
159
+ viewAtom,
160
+ busyAtom,
161
+ setView,
162
+ handleClear,
163
+ handleCurate
164
+ ]),
165
+ viewAtom
166
+ };
167
+ };
168
+ useToolbar.displayName = "useToolbar";
169
+ //#endregion
170
+ //#region src/containers/MagazineArticle/MagazineArticle.tsx
171
+ var __dxlog_file = "/__w/dxos/dxos/packages/plugins/plugin-magazine/src/containers/MagazineArticle/MagazineArticle.tsx";
172
+ var MagazineArticle = ({ role, subject, attendableId }) => {
173
+ const { t } = useTranslation(meta.profile.key);
174
+ const invoker = useOperationInvoker();
175
+ const [magazine] = useObject(subject);
176
+ const { menu, viewAtom } = useToolbar({ magazine: subject });
177
+ const view = useAtomValue(viewAtom);
178
+ const showItem = useShowItem();
179
+ const id = attendableId ?? Obj.getURI(magazine);
180
+ const currentId = useSelection(id, "single");
181
+ const db = Obj.getDatabase(magazine);
182
+ const posts = useVisibleMagazinePosts(subject, view);
183
+ const handleToggleStar = useCallback(async (post, starred) => {
184
+ const subscription = await post.source?.load();
185
+ if (db && subscription) Subscription.setTag(subscription, post.id, db, "starred", !starred);
186
+ }, [db]);
187
+ const handleOpen = useCallback(async (post) => {
188
+ const subscription = await post.source?.load();
189
+ if (subscription && !Subscription.getReadAt(subscription, post.id)) Subscription.setReadAt(subscription, post.id, (/* @__PURE__ */ new Date()).toISOString());
190
+ if (post.link) invoker.invokePromise(FeedOperation.LoadPostContent, { post: Ref.make(post) }).catch((err) => log.catch(err, { postLink: post.link }, {
191
+ "~LogMeta": "~LogMeta",
192
+ F: __dxlog_file,
193
+ L: 63,
194
+ S: void 0
195
+ }));
196
+ showItem({
197
+ contextId: id,
198
+ selectionId: post.id,
199
+ companion: Attention.linkedSegment("post"),
200
+ path: GraphPath.getObjectPathFromObject(subject)
201
+ });
202
+ }, [
203
+ id,
204
+ showItem,
205
+ invoker,
206
+ subject
207
+ ]);
208
+ const noPosts = posts.length === 0;
209
+ useEffect(() => {
210
+ if (noPosts) invoker.invokePromise(LayoutOperation.UpdateCompanion, { subject: Attention.linkedSegment("settings") });
211
+ }, [noPosts, invoker]);
212
+ const tileItems = useMemo(() => posts.map((post) => ({
213
+ id: post.id,
214
+ post,
215
+ magazine: subject,
216
+ current: currentId === post.id,
217
+ onToggleStar: handleToggleStar,
218
+ onOpen: handleOpen
219
+ })), [
220
+ posts,
221
+ currentId,
222
+ handleOpen,
223
+ handleToggleStar
224
+ ]);
225
+ return /* @__PURE__ */ jsxs(Panel.Root, {
226
+ role,
227
+ children: [/* @__PURE__ */ jsx(Menu.Root, {
228
+ ...menu,
229
+ attendableId,
230
+ children: /* @__PURE__ */ jsx(Panel.Toolbar, {
231
+ asChild: true,
232
+ children: /* @__PURE__ */ jsx(Menu.Toolbar, {})
233
+ })
234
+ }), /* @__PURE__ */ jsx(Panel.Content, { children: noPosts ? /* @__PURE__ */ jsx("div", {
235
+ className: "h-full flex items-center justify-center text-subdued text-sm",
236
+ children: t("empty-magazine.message")
237
+ }) : /* @__PURE__ */ jsx(Masonry.Root, {
238
+ Tile: TileAdapter,
239
+ minColumnWidth: 20,
240
+ maxColumnWidth: 25,
241
+ children: /* @__PURE__ */ jsx(Masonry.Content, {
242
+ thin: true,
243
+ centered: true,
244
+ padding: true,
245
+ children: /* @__PURE__ */ jsx(Masonry.Viewport, {
246
+ classNames: "py-2",
247
+ items: tileItems
248
+ })
249
+ })
250
+ }) })]
251
+ });
252
+ };
253
+ var TileAdapter = ({ data }) => {
254
+ if (!data?.post) return null;
255
+ return /* @__PURE__ */ jsx(MagazineTile, {
256
+ post: data.post,
257
+ magazine: data.magazine,
258
+ current: data.current,
259
+ onOpen: data.onOpen,
260
+ onToggleStar: data.onToggleStar
261
+ });
262
+ };
263
+ MagazineArticle.displayName = "MagazineArticle";
264
+ //#endregion
265
+ export { MagazineArticle as default };
266
+
267
+ //# sourceMappingURL=chunk-MagazineArticle.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-MagazineArticle.mjs","names":[],"sources":["../../src/containers/MagazineArticle/MagazineTile.tsx","../../src/containers/MagazineArticle/useToolbar.tsx","../../src/containers/MagazineArticle/MagazineArticle.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { type MouseEvent, useCallback } from 'react';\n\nimport { Obj } from '@dxos/echo';\nimport { Card, Focus, SystemIconButton } from '@dxos/react-ui';\nimport { mx } from '@dxos/ui-theme';\n\nimport { useMagazinePostData } from '#atoms';\nimport { type Magazine, type Subscription } from '#types';\n\nimport { formatDate } from '../../util/date';\n\nexport type MagazineTileProps = {\n post: Subscription.Post;\n magazine: Magazine.Magazine;\n current?: boolean;\n onOpen?: (post: Subscription.Post) => void;\n onToggleStar?: (post: Subscription.Post, starred: boolean) => void;\n};\n\nexport const MagazineTile = ({ post, magazine, current, onToggleStar, onOpen }: MagazineTileProps) => {\n // All per-Post derivation (snapshot, read, starred, snippet, image, feed name) happens in\n // atom-land; this tile re-renders only when THIS post's slice changes, not when a sibling does.\n const { post: snapshot, feedName, read, starred, snippet, imageUrl } = useMagazinePostData(post, magazine);\n\n // `Focus.Item` calls `onCurrentChange` on click and on Enter.\n const handleCurrentChange = useCallback(() => {\n onOpen?.(post);\n }, [onOpen, post]);\n\n const handleToggleStar = useCallback(\n (event: MouseEvent<HTMLButtonElement>) => {\n // Prevent Focus.Item's onClick from firing the open action — clicks on the star toggle should not open the post.\n event.stopPropagation();\n onToggleStar?.(post, starred);\n },\n [onToggleStar, post, starred],\n );\n\n return (\n <Focus.Item asChild current={current} onCurrentChange={handleCurrentChange}>\n <Card.Root\n fullWidth\n classNames={mx('dx-hover dx-current cursor-pointer transition-opacity', read && !current && 'opacity-60')}\n >\n {imageUrl && (\n <Card.Poster alt={snapshot.title ?? 'Article'} image={imageUrl} fit='cover' classNames='rounded-t-xs' />\n )}\n <Card.Header>\n <Card.Block>\n <SystemIconButton.Star\n variant='ghost'\n iconOnly\n square\n size={4}\n active={starred}\n onClick={handleToggleStar}\n />\n </Card.Block>\n {snapshot.title ? <Card.Title classNames='line-clamp-2'>{snapshot.title}</Card.Title> : <div />}\n <Card.Block end />\n </Card.Header>\n <Card.Body>\n {snippet && (\n <Card.Row>\n <Card.Text variant='description' classNames='line-clamp-3'>\n {snippet}\n </Card.Text>\n </Card.Row>\n )}\n <Card.Row>\n <div className='grid grid-cols-[minmax(0,1fr)_auto] items-center gap-2 py-1.5 text-sm text-description overflow-hidden'>\n <span className='truncate'>{feedName ?? ''}</span>\n <span className='text-end shrink-0'>{formatPublished(snapshot) ?? ''}</span>\n </div>\n </Card.Row>\n </Card.Body>\n </Card.Root>\n </Focus.Item>\n );\n};\n\n/** Convenience: format a Post's published date the way the magazine view shows it. */\nconst formatPublished = (post: Obj.Snapshot<Subscription.Post>): string | undefined =>\n post.published ? formatDate(post.published) : undefined;\n\nMagazineTile.displayName = 'MagazineTile';\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { Atom, RegistryContext, useAtomSet } from '@effect-atom/atom-react';\nimport * as Effect from 'effect/Effect';\nimport { useCallback, useContext, useMemo } from 'react';\n\nimport { useOperationInvoker } from '@dxos/app-framework/ui';\nimport { Obj, Ref } from '@dxos/echo';\nimport { EffectEx } from '@dxos/effect';\nimport { MenuBuilder, useMenuBuilder } from '@dxos/react-ui-menu';\n\nimport { type MagazineView } from '#atoms';\nimport { meta } from '#meta';\nimport { FeedOperation, type Magazine } from '#types';\n\nexport type UseToolbarProps = {\n magazine: Magazine.Magazine;\n};\n\n/**\n * Owns the magazine toolbar: the view-filter and curate-busy atoms, the Curate/Clear operation\n * handlers, and the reactive menu action graph. The action graph reads its state — view filter,\n * curate-busy flag, and whether the magazine has feeds — via `get` inside the builder, so it\n * subscribes to those atoms and rebuilds whenever they change. Returns the menu props (spread into\n * `Menu.Root`) plus `viewAtom`, which the article also reads to filter the visible posts.\n */\nexport const useToolbar = ({ magazine }: UseToolbarProps) => {\n const invoker = useOperationInvoker();\n const registry = useContext(RegistryContext);\n const db = Obj.getDatabase(magazine);\n const viewAtom = useMemo(() => Atom.make<MagazineView>('default'), []);\n const busyAtom = useMemo(() => Atom.make(false), []);\n const setView = useAtomSet(viewAtom);\n\n // Curate and Clear share the busy guard: ignore re-entry while a run is in flight, flip the busy flag\n // for the duration, then run the supplied invocation (failures surface via its own notify config).\n const runExclusive = useCallback(\n <A, E>(effect: Effect.Effect<A, E>) => {\n if (registry.get(busyAtom)) {\n return;\n }\n registry.set(busyAtom, true);\n void EffectEx.runAndForwardErrors(effect.pipe(Effect.ensuring(Effect.sync(() => registry.set(busyAtom, false)))));\n },\n [registry, busyAtom],\n );\n\n const handleCurate = useCallback(\n () =>\n runExclusive(\n invoker.invoke(\n FeedOperation.CurateMagazine,\n { magazine: Ref.make(magazine) },\n { spaceId: db?.spaceId, notify: { error: ['curate-error.message', { ns: meta.profile.key }] } },\n ),\n ),\n [runExclusive, invoker, magazine, db],\n );\n\n const handleClear = useCallback(\n () =>\n runExclusive(\n invoker.invoke(\n FeedOperation.ClearMagazine,\n { magazine: Ref.make(magazine) },\n { spaceId: db?.spaceId, notify: { error: ['clear-error.message', { ns: meta.profile.key }] } },\n ),\n ),\n [runExclusive, invoker, magazine, db],\n );\n\n const menu = useMenuBuilder(\n (get) => {\n const view = get(viewAtom);\n const busy = get(busyAtom);\n const hasFeeds = (get(Obj.atomProperty(magazine, 'feeds')) ?? []).length > 0;\n // Curate runs the magazine's Routine, so it is unavailable until that Routine exists.\n const hasRoutine = Boolean(get(Obj.atomProperty(magazine, 'instructions')));\n // Curate is disabled while busy or until there is at least one feed; the tooltip explains why.\n const curateLabel = !hasFeeds ? 'no-feeds.label' : busy ? 'refreshing-magazine.label' : 'curate.label';\n\n return (\n MenuBuilder.make()\n // Single-select filter: `value` is the active child id; `'default'` matches neither, so both\n // toggles read as off. Re-clicking the active toggle returns to `'default'`.\n .group(\n 'view',\n {\n label: ['view-filter.label', { ns: meta.profile.key }],\n variant: 'toggleGroup',\n selectCardinality: 'single',\n value: view,\n },\n (group) => {\n group.action(\n 'starred',\n {\n label: ['only-starred.label', { ns: meta.profile.key }],\n icon: view === 'starred' ? 'ph--star--fill' : 'ph--star--regular',\n },\n () => setView(view === 'starred' ? 'default' : 'starred'),\n );\n group.action(\n 'archived',\n {\n label: ['show-archived.label', { ns: meta.profile.key }],\n icon: 'ph--archive--regular',\n },\n () => setView(view === 'archived' ? 'default' : 'archived'),\n );\n },\n )\n // `gap` is a flexible spacer that pushes Clear + Curate to the trailing edge.\n .separator('gap')\n .action(\n 'clear',\n {\n label: ['clear-magazine.label', { ns: meta.profile.key }],\n icon: 'ph--broom--regular',\n iconOnly: true,\n disabled: busy,\n },\n handleClear,\n )\n .action(\n 'curate',\n {\n label: [curateLabel, { ns: meta.profile.key }],\n icon: busy ? 'ph--circle-notch--regular' : 'ph--sparkle--regular',\n iconClassNames: busy ? 'animate-spin' : undefined,\n iconOnly: true,\n disabled: busy || !hasFeeds || !hasRoutine,\n },\n handleCurate,\n )\n .build()\n );\n },\n [magazine, viewAtom, busyAtom, setView, handleClear, handleCurate],\n );\n\n return { menu, viewAtom };\n};\n\nuseToolbar.displayName = 'useToolbar';\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { useAtomValue } from '@effect-atom/atom-react';\nimport React, { useCallback, useEffect, useMemo } from 'react';\n\nimport { useOperationInvoker } from '@dxos/app-framework/ui';\nimport { GraphPath, LayoutOperation } from '@dxos/app-toolkit';\nimport { type AppSurface, useShowItem } from '@dxos/app-toolkit/ui';\nimport { Obj, Ref } from '@dxos/echo';\nimport { useObject } from '@dxos/echo-react';\nimport { log } from '@dxos/log';\nimport { Panel, useTranslation } from '@dxos/react-ui';\nimport { Attention, useSelection } from '@dxos/react-ui-attention';\nimport { Masonry } from '@dxos/react-ui-masonry';\nimport { Menu } from '@dxos/react-ui-menu';\n\nimport { useVisibleMagazinePosts } from '#atoms';\nimport { meta } from '#meta';\nimport { FeedOperation, Magazine, Subscription } from '#types';\n\nimport { MagazineTile } from './MagazineTile';\nimport { useToolbar } from './useToolbar';\n\nexport type MagazineArticleProps = AppSurface.ObjectArticleProps<Magazine.Magazine>;\n\nexport const MagazineArticle = ({ role, subject, attendableId }: MagazineArticleProps) => {\n const { t } = useTranslation(meta.profile.key);\n const invoker = useOperationInvoker();\n const [magazine] = useObject(subject);\n\n // The toolbar owns the view-filter atom and the curate/clear handlers; the article reads `view` to\n // filter the visible posts.\n const { menu, viewAtom } = useToolbar({ magazine: subject });\n const view = useAtomValue(viewAtom);\n\n const showItem = useShowItem();\n const id = attendableId ?? Obj.getURI(magazine);\n const currentId = useSelection(id, 'single');\n const db = Obj.getDatabase(magazine);\n const posts = useVisibleMagazinePosts(subject, view);\n\n const handleToggleStar = useCallback(\n async (post: Subscription.Post, starred: boolean) => {\n const subscription = await post.source?.load();\n if (db && subscription) {\n void Subscription.setTag(subscription, post.id, db, 'starred', !starred);\n }\n },\n [db],\n );\n\n const handleOpen = useCallback(\n async (post: Subscription.Post) => {\n const subscription = await post.source?.load();\n if (subscription && !Subscription.getReadAt(subscription, post.id)) {\n void Subscription.setReadAt(subscription, post.id, new Date().toISOString());\n }\n if (post.link) {\n void invoker\n .invokePromise(FeedOperation.LoadPostContent, { post: Ref.make(post) })\n .catch((err) => log.catch(err, { postLink: post.link }));\n }\n void showItem({\n contextId: id,\n selectionId: post.id,\n companion: Attention.linkedSegment('post'),\n path: GraphPath.getObjectPathFromObject(subject),\n });\n },\n [id, showItem, invoker, subject],\n );\n\n const noPosts = posts.length === 0;\n useEffect(() => {\n if (noPosts) {\n void invoker.invokePromise(LayoutOperation.UpdateCompanion, {\n subject: Attention.linkedSegment('settings'),\n });\n }\n }, [noPosts, invoker]);\n\n const tileItems = useMemo<TileData[]>(\n () =>\n posts.map((post) => ({\n id: post.id,\n post,\n magazine: subject,\n current: currentId === post.id,\n onToggleStar: handleToggleStar,\n onOpen: handleOpen,\n })),\n [posts, currentId, handleOpen, handleToggleStar],\n );\n\n return (\n <Panel.Root role={role}>\n <Menu.Root {...menu} attendableId={attendableId}>\n <Panel.Toolbar asChild>\n <Menu.Toolbar />\n </Panel.Toolbar>\n </Menu.Root>\n <Panel.Content>\n {noPosts ? (\n // TODO(burdon): Factor out common EmptyState component; of push into Masonry, List, etc.\n <div className='h-full flex items-center justify-center text-subdued text-sm'>\n {t('empty-magazine.message')}\n </div>\n ) : (\n <Masonry.Root Tile={TileAdapter} minColumnWidth={20} maxColumnWidth={25}>\n <Masonry.Content thin centered padding>\n {/* TODO(burdon): Move items into Root. */}\n <Masonry.Viewport classNames='py-2' items={tileItems} />\n </Masonry.Content>\n </Masonry.Root>\n )}\n </Panel.Content>\n </Panel.Root>\n );\n};\n\ntype TileData = {\n id: string;\n post: Subscription.Post;\n magazine: Magazine.Magazine;\n current: boolean;\n onToggleStar?: (post: Subscription.Post, starred: boolean) => void;\n onOpen?: (post: Subscription.Post) => void;\n};\n\nconst TileAdapter = ({ data }: { data: TileData | undefined; index: number }) => {\n if (!data?.post) {\n return null;\n }\n\n return (\n <MagazineTile\n post={data.post}\n magazine={data.magazine}\n current={data.current}\n onOpen={data.onOpen}\n onToggleStar={data.onToggleStar}\n />\n );\n};\n\nMagazineArticle.displayName = 'MagazineArticle';\n"],"mappings":"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"}
@@ -0,0 +1,174 @@
1
+ import { Filter, Obj, Ref } from "@dxos/echo";
2
+ import { meta } from "#meta";
3
+ import { FeedOperation, Subscription } from "#types";
4
+ import { useCallback, useContext, useEffect, useMemo, useRef } from "react";
5
+ import { Panel } from "@dxos/react-ui";
6
+ import { postReadAtom, postTagsAtom, usePostContentAtom } from "#atoms";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { Atom, RegistryContext } from "@effect-atom/atom-react";
9
+ import { Attention } from "@dxos/react-ui-attention";
10
+ import { useOperationInvoker } from "@dxos/app-framework/ui";
11
+ import { useObject, useQuery } from "@dxos/echo-react";
12
+ import { PostContent } from "#components";
13
+ import { Menu, MenuBuilder, useMenuBuilder } from "@dxos/react-ui-menu";
14
+ import { log } from "@dxos/log";
15
+ //#region src/containers/PostArticle/PostToolbar.tsx
16
+ /**
17
+ * Reactive post toolbar built from the menu action-graph idiom. The action graph reads the post's
18
+ * star/archive/read slices (and the refreshing flag) via `get` inside the builder, so it subscribes
19
+ * to those atoms and rebuilds whenever this post's state changes. Deps hold only stable references
20
+ * (the post, the refreshing atom, callbacks), never the reactive values.
21
+ */
22
+ var PostToolbar = ({ post, refreshingAtom, attendableId, onSetStarred, onSetArchived, onMarkUnread, onRefresh, onOpenOriginal }) => {
23
+ const menuActions = useMenuBuilder((get) => {
24
+ const { starred, archived } = get(postTagsAtom(post));
25
+ const { readAt } = get(postReadAtom(post));
26
+ const read = readAt !== void 0;
27
+ const refreshing = get(refreshingAtom);
28
+ const hasLink = Boolean(post.link);
29
+ return MenuBuilder.make().action("star", {
30
+ label: [starred ? "unstar-post.label" : "star-post.label", { ns: meta.profile.key }],
31
+ icon: starred ? "ph--star--fill" : "ph--star--regular",
32
+ iconOnly: true
33
+ }, () => onSetStarred(!starred)).action("archive", {
34
+ label: [archived ? "unarchive-post.label" : "archive-post.label", { ns: meta.profile.key }],
35
+ icon: archived ? "ph--archive--fill" : "ph--archive--regular",
36
+ iconOnly: true
37
+ }, () => onSetArchived(!archived)).action("mark-unread", {
38
+ label: ["mark-unread.label", { ns: meta.profile.key }],
39
+ icon: "ph--envelope--regular",
40
+ iconOnly: true,
41
+ hidden: !read
42
+ }, onMarkUnread).separator("gap").action("refresh", {
43
+ label: [refreshing ? "refresh-content-pending.label" : "refresh-content.label", { ns: meta.profile.key }],
44
+ icon: "ph--arrows-clockwise--regular",
45
+ iconClassNames: refreshing ? "animate-spin" : void 0,
46
+ iconOnly: true,
47
+ disabled: refreshing,
48
+ hidden: !hasLink
49
+ }, onRefresh).action("open-original", {
50
+ label: ["open-original.label", { ns: meta.profile.key }],
51
+ icon: "ph--arrow-square-out--regular",
52
+ iconOnly: true,
53
+ hidden: !hasLink
54
+ }, onOpenOriginal).build();
55
+ }, [
56
+ post,
57
+ refreshingAtom,
58
+ onSetStarred,
59
+ onSetArchived,
60
+ onMarkUnread,
61
+ onRefresh,
62
+ onOpenOriginal
63
+ ]);
64
+ return /* @__PURE__ */ jsx(Menu.Root, {
65
+ ...menuActions,
66
+ attendableId,
67
+ children: /* @__PURE__ */ jsx(Panel.Toolbar, {
68
+ asChild: true,
69
+ children: /* @__PURE__ */ jsx(Menu.Toolbar, {})
70
+ })
71
+ });
72
+ };
73
+ PostToolbar.displayName = "PostToolbar";
74
+ //#endregion
75
+ //#region src/containers/PostArticle/PostArticle.tsx
76
+ var __dxlog_file = "/__w/dxos/dxos/packages/plugins/plugin-magazine/src/containers/PostArticle/PostArticle.tsx";
77
+ var PostArticle = ({ role, subject, attendableId }) => {
78
+ const toolbarAttendableId = attendableId && Attention.isLinkedSegment(attendableId) ? Attention.getParentId(attendableId) : attendableId;
79
+ const { invokePromise } = useOperationInvoker();
80
+ const registry = useContext(RegistryContext);
81
+ const [post] = useObject(subject);
82
+ const db = Obj.getDatabase(post);
83
+ const subscription = post.source?.target;
84
+ const { id } = post;
85
+ const postContent = usePostContentAtom(subject);
86
+ const requestedContentFor = useRef(void 0);
87
+ useEffect(() => {
88
+ if (requestedContentFor.current === id) return;
89
+ if (!post.link || postContent) return;
90
+ requestedContentFor.current = id;
91
+ invokePromise(FeedOperation.LoadPostContent, { post: Ref.make(subject) }).catch((err) => log.catch(err, { postLink: post.link }, {
92
+ "~LogMeta": "~LogMeta",
93
+ F: __dxlog_file,
94
+ L: 54,
95
+ S: void 0
96
+ }));
97
+ }, [
98
+ subject,
99
+ post,
100
+ post.link,
101
+ postContent,
102
+ invokePromise,
103
+ id
104
+ ]);
105
+ const allFeeds = useQuery(db, Filter.type(Subscription.Subscription));
106
+ const feedName = useMemo(() => {
107
+ const dxn = post.source?.uri;
108
+ if (!dxn) return;
109
+ return allFeeds.find((feed) => Obj.getURI(feed) === dxn)?.name;
110
+ }, [post.source, allFeeds]);
111
+ const handleOpenOriginal = useCallback(() => {
112
+ if (post.link) window.open(post.link, "_blank", "noopener,noreferrer");
113
+ }, [post.link]);
114
+ const handleMarkUnread = useCallback(() => {
115
+ if (subscription) Subscription.setReadAt(subscription, id, void 0);
116
+ }, [subscription, id]);
117
+ const handleSetArchived = useCallback((value) => {
118
+ if (db && subscription) Subscription.setTag(subscription, id, db, "archived", value);
119
+ }, [
120
+ db,
121
+ subscription,
122
+ id
123
+ ]);
124
+ const handleSetStarred = useCallback((value) => {
125
+ if (db && subscription) Subscription.setTag(subscription, id, db, "starred", value);
126
+ }, [
127
+ db,
128
+ subscription,
129
+ id
130
+ ]);
131
+ const refreshingAtom = useMemo(() => Atom.make(false), []);
132
+ const handleRefresh = useCallback(async () => {
133
+ if (!post.link || registry.get(refreshingAtom)) return;
134
+ registry.set(refreshingAtom, true);
135
+ try {
136
+ await invokePromise(FeedOperation.LoadPostContent, {
137
+ post: Ref.make(subject),
138
+ force: true
139
+ }, { notify: { error: ["refresh-content-error.message", { ns: meta.profile.key }] } });
140
+ } finally {
141
+ registry.set(refreshingAtom, false);
142
+ }
143
+ }, [
144
+ registry,
145
+ refreshingAtom,
146
+ subject,
147
+ post.link,
148
+ invokePromise
149
+ ]);
150
+ return /* @__PURE__ */ jsxs(Panel.Root, {
151
+ role,
152
+ children: [/* @__PURE__ */ jsx(PostToolbar, {
153
+ post: subject,
154
+ refreshingAtom,
155
+ attendableId: toolbarAttendableId,
156
+ onSetStarred: handleSetStarred,
157
+ onSetArchived: handleSetArchived,
158
+ onMarkUnread: handleMarkUnread,
159
+ onRefresh: () => void handleRefresh(),
160
+ onOpenOriginal: handleOpenOriginal
161
+ }), /* @__PURE__ */ jsx(Panel.Content, {
162
+ asChild: true,
163
+ children: /* @__PURE__ */ jsx(PostContent, {
164
+ post: subject,
165
+ metadata: feedName ? [feedName] : void 0
166
+ })
167
+ })]
168
+ });
169
+ };
170
+ PostArticle.displayName = "PostArticle";
171
+ //#endregion
172
+ export { PostArticle as default };
173
+
174
+ //# sourceMappingURL=chunk-PostArticle.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-PostArticle.mjs","names":[],"sources":["../../src/containers/PostArticle/PostToolbar.tsx","../../src/containers/PostArticle/PostArticle.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { type Atom } from '@effect-atom/atom';\nimport React from 'react';\n\nimport { Panel } from '@dxos/react-ui';\nimport { Menu, MenuBuilder, useMenuBuilder } from '@dxos/react-ui-menu';\n\nimport { postReadAtom, postTagsAtom } from '#atoms';\nimport { meta } from '#meta';\nimport { type Subscription } from '#types';\n\nexport type PostToolbarProps = {\n post: Subscription.Post;\n /** Whether a content re-fetch is in flight. */\n refreshingAtom: Atom.Atom<boolean>;\n attendableId?: string;\n onSetStarred: (value: boolean) => void;\n onSetArchived: (value: boolean) => void;\n onMarkUnread: () => void;\n onRefresh: () => void;\n onOpenOriginal: () => void;\n};\n\n/**\n * Reactive post toolbar built from the menu action-graph idiom. The action graph reads the post's\n * star/archive/read slices (and the refreshing flag) via `get` inside the builder, so it subscribes\n * to those atoms and rebuilds whenever this post's state changes. Deps hold only stable references\n * (the post, the refreshing atom, callbacks), never the reactive values.\n */\nexport const PostToolbar = ({\n post,\n refreshingAtom,\n attendableId,\n onSetStarred,\n onSetArchived,\n onMarkUnread,\n onRefresh,\n onOpenOriginal,\n}: PostToolbarProps) => {\n const menuActions = useMenuBuilder(\n (get) => {\n const { starred, archived } = get(postTagsAtom(post));\n const { readAt } = get(postReadAtom(post));\n const read = readAt !== undefined;\n const refreshing = get(refreshingAtom);\n const hasLink = Boolean(post.link);\n\n return (\n MenuBuilder.make()\n .action(\n 'star',\n {\n label: [starred ? 'unstar-post.label' : 'star-post.label', { ns: meta.profile.key }],\n icon: starred ? 'ph--star--fill' : 'ph--star--regular',\n iconOnly: true,\n },\n () => onSetStarred(!starred),\n )\n .action(\n 'archive',\n {\n label: [archived ? 'unarchive-post.label' : 'archive-post.label', { ns: meta.profile.key }],\n icon: archived ? 'ph--archive--fill' : 'ph--archive--regular',\n iconOnly: true,\n },\n () => onSetArchived(!archived),\n )\n .action(\n 'mark-unread',\n {\n label: ['mark-unread.label', { ns: meta.profile.key }],\n icon: 'ph--envelope--regular',\n iconOnly: true,\n hidden: !read,\n },\n onMarkUnread,\n )\n // `gap` is a flexible spacer that pushes the link actions to the trailing edge.\n .separator('gap')\n .action(\n 'refresh',\n {\n label: [refreshing ? 'refresh-content-pending.label' : 'refresh-content.label', { ns: meta.profile.key }],\n icon: 'ph--arrows-clockwise--regular',\n iconClassNames: refreshing ? 'animate-spin' : undefined,\n iconOnly: true,\n disabled: refreshing,\n hidden: !hasLink,\n },\n onRefresh,\n )\n .action(\n 'open-original',\n {\n label: ['open-original.label', { ns: meta.profile.key }],\n icon: 'ph--arrow-square-out--regular',\n iconOnly: true,\n hidden: !hasLink,\n },\n onOpenOriginal,\n )\n .build()\n );\n },\n [post, refreshingAtom, onSetStarred, onSetArchived, onMarkUnread, onRefresh, onOpenOriginal],\n );\n\n return (\n <Menu.Root {...menuActions} attendableId={attendableId}>\n <Panel.Toolbar asChild>\n <Menu.Toolbar />\n </Panel.Toolbar>\n </Menu.Root>\n );\n};\n\nPostToolbar.displayName = 'PostToolbar';\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { Atom, RegistryContext } from '@effect-atom/atom-react';\nimport React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react';\n\nimport { useOperationInvoker } from '@dxos/app-framework/ui';\nimport { type AppSurface } from '@dxos/app-toolkit/ui';\nimport { Filter, Obj, Ref } from '@dxos/echo';\nimport { useObject, useQuery } from '@dxos/echo-react';\nimport { log } from '@dxos/log';\nimport { Panel } from '@dxos/react-ui';\nimport { Attention } from '@dxos/react-ui-attention';\n\nimport { usePostContentAtom } from '#atoms';\nimport { PostContent } from '#components';\nimport { meta } from '#meta';\nimport { FeedOperation } from '#types';\nimport { Subscription } from '#types';\n\nimport { PostToolbar } from './PostToolbar';\n\nexport type PostArticleProps = AppSurface.ObjectArticleProps<Subscription.Post>;\n\nexport const PostArticle = ({ role, subject, attendableId }: PostArticleProps) => {\n // When shown as a companion the `attendableId` is a linked segment, but attention lives on the\n // parent plank — resolve to the parent so the toolbar reads as attended (active).\n const toolbarAttendableId =\n attendableId && Attention.isLinkedSegment(attendableId) ? Attention.getParentId(attendableId) : attendableId;\n const { invokePromise } = useOperationInvoker();\n const registry = useContext(RegistryContext);\n const [post] = useObject(subject);\n const db = Obj.getDatabase(post);\n const subscription = post.source?.target;\n const { id } = post;\n const postContent = usePostContentAtom(subject);\n\n // Lazily fetch full article content the first time this Post is shown — covers\n // entry points that bypass MagazineArticle's `handleOpen` (deep-link, agent surface,\n // restored attention). LoadPostContent is idempotent (no-op if `content` already\n // populated or `link` missing), so a unique guard per Post id is enough to keep us\n // from re-firing on every render.\n const requestedContentFor = useRef<string | undefined>(undefined);\n useEffect(() => {\n if (requestedContentFor.current === id) {\n return;\n }\n if (!post.link || postContent) {\n return;\n }\n requestedContentFor.current = id;\n void invokePromise(FeedOperation.LoadPostContent, { post: Ref.make(subject) }).catch((err) =>\n log.catch(err, { postLink: post.link }),\n );\n }, [subject, post, post.link, postContent, invokePromise, id]);\n\n // Reactive lookup of the source feed name. `post.feed?.target?.name` only renders\n // synchronously when the ref is already resolved; querying feeds via useQuery means\n // the meta line updates as soon as the feed object is loaded into the space.\n const allFeeds = useQuery(db, Filter.type(Subscription.Subscription));\n const feedName = useMemo(() => {\n const dxn = post.source?.uri;\n if (!dxn) {\n return undefined;\n }\n return allFeeds.find((feed) => Obj.getURI(feed) === dxn)?.name;\n }, [post.source, allFeeds]);\n\n const handleOpenOriginal = useCallback(() => {\n if (post.link) {\n window.open(post.link, '_blank', 'noopener,noreferrer');\n }\n }, [post.link]);\n\n const handleMarkUnread = useCallback(() => {\n if (subscription) {\n Subscription.setReadAt(subscription, id, undefined);\n }\n }, [subscription, id]);\n\n // Value-setters (not toggles): the toolbar reads the current star/archive state reactively via\n // `get` and passes the desired next value, so these don't need to subscribe to the current state.\n const handleSetArchived = useCallback(\n (value: boolean) => {\n if (db && subscription) {\n void Subscription.setTag(subscription, id, db, 'archived', value);\n }\n },\n [db, subscription, id],\n );\n\n const handleSetStarred = useCallback(\n (value: boolean) => {\n if (db && subscription) {\n void Subscription.setTag(subscription, id, db, 'starred', value);\n }\n },\n [db, subscription, id],\n );\n\n // Re-fetch the article body from the source via the LoadPostContent operation with `force`, so the\n // fetch/extraction/storage logic lives in one place (the operation) rather than inline here. Appends\n // a fresh content entry to the subscription's contentFeed; the reverse-ref lookup picks the newest.\n // `refreshing` is an atom so the toolbar subscribes to it via `get` rather than via React deps.\n const refreshingAtom = useMemo(() => Atom.make(false), []);\n const handleRefresh = useCallback(async () => {\n if (!post.link || registry.get(refreshingAtom)) {\n return;\n }\n registry.set(refreshingAtom, true);\n try {\n // Failures surface as a toast via `notify`; invokePromise resolves with `{ error }`, never throws.\n await invokePromise(\n FeedOperation.LoadPostContent,\n { post: Ref.make(subject), force: true },\n { notify: { error: ['refresh-content-error.message', { ns: meta.profile.key }] } },\n );\n } finally {\n registry.set(refreshingAtom, false);\n }\n }, [registry, refreshingAtom, subject, post.link, invokePromise]);\n\n return (\n <Panel.Root role={role}>\n <PostToolbar\n post={subject}\n refreshingAtom={refreshingAtom}\n attendableId={toolbarAttendableId}\n onSetStarred={handleSetStarred}\n onSetArchived={handleSetArchived}\n onMarkUnread={handleMarkUnread}\n onRefresh={() => void handleRefresh()}\n onOpenOriginal={handleOpenOriginal}\n />\n <Panel.Content asChild>\n <PostContent post={subject} metadata={feedName ? [feedName] : undefined} />\n </Panel.Content>\n </Panel.Root>\n );\n};\n\nPostArticle.displayName = 'PostArticle';\n"],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import { t as formatDate } from "./chunk-date.mjs";
2
+ import { n as getSnippet, t as getImageUrl } from "./chunk-post-content.mjs";
3
+ import { Filter, Obj } from "@dxos/echo";
4
+ import { Subscription } from "#types";
5
+ import { useMemo } from "react";
6
+ import { Card } from "@dxos/react-ui";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { useObject, useQuery } from "@dxos/echo-react";
9
+ //#region src/containers/PostCard/PostCard.tsx
10
+ /**
11
+ * Compact preview of a {@link Subscription.Post}. Rendered into the
12
+ * `AppSurface.CardContent` slot — Card.Root is supplied by the surface host
13
+ * (popovers, sections, related-objects), so the body emits Card.Body only.
14
+ */
15
+ var PostCard = ({ subject }) => {
16
+ const [post] = useObject(subject);
17
+ const allFeeds = useQuery(Obj.getDatabase(post), Filter.type(Subscription.Subscription));
18
+ const feedName = useMemo(() => {
19
+ const dxn = post.source?.uri;
20
+ if (!dxn) return;
21
+ return allFeeds.find((feed) => Obj.getURI(feed) === dxn)?.name;
22
+ }, [post.source, allFeeds]);
23
+ const published = formatDate(post.published);
24
+ const imageUrl = getImageUrl(post);
25
+ const snippet = useMemo(() => getSnippet(post) || void 0, [post.description]);
26
+ return /* @__PURE__ */ jsxs(Card.Body, { children: [
27
+ imageUrl && /* @__PURE__ */ jsx(Card.Poster, {
28
+ alt: post.title ?? "",
29
+ image: imageUrl,
30
+ fit: "cover",
31
+ classNames: "rounded-t-xs"
32
+ }),
33
+ post.title && /* @__PURE__ */ jsx(Card.Row, { children: /* @__PURE__ */ jsx(Card.Title, {
34
+ classNames: "line-clamp-2",
35
+ children: post.title
36
+ }) }),
37
+ snippet && /* @__PURE__ */ jsx(Card.Row, { children: /* @__PURE__ */ jsx(Card.Text, {
38
+ variant: "description",
39
+ classNames: "line-clamp-3",
40
+ children: snippet
41
+ }) }),
42
+ (feedName || published) && /* @__PURE__ */ jsx(Card.Row, { children: /* @__PURE__ */ jsxs("div", {
43
+ className: "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-2 text-sm text-description overflow-hidden",
44
+ children: [/* @__PURE__ */ jsx("span", {
45
+ className: "truncate",
46
+ children: feedName ?? ""
47
+ }), /* @__PURE__ */ jsx("span", {
48
+ className: "text-end shrink-0",
49
+ children: published ?? ""
50
+ })]
51
+ }) }),
52
+ post.link && /* @__PURE__ */ jsx(Card.Link, {
53
+ label: post.link,
54
+ href: post.link
55
+ })
56
+ ] });
57
+ };
58
+ PostCard.displayName = "PostCard";
59
+ //#endregion
60
+ export { PostCard as default };
61
+
62
+ //# sourceMappingURL=chunk-PostCard.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-PostCard.mjs","names":[],"sources":["../../src/containers/PostCard/PostCard.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { useMemo } from 'react';\n\nimport { type AppSurface } from '@dxos/app-toolkit/ui';\nimport { Filter, Obj } from '@dxos/echo';\nimport { useObject, useQuery } from '@dxos/echo-react';\nimport { Card } from '@dxos/react-ui';\n\nimport { Subscription } from '#types';\n\nimport { formatDate, getImageUrl, getSnippet } from '../../util';\n\nexport type PostCardProps = AppSurface.ObjectCardProps<Subscription.Post>;\n\n/**\n * Compact preview of a {@link Subscription.Post}. Rendered into the\n * `AppSurface.CardContent` slot — Card.Root is supplied by the surface host\n * (popovers, sections, related-objects), so the body emits Card.Body only.\n */\nexport const PostCard = ({ subject }: PostCardProps) => {\n const [post] = useObject(subject);\n\n // Resolve the source feed's display name when present. `post.feed?.target?.name` only\n // resolves synchronously when the ref is already loaded; querying via useQuery means\n // the meta line lights up as soon as the feed lands in the space.\n const db = Obj.getDatabase(post);\n const allFeeds = useQuery(db, Filter.type(Subscription.Subscription));\n const feedName = useMemo(() => {\n const dxn = post.source?.uri;\n if (!dxn) {\n return undefined;\n }\n return allFeeds.find((feed) => Obj.getURI(feed) === dxn)?.name;\n }, [post.source, allFeeds]);\n\n const published = formatDate(post.published);\n // snippet/imageUrl are derived from the Post's description (no content-feed entry in this surface).\n const imageUrl = getImageUrl(post);\n const snippet = useMemo(() => getSnippet(post) || undefined, [post.description]);\n\n return (\n <Card.Body>\n {imageUrl && <Card.Poster alt={post.title ?? ''} image={imageUrl} fit='cover' classNames='rounded-t-xs' />}\n {post.title && (\n <Card.Row>\n <Card.Title classNames='line-clamp-2'>{post.title}</Card.Title>\n </Card.Row>\n )}\n {snippet && (\n <Card.Row>\n <Card.Text variant='description' classNames='line-clamp-3'>\n {snippet}\n </Card.Text>\n </Card.Row>\n )}\n {(feedName || published) && (\n <Card.Row>\n <div className='grid grid-cols-[minmax(0,1fr)_auto] items-center gap-2 text-sm text-description overflow-hidden'>\n <span className='truncate'>{feedName ?? ''}</span>\n <span className='text-end shrink-0'>{published ?? ''}</span>\n </div>\n </Card.Row>\n )}\n {post.link && <Card.Link label={post.link} href={post.link} />}\n </Card.Body>\n );\n};\n\nPostCard.displayName = 'PostCard';\n"],"mappings":";;;;;;;;;;;;;;AAsBA,IAAa,YAAY,EAAE,cAA6B;CACtD,MAAM,CAAC,QAAQ,UAAU,OAAO;CAMhC,MAAM,WAAW,SADN,IAAI,YAAY,IACD,GAAI,OAAO,KAAK,aAAa,YAAY,CAAC;CACpE,MAAM,WAAW,cAAc;EAC7B,MAAM,MAAM,KAAK,QAAQ;EACzB,IAAI,CAAC,KACH;EAEF,OAAO,SAAS,MAAM,SAAS,IAAI,OAAO,IAAI,MAAM,GAAG,CAAC,EAAE;CAC5D,GAAG,CAAC,KAAK,QAAQ,QAAQ,CAAC;CAE1B,MAAM,YAAY,WAAW,KAAK,SAAS;CAE3C,MAAM,WAAW,YAAY,IAAI;CACjC,MAAM,UAAU,cAAc,WAAW,IAAI,KAAK,KAAA,GAAW,CAAC,KAAK,WAAW,CAAC;CAE/E,OACE,qBAAC,KAAK,MAAN,EAAA,UAAA;EACG,YAAY,oBAAC,KAAK,QAAN;GAAa,KAAK,KAAK,SAAS;GAAI,OAAO;GAAU,KAAI;GAAQ,YAAW;EAAgB,CAAA;EACxG,KAAK,SACJ,oBAAC,KAAK,KAAN,EAAA,UACE,oBAAC,KAAK,OAAN;GAAY,YAAW;aAAgB,KAAK;EAAkB,CAAA,EACtD,CAAA;EAEX,WACC,oBAAC,KAAK,KAAN,EAAA,UACE,oBAAC,KAAK,MAAN;GAAW,SAAQ;GAAc,YAAW;aACzC;EACQ,CAAA,EACH,CAAA;GAEV,YAAY,cACZ,oBAAC,KAAK,KAAN,EAAA,UACE,qBAAC,OAAD;GAAK,WAAU;aAAf,CACE,oBAAC,QAAD;IAAM,WAAU;cAAY,YAAY;GAAS,CAAA,GACjD,oBAAC,QAAD;IAAM,WAAU;cAAqB,aAAa;GAAS,CAAA,CACxD;KACG,CAAA;EAEX,KAAK,QAAQ,oBAAC,KAAK,MAAN;GAAW,OAAO,KAAK;GAAM,MAAM,KAAK;EAAO,CAAA;CACpD,EAAA,CAAA;AAEf;AAEA,SAAS,cAAc"}