@dxos/plugin-magazine 0.10.0 → 0.11.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 (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,36 @@
1
+ import { ActivationEvent, Plugin } from "@dxos/app-framework";
2
+ import { StateMap, TagIndex } from "@dxos/schema";
3
+ import { Instructions } from "@dxos/compute";
4
+ import { meta } from "#meta";
5
+ import { AppActivationEvents, AppPlugin } from "@dxos/app-toolkit";
6
+ import { AttentionEvents } from "@dxos/plugin-attention";
7
+ import { AppGraphBuilder, CreateObject, OperationHandler, ReactSurface, RoutineTemplates, SkillDefinition } from "#capabilities";
8
+ import { translations } from "#translations";
9
+ import { Magazine, Subscription } from "#types";
10
+ //#endregion
11
+ //#region src/MagazinePlugin.tsx
12
+ var MagazinePlugin = Plugin.define(meta).pipe(AppPlugin.addAppGraphModule({
13
+ activatesOn: ActivationEvent.allOf(AppActivationEvents.SetupAppGraph, AttentionEvents.AttentionReady),
14
+ activate: AppGraphBuilder
15
+ }), AppPlugin.addCreateObjectModule({ activate: CreateObject }), AppPlugin.addOperationHandlerModule({ activate: OperationHandler }), AppPlugin.addPluginAssetModule({ asset: {
16
+ pluginId: meta.profile.key,
17
+ path: "PLUGIN.mdl",
18
+ content: "---\nid: org.dxos.plugin.magazine\nname: MagazinePlugin\nversion: 0.2.0\n---\n\nA Composer plugin for syndicated content. Today it supports RSS and ATproto\nFeeds whose Posts are synced into the space as ECHO objects. This revision\nadds **Magazines** — agent-curated collections of articles drawn from one or\nmore Feeds and displayed as a Masonry grid. The user describes in natural\nlanguage what content a Magazine should gather; pressing *Curate* triggers\nfeed sync followed by an agent that selects matching articles, fetches their\ntext and hero images, and appends them to the Magazine.\n\nThe design also scaffolds (schema-only) a future tagging feature in which the\nagent assigns tags to curated Posts to enable navigation, filtering, and\nfeed-discovery flows.\n\n## Extensions\n\nThe following extension dialects are used in this document.\n\n| Term | URI |\n|-------------|--------------------------------|\n| `type` | `org.dxos.mdl.type@1.0` |\n| `feat` | `org.dxos.mdl.feat@1.0` |\n| `test` | `org.dxos.mdl.test@1.0` |\n| `component` | `org.dxos.mdl.component@1.0` |\n| `op` | `org.dxos.mdl.op@1.0` |\n| `service` | `org.dxos.mdl.service@1.0` |\n\n## Types\n\n```mdl\ntype FeedType\n literals: atproto | rss\n```\n\n```mdl\ntype Feed\n desc: >\n A subscription source. Existing type — documented here for context.\n SyncFeed pulls Posts from the URL into the space.\n fields:\n name?: string\n url?: string\n type?: FeedType\n description?: string\n link?: string\n iconUrl?: string\n cursor?: string # sync state\n feed?: Ref<Queue> # backing ECHO feed\n```\n\n```mdl\ntype Post\n desc: >\n An individual syndicated article. Existing fields are unchanged.\n This revision adds four optional fields populated by the Magazine\n curation flow. All additions are optional — existing Posts remain valid.\n fields:\n title?: string\n link?: string # URL of the original article\n description?: string # feed-provided summary (as-is)\n author?: string\n published?: string # ISO 8601 publication date\n guid?: string # feed-provided unique id\n\n # --- Additions ---\n snippet?: string # short description-derived preview (~200 chars) for tiles\n content?: string # full article body plain text, populated on first open\n imageUrl?: string # hero image URL extracted from the article page\n readAt?: string # ISO 8601; presence = read, absence = unread\n tags?: string[] # agent-assigned tags (future tagging feature; always [] today)\n```\n\n```mdl\ntype Magazine\n desc: >\n An agent-curated collection of articles drawn from one or more Feeds.\n Curation is driven by a Routine (see MagazineSkill): the Routine's\n editorial instructions describe what content this Magazine should gather,\n and it references the MagazineSkill (the HOW — tools and output\n contract). The Routine is created lazily on first curation, its\n instructions seeded with the default editorial methodology and the user's\n `topic`. The agent selects matching Posts from the referenced Feeds,\n enriches them with snippet and hero image, and their refs are appended to\n `posts`.\n fields:\n name?: string\n feeds: Ref<Feed>[] # Feeds to pull from\n topic?: string # create-dialog editorial seed; woven into the Routine's instructions on first curate\n routine?: Ref<Routine> # editorial Routine (lazy; created by the curate op); not rendered as form input\n posts: Ref<Post>[] # curated Post refs (insertion order; UI displays newest-last reversed)\n annotations:\n icon: ph--book-open-text--regular\n hue: indigo\n label: name\n note: >\n The Routine is added to its own `objects` (context) so the agent sees the\n Magazine as bound context, not only the candidate JSON `input`.\n```\n\n## Components\n\n```mdl\ncomponent MagazineArticle\n desc: >\n Article surface bound to a Magazine object. Shows curated articles in a\n Masonry grid and offers a Curate button that triggers feed sync plus\n agent curation.\n props:\n magazine: Magazine\n state:\n running: boolean # Curate flow in progress\n phase?: 'syncing' | 'curating' # shown as button tooltip while running\n slots:\n header?: ReactNode\n actions:\n curate() # run syncFeeds → invoke CurateMagazine\n openPost(post: Post) # sets post.readAt if unset; dispatches useShowItem (plugin-deck)\n emits:\n onCurateComplete(added: number)\n layout: |\n ┌─────────────────────────────────────────────┐\n │ [header slot] [✨ Curate] │\n ├─────────────────────────────────────────────┤\n │ ┌─────┐ ┌─────┐ ┌─────┐ │\n │ │tile │ │tile │ │tile │ (Masonry grid) │\n │ └─────┘ └─────┘ └─────┘ │\n │ ┌─────┐ ┌─────┐ │\n │ │tile │ │tile │ │\n │ └─────┘ └─────┘ │\n └─────────────────────────────────────────────┘\n PostArticle is rendered by the host layout (complementary drawer,\n sibling plank, or deck companion) — not composed inline.\n```\n\n```mdl\ncomponent MagazineTile\n desc: >\n Single masonry cell rendering a curated Post. Dimmed when post.readAt is set.\n Click opens the ArticleReader. Tags row appears only when tags.length > 0.\n props:\n post: Post\n feed: Feed # resolved parent feed for name/icon\n emits:\n onOpen(post: Post)\n layout: |\n ┌─────────────────────────┐\n │ [hero image if present] │\n ├─────────────────────────┤\n │ Title (2-line clamp) │\n │ Snippet (3-line clamp) │\n │ ─────────────────────── │\n │ Feed · Author · Date │\n │ [#tag] [#tag] │ (only when tags.length > 0)\n └─────────────────────────┘\n```\n\n```mdl\ncomponent PostArticle\n desc: >\n Independent article surface for a single Post. Registered as a React\n surface matching `object(Article, Subscription.Post)` so it can render\n in any article/companion context — the complementary sidebar in\n 'simple' mode, a sibling plank in 'multi' mode, or the deck companion\n in the default 'deck' mode.\n\n Panel-based layout with a toolbar; the \"Open original\" link lives in\n the toolbar so it's always accessible. post.readAt is set by the\n master (MagazineArticle) when the tile is opened, not by PostArticle.\n props:\n post: Post # via Surface data.subject\n layout: |\n ┌───────────────────────────────────────┐\n │ Title [Open original] │ ← toolbar\n ├───────────────────────────────────────┤\n │ Author · Date │\n │ [hero image] │\n │ Snippet │\n └───────────────────────────────────────┘\n```\n\n## Services\n\n```mdl\nservice MagazineSkill\n desc: |\n Agent skill referenced by each Magazine's Routine. Registered as a\n SkillDefinition capability. The skill instructions are limited to\n HOW to operate: how to call the FetchArticleContent tool and the\n completeJob output contract. The editorial methodology (what to select,\n dedup, snippet style, hero-image choice) and the magazine's topic live on\n the Routine's instructions, not here.\n key: org.dxos.skill.magazine\n ops:\n # Agent-facing tools exposed by the skill:\n # - FetchArticleContent\n # (completeJob is provided by the agent runtime; SyncFeed / CurateMagazine /\n # ClearMagazine / LoadPostContent are orchestration/UI, not agent tools.)\n note: >\n The curate op resolves this skill from the registry and references it\n from the Magazine's Routine. The agent reads candidate summaries from the\n `input`, MAY call FetchArticleContent to read full text, then calls\n completeJob with the structured selection.\n```\n\n## Operations\n\n```mdl\nop SyncFeed\n desc: Existing operation. Pulls new Posts from a Feed's source URL.\n input:\n feedId: DXN\n output: number # number of Posts added\n effects: [http, echo:write]\n note: Documented here for context; unchanged by this revision.\n```\n\n```mdl\nop ListCandidatePosts\n desc: >\n Returns lightweight summaries of Posts from the Magazine's referenced\n feeds that have NOT yet been added to the Magazine. Excludes Posts whose\n refs already appear in magazine.posts.\n input:\n magazineId: DXN\n output:\n Array<{\n postId: DXN,\n feedName?: string,\n title?: string,\n description?: string,\n author?: string,\n published?: string\n }>\n effects: [echo:read]\n note: >\n Deliberately excludes the full article body and image — the agent\n decides from summaries whether to call FetchArticleContent.\n```\n\n```mdl\nop FetchArticleContent\n desc: >\n Fetches post.link over HTTP and returns extracted plain text plus any\n image URLs found (og:image and <img> tags). Pure read — does not mutate\n the Post. Reusable by the future tagging feature.\n input:\n postId: DXN\n output:\n {\n text: string,\n imageUrls: string[]\n }\n errors:\n FetchFailed: HTTP error or network failure\n NoLink: the Post has no `link` field\n effects: [http, echo:read]\n```\n\n```mdl\nop AddPostToMagazine\n desc: >\n Writes snippet and imageUrl onto the Post and appends the Post's ref to\n magazine.posts. Idempotent — no-op if the Post ref is already present.\n input:\n magazineId: DXN\n postId: DXN\n snippet: string\n imageUrl?: string\n output: Post\n effects: [echo:write]\n```\n\n```mdl\nop CurateMagazine\n desc: >\n Deterministic one-shot curation invoked by the Curate button. For each\n uncurated candidate Post, derives a snippet and hero image from the Post's\n existing `description` (no HTTP) and appends the enriched Post ref to\n `magazine.posts`. Skips Posts with empty descriptions.\n input:\n magazineId: DXN\n output:\n added: number\n effects: [echo:write]\n note: >\n Exists alongside the skill. The deterministic path gives the button\n an immediate, reliable result without needing an LLM. The agent path\n (via the skill's fine-grained tools) delivers instruction-driven,\n higher-quality curation through chat.\n```\n\n## Features\n\n```mdl\nfeat F-1: Magazine Type\n desc: Introduce the Magazine ECHO type with feeds, instructions, and curated posts.\n\n req F-1.1: User can create a Magazine object via the plugin's create menu.\n req F-1.2: Magazine.name is optional; magazine is labeled by name when set.\n req F-1.3: Magazine.feeds is an array of Ref<Feed>; empty by default.\n req F-1.4: Magazine.routine is an optional Ref<Routine>, created lazily on first curate; hidden from the standard form input. Magazine.topic holds the create-dialog editorial seed until the Routine exists.\n req F-1.5: Magazine.posts is an array of Ref<Post>; empty by default; insertion order preserved.\n```\n\n```mdl\nfeat F-2: Post Extensions\n desc: Add optional snippet, imageUrl, readAt, and tags to existing Post type.\n\n req F-2.1: All new fields are optional; existing Post objects load unchanged.\n req F-2.2: snippet is a plain string; no max length enforced at schema level.\n req F-2.3: imageUrl is a plain string URL.\n req F-2.4: readAt is an ISO 8601 timestamp; presence = read, absence = unread.\n req F-2.5: tags is a string array; defaults to empty; not populated in this revision.\n```\n\n```mdl\nfeat F-3: Magazine Curate Flow\n desc: Sync referenced feeds then invoke the agent to curate articles.\n\n req F-3.1: MagazineArticle toolbar includes a Curate button.\n req F-3.2:\n when: user clicks Curate\n then: button shows spinner, is disabled, tooltip shows phase ('Syncing feeds...' / 'Curating articles...')\n req F-3.3:\n when: Curate runs\n then: SyncFeed invoked in parallel for each Ref in magazine.feeds; per-feed failures are collected but do not abort the flow\n req F-3.4:\n when: all feed syncs resolve (success or failure)\n then: CurateMagazine operation is invoked with the Magazine ref\n req F-3.5:\n when: CurateMagazine completes\n then: button returns to idle; reactive UI renders the new tiles\n req F-3.6:\n when: magazine.feeds is empty\n then: Curate button is disabled with tooltip 'Add at least one feed first'\n req F-3.7: Re-clicking Curate while running is a no-op (button disabled; in-memory running flag).\n```\n\n```mdl\nfeat F-4: Agent Curation Behavior\n desc: Agent uses tools to select, fetch, and add Posts.\n\n req F-4.1: Agent reads magazine.instructions Text and magazine.feeds refs from context.\n req F-4.2: Agent calls ListCandidatePosts(magazineId) to receive unenriched candidates.\n req F-4.3: Agent filters candidates against the instructions and skips low-relevance posts.\n req F-4.4: For each selected Post, agent calls FetchArticleContent(postId) to obtain text and images.\n req F-4.5: Agent produces a concise snippet (target ~200 chars) and selects the best image URL.\n req F-4.6: Agent calls AddPostToMagazine(magazineId, postId, snippet, imageUrl).\n req F-4.7: AddPostToMagazine is idempotent; re-invocation with the same postId is a no-op append.\n```\n\n```mdl\nfeat F-5: Masonry Display\n desc: Render curated Posts in a Masonry grid with rich tiles.\n\n req F-5.1: MagazineArticle renders magazine.posts in reverse insertion order (newest first) in a Masonry grid.\n req F-5.2: Each tile shows imageUrl (if present), title, snippet, feed name, author, published date.\n req F-5.3: Tile tags row renders only when post.tags.length > 0.\n req F-5.4: Tiles where post.readAt is set are visually dimmed.\n req F-5.5: Clicking a tile opens the ArticleReader for that Post.\n req F-5.6: Opening the reader sets post.readAt to the current ISO timestamp if not already set.\n req F-5.7: Empty state — when magazine.posts is empty — shows: 'No articles yet. Add feeds and press Curate.'\n req F-5.8: Grid uses the @dxos/react-ui-masonry Masonry component.\n```\n\n```mdl\nfeat F-6: Post Article Detail (master-detail via layout dispatch)\n desc: Post detail is rendered by a standalone PostArticle surface. The\n master (MagazineArticle) does not compose the detail inline;\n instead it asks the deck's `useShowItem` to select the post and\n route the detail to the appropriate place for the current\n layout mode (complementary drawer / sibling plank / companion).\n\n req F-6.1: PostArticle is registered as a React surface matching\n `object(Article, Subscription.Post)` — usable anywhere an\n article for a Post is requested.\n req F-6.2: PostArticle is a Panel with a Toolbar. The 'Open original'\n link lives in the toolbar. Body shows metadata, hero image,\n and snippet.\n req F-6.3:\n when: user clicks a tile in MagazineArticle\n then:\n - post.readAt is set to the current ISO timestamp if unset\n - useShowItem is invoked with:\n contextId = magazine attendableId\n selectionId = post.id\n companion = linkedSegment('post')\n path = getObjectPathFromObject(post)\n req F-6.4:\n when: useShowItem is invoked in 'simple' layout mode\n then: the complementary sidebar opens on linkedSegment('post')\n req F-6.5:\n when: useShowItem is invoked in 'multi' layout mode\n then: a sibling plank opens for the post's path, pivoted on the magazine\n req F-6.6:\n when: useShowItem is invoked in the default ('deck') layout mode\n then: the current plank's companion changes to linkedSegment('post')\n req F-6.7: An app-graph extension under each Magazine node exposes a\n companion node for linkedSegment('post') whose data resolves\n to the currently-selected Post from magazine.posts.\n```\n\n```mdl\nfeat F-7: Future Tagging (schema scaffolding only)\n desc: Schema fields in place so the tagging feature composes without migration.\n\n req F-7.1: Post.tags exists as Schema.Array(Schema.String).pipe(Schema.optional).\n req F-7.2: MagazineTile renders a tags row conditionally on tags.length > 0.\n req F-7.3: FetchArticleContent is reusable as the input stage for a future TagPosts operation.\n note: >\n Later features (out of scope): TagPosts operation, a MagazineTagger skill\n (or extension of MagazineSkill), tag-filter chips in the toolbar,\n 'Find more like this' action, and 'Suggest feeds' action proposing new Feed\n URLs based on accumulated tag distribution.\n```\n\n## Acceptance\n\n```mdl\ntest T-1: Create Magazine\n given: user is in a space in Composer\n when: user creates a Magazine via the plugin's create menu\n then:\n - Magazine object created in space\n - Magazine.feeds is an empty array\n - Magazine.posts is an empty array\n - Magazine.instructions is a Ref to an empty Text object\n```\n\n```mdl\ntest T-2: Post schema backward compatibility\n given: a Post created before this revision exists in the space\n when: the new plugin version loads\n then:\n - Post loads without schema error\n - Post.snippet, Post.imageUrl, Post.readAt, Post.tags are all undefined / empty\n```\n\n```mdl\ntest T-3: Curate button disabled without feeds\n given: a Magazine with magazine.feeds.length === 0\n when: MagazineArticle renders\n then:\n - Curate button is disabled\n - tooltip text === 'Add at least one feed first'\n```\n\n```mdl\ntest T-4: Curate flow runs sync then deterministic curation\n given: a Magazine with two referenced feeds, each with unsynced remote posts\n when: user clicks Curate\n then:\n - SyncFeed invoked once per referenced feed\n - After all SyncFeed calls resolve, CurateMagazine operation is invoked\n - Magazine.posts grows by the count returned from CurateMagazine\n - Button returns to idle\n```\n\n```mdl\ntest T-4a: Agent curation via skill\n given: a chat referencing a Magazine whose MagazineSkill is active\n when: the agent is prompted to curate the magazine\n then:\n - Agent calls ListCandidatePosts, FetchArticleContent, AddPostToMagazine\n - Magazine.posts grows by the agent-selected count\n note: >\n The skill path is distinct from the Curate button (T-4). The button\n runs the deterministic CurateMagazine op; agents invoke the fine-grained\n tools through chat.\n```\n\n```mdl\ntest T-5: Partial feed sync failure is non-fatal\n given: a Magazine with two feeds; one feed's URL returns 500\n when: user clicks Curate\n then:\n - CurateMagazine still runs against posts from the successful feed\n - Error surfaced via a localized message in the status bar\n```\n\n```mdl\ntest T-6: ListCandidatePosts excludes curated\n given: a Magazine whose posts array already contains refs to two Posts from feed A\n when: ListCandidatePosts(magazineId) is called\n then: returned list contains no entries whose postId matches an existing magazine.posts ref\n```\n\n```mdl\ntest T-7: AddPostToMagazine is idempotent\n given: a Magazine already containing a Post ref\n when: AddPostToMagazine invoked with the same postId\n then:\n - magazine.posts length unchanged\n - Post.snippet and Post.imageUrl left unchanged (no overwrite on duplicate)\n```\n\n```mdl\ntest T-8: FetchArticleContent surfaces images\n given: a Post whose link resolves to an HTML page with og:image and two <img> tags\n when: FetchArticleContent(postId) is called\n then:\n - output.text contains the page's main text\n - output.imageUrls includes the og:image URL\n - output.imageUrls includes URLs from <img> tags\n```\n\n```mdl\ntest T-9: Tile dimming on read\n given: a Magazine with one Post where readAt is unset\n when: user clicks the tile\n then:\n - ArticleReader opens for that Post\n - Post.readAt is set to an ISO timestamp\n - On reader close, the tile renders dimmed\n```\n\n```mdl\ntest T-10: Tile metadata\n given: a curated Post with title, snippet, imageUrl, author, published, and a feed with name\n when: MagazineTile renders\n then:\n - hero image rendered from imageUrl\n - title rendered with 2-line clamp\n - snippet rendered with 3-line clamp\n - metadata row shows feed name, author, and a formatted date\n - tags row is NOT rendered (tags is empty)\n```\n\n---\n\n## Design Notes\n\n### Curation via Routine (this revision)\n\nCuration runs a persisted **Routine** rather than an ephemeral one. The Magazine\nno longer carries an `instructions` Template; it carries an optional\n`routine: Ref<Routine>` plus a `topic` seed string.\n\n- **Eager creation.** `Magazine.make()` creates the `Routine` at magazine\n instantiation time. The Routine's `instructions` Text is seeded with the\n default editorial methodology and an optional topic woven into a `## Topic`\n section; it references the MagazineSkill and is parented to the magazine\n (cascade-delete). The magazine is added to the Routine's `objects` (context).\n- **Run.** The curate op invokes `AgentPrompt` (Run Routine) with\n `prompt: magazine.routine` and the candidate JSON `input`; it decodes the\n `completeJob` structured output and applies selections mechanically (keep-bound,\n per-post snippet/imageUrl). No ephemeral routine is constructed.\n- **Instruction split.** Skill = HOW (FetchArticleContent + completeJob\n contract). Routine = editorial (selection criteria, dedup, snippet style,\n hero-image choice) + topic. This removes the prior overlap where the skill\n held both.\n- **Properties editing.** The `routine` field carries `FormInlineAnnotation`,\n so the Routine's own fields (name, description, instructions) render inline in\n the generic `DefaultProperties` companion — no custom surface needed.\n- **Template.** `Template` is retained (it is the type of `Skill.instructions`\n and is used across the codebase). The Magazine simply stops using it; the\n Routine's `instructions` is a plain `Ref<Text>`.\n\n### Content Flow\n\nTwo curation paths exist side-by-side:\n\n- **Button path (deterministic).** The Curate toolbar button runs `SyncFeed`\n per referenced feed, then invokes `CurateMagazine`. Snippets and images are\n derived from each Post's existing `description` field (no HTTP, no LLM).\n Chosen for the user-initiated button because it is fast, reliable, and does\n not depend on an LLM runtime being present.\n- **Agent path (skill).** The `MagazineSkill` exposes three fine-grained\n tools (`ListCandidatePosts`, `FetchArticleContent`, `AddPostToMagazine`) and\n an instruction template that tells an agent to consult `Magazine.instructions`\n when selecting Posts. Triggered when an agent chat references the Magazine —\n not when the button is clicked. Delivers higher-quality curation (full-article\n fetch + LLM-driven selection) at the cost of latency and LLM availability.\n\n`SyncFeed` is always run deterministically (either by the button or outside\nthe scope of this feature). Neither curation path fetches feed indices on its\nown — that is `SyncFeed`'s job.\n\n### Tool Granularity\n\nThree tools rather than one combined `curateMagazine` tool:\n\n- `ListCandidatePosts` returns lightweight summaries so the agent can triage\n cheaply without loading article bodies.\n- `FetchArticleContent` is a read-only HTTP fetch + extract; it is reusable by\n the future `TagPosts` operation which will need the same inputs.\n- `AddPostToMagazine` is the sole mutation. Idempotence lives in this tool\n (by ref-set membership check) rather than in the prompt.\n\nQuality of selection is the agent's job; the tools deliberately don't try to\nrank or filter on the agent's behalf.\n\n### Snippet Storage\n\n`Post.snippet` is a plain inline string, not a `Ref<Text>`. Snippets are\nshort (~200 chars target) and read-only to the user; the `Ref<Text>` indirection\nused for `Agent.instructions` and `Magazine.instructions` is appropriate for\nlong-form, user-editable content and is not needed here.\n\n### Read State\n\nRepresented as an optional ISO timestamp `readAt`, not a boolean or enum.\nAbsence = unread; presence = read (at that time). This leaves room to show\n\"read 2 days ago\" or to expire reads without a schema change.\n\n### Image Extraction\n\n`FetchArticleContent` returns a list of candidate image URLs; the agent picks\nthe \"best\" one for the Post's `imageUrl`. The prompt should prefer `og:image`\nwhen present. If no suitable image exists the Post is still added with\n`imageUrl` undefined — `MagazineTile` renders without the image region.\n\nHTML parsing uses whatever parser the existing codebase already uses for\nsimilar tasks. If no parser is in use, `linkedom` is the proposed addition —\nsmall, no-DOM-globals footprint, works in Node and browser. Addition of a\nparser dependency is a decision point flagged during implementation planning.\n\n### Deduplication Policy\n\nDeduplication is keyed on Post ref identity, not on URL or guid. If a Post is\nremoved from the Magazine and later re-curated, the agent will re-add it.\nDedup across a Feed's historical sync is out of scope — `Feed` already\ndeduplicates on `guid` when syncing.\n\n### Relationship to Future Tagging\n\nThe tagging feature is strictly additive to this design:\n\n- `Post.tags` already exists as an optional string array.\n- `FetchArticleContent` is the reusable read-stage for tagging.\n- `MagazineTile` renders a tags row when non-empty — no UI change needed.\n- A `TagPosts` operation and either an extended prompt in `MagazineSkill`\n or a second `MagazineTagger` skill will complete the feature in a later\n revision.\n\nFurther future work enabled by tags (all out of scope here): tag-filter chips\nin the Magazine toolbar; a \"Find more like this\" action prompting the agent\nto curate Posts with overlapping tags; a \"Suggest feeds\" action proposing new\nFeed URLs based on accumulated tag distribution.\n\n### Out of Scope\n\n- Rendering the full original article body inside the reader (would require\n storing full extracted text; current design stores only the snippet).\n- Scheduled / background Curate runs — this revision only supports the\n explicit toolbar-triggered flow.\n- Cross-Magazine deduplication — two Magazines may both curate the same Post.\n- Editing or regenerating a snippet after curation — re-curation is the path\n to different content.\n",
19
+ mimeType: "application/x-mdl"
20
+ } }), AppPlugin.addSchemaModule({ schema: [
21
+ Subscription.Subscription,
22
+ Subscription.Post,
23
+ Subscription.PostContent,
24
+ Magazine.Magazine,
25
+ Instructions.Instructions,
26
+ StateMap.StateMap,
27
+ TagIndex.TagIndex
28
+ ] }), AppPlugin.addSkillDefinitionModule({ activate: SkillDefinition }), AppPlugin.addSurfaceModule({ activate: ReactSurface }), AppPlugin.addTranslationsModule({ translations }), Plugin.addModule({
29
+ id: "magazine-automation-templates",
30
+ activatesOn: AppActivationEvents.SetupSchema,
31
+ activate: RoutineTemplates
32
+ }), Plugin.make);
33
+ //#endregion
34
+ export { MagazinePlugin, MagazinePlugin as default };
35
+
36
+ //# sourceMappingURL=MagazinePlugin.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MagazinePlugin.mjs","names":[],"sources":["../../src/MagazinePlugin.tsx"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport { ActivationEvent, Plugin } from '@dxos/app-framework';\nimport { AppActivationEvents, AppPlugin } from '@dxos/app-toolkit';\nimport { Instructions } from '@dxos/compute';\nimport { AttentionEvents } from '@dxos/plugin-attention';\nimport { StateMap, TagIndex } from '@dxos/schema';\n\nimport {\n AppGraphBuilder,\n CreateObject,\n OperationHandler,\n ReactSurface,\n RoutineTemplates,\n SkillDefinition,\n} from '#capabilities';\nimport { meta } from '#meta';\nimport { translations } from '#translations';\nimport { Magazine, Subscription } from '#types';\n\n// eslint-disable-next-line import/no-relative-packages\nimport pluginSpec from '../PLUGIN.mdl?raw';\n\nexport const MagazinePlugin = Plugin.define(meta).pipe(\n AppPlugin.addAppGraphModule({\n activatesOn: ActivationEvent.allOf(AppActivationEvents.SetupAppGraph, AttentionEvents.AttentionReady),\n activate: AppGraphBuilder,\n }),\n AppPlugin.addCreateObjectModule({ activate: CreateObject }),\n AppPlugin.addOperationHandlerModule({ activate: OperationHandler }),\n AppPlugin.addPluginAssetModule({\n asset: { pluginId: meta.profile.key, path: 'PLUGIN.mdl', content: pluginSpec, mimeType: 'application/x-mdl' },\n }),\n AppPlugin.addSchemaModule({\n schema: [\n Subscription.Subscription,\n Subscription.Post,\n Subscription.PostContent,\n Magazine.Magazine,\n Instructions.Instructions,\n StateMap.StateMap,\n TagIndex.TagIndex,\n ],\n }),\n AppPlugin.addSkillDefinitionModule({ activate: SkillDefinition }),\n AppPlugin.addSurfaceModule({ activate: ReactSurface }),\n AppPlugin.addTranslationsModule({ translations }),\n Plugin.addModule({\n id: 'magazine-automation-templates',\n activatesOn: AppActivationEvents.SetupSchema,\n activate: RoutineTemplates,\n }),\n Plugin.make,\n);\n\nexport default MagazinePlugin;\n"],"mappings":";;;;;;;;;;;AAyBA,IAAa,iBAAiB,OAAO,OAAO,IAAI,CAAC,CAAC,KAChD,UAAU,kBAAkB;CAC1B,aAAa,gBAAgB,MAAM,oBAAoB,eAAe,gBAAgB,cAAc;CACpG,UAAU;AACZ,CAAC,GACD,UAAU,sBAAsB,EAAE,UAAU,aAAa,CAAC,GAC1D,UAAU,0BAA0B,EAAE,UAAU,iBAAiB,CAAC,GAClE,UAAU,qBAAqB,EAC7B,OAAO;CAAE,UAAU,KAAK,QAAQ;CAAK,MAAM;CAAc,SAAS;CAAY,UAAU;AAAoB,EAC9G,CAAC,GACD,UAAU,gBAAgB,EACxB,QAAQ;CACN,aAAa;CACb,aAAa;CACb,aAAa;CACb,SAAS;CACT,aAAa;CACb,SAAS;CACT,SAAS;AACX,EACF,CAAC,GACD,UAAU,yBAAyB,EAAE,UAAU,gBAAgB,CAAC,GAChE,UAAU,iBAAiB,EAAE,UAAU,aAAa,CAAC,GACrD,UAAU,sBAAsB,EAAE,aAAa,CAAC,GAChD,OAAO,UAAU;CACf,IAAI;CACJ,aAAa,oBAAoB;CACjC,UAAU;AACZ,CAAC,GACD,OAAO,IACT"}
@@ -0,0 +1,26 @@
1
+ import { t as operation_handler_default } from "./chunk-operation-handler.mjs";
2
+ import { t as skillDefinition } from "./chunk-skill-definition.mjs";
3
+ import { Plugin } from "@dxos/app-framework";
4
+ import { StateMap, TagIndex } from "@dxos/schema";
5
+ import { meta } from "#meta";
6
+ import { AppPlugin } from "@dxos/app-toolkit";
7
+ import { Magazine, Subscription } from "#types";
8
+ //#region src/MagazinePlugin.workerd.ts
9
+ var MagazinePlugin = Plugin.define(meta).pipe(AppPlugin.addSkillDefinitionModule({
10
+ id: "skill-definition",
11
+ activate: skillDefinition
12
+ }), AppPlugin.addOperationHandlerModule({
13
+ id: "operation-handler",
14
+ activate: operation_handler_default
15
+ }), AppPlugin.addSchemaModule({ schema: [
16
+ Subscription.Subscription,
17
+ Subscription.Post,
18
+ Subscription.PostContent,
19
+ Magazine.Magazine,
20
+ StateMap.StateMap,
21
+ TagIndex.TagIndex
22
+ ] }), Plugin.make);
23
+ //#endregion
24
+ export { MagazinePlugin, MagazinePlugin as default };
25
+
26
+ //# sourceMappingURL=MagazinePlugin.workerd.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MagazinePlugin.workerd.mjs","names":[],"sources":["../../src/MagazinePlugin.workerd.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { Plugin } from '@dxos/app-framework';\nimport { AppPlugin } from '@dxos/app-toolkit';\nimport { StateMap, TagIndex } from '@dxos/schema';\n\nimport { meta } from '#meta';\nimport { Magazine, Subscription } from '#types';\n\nimport OperationHandler from './capabilities/operation-handler';\nimport SkillDefinition from './capabilities/skill-definition';\n\nexport const MagazinePlugin = Plugin.define(meta).pipe(\n AppPlugin.addSkillDefinitionModule({ id: 'skill-definition', activate: SkillDefinition }),\n AppPlugin.addOperationHandlerModule({ id: 'operation-handler', activate: OperationHandler }),\n AppPlugin.addSchemaModule({\n schema: [\n Subscription.Subscription,\n Subscription.Post,\n Subscription.PostContent,\n Magazine.Magazine,\n StateMap.StateMap,\n TagIndex.TagIndex,\n ],\n }),\n Plugin.make,\n);\n\nexport default MagazinePlugin;\n"],"mappings":";;;;;;;;AAcA,IAAa,iBAAiB,OAAO,OAAO,IAAI,CAAC,CAAC,KAChD,UAAU,yBAAyB;CAAE,IAAI;CAAoB,UAAU;AAAgB,CAAC,GACxF,UAAU,0BAA0B;CAAE,IAAI;CAAqB,UAAU;AAAiB,CAAC,GAC3F,UAAU,gBAAgB,EACxB,QAAQ;CACN,aAAa;CACb,aAAa;CACb,aAAa;CACb,SAAS;CACT,SAAS;CACT,SAAS;AACX,EACF,CAAC,GACD,OAAO,IACT"}
@@ -0,0 +1,175 @@
1
+ import { f as SYSTEM_TAGS, v as pickLatestPostContent, y as queryPostContentForPost } from "./chunk-types.mjs";
2
+ import { n as publishedTimestamp } from "./chunk-date.mjs";
3
+ import { n as getSnippet, t as getImageUrl } from "./chunk-post-content.mjs";
4
+ import { Filter, Obj, Tag } from "@dxos/echo";
5
+ import { StateMap, TagIndex } from "@dxos/schema";
6
+ import { Atom, useAtomValue } from "@effect-atom/atom-react";
7
+ import * as Data from "effect/Data";
8
+ //#region src/atoms/post-tags.ts
9
+ var EMPTY_TAG_SLICE = {
10
+ starred: false,
11
+ archived: false
12
+ };
13
+ var EMPTY_TAG_URIS = {
14
+ starredUri: void 0,
15
+ archivedUri: void 0
16
+ };
17
+ /** Resolve the uri of a system Tag by foreign key (mirrors `useSystemTags`). */
18
+ var uriForTag = (tags, key) => {
19
+ const match = tags.find((tag) => Obj.getKeys(tag, key.source).some((foreignKey) => foreignKey.id === key.id));
20
+ return match ? Obj.getURI(match).toString() : void 0;
21
+ };
22
+ /**
23
+ * Resolved uris of the `starred` / `archived` system Tags, keyed by database. Fires only when the
24
+ * Tag set changes (rare — on first star/archive). Shared by every per-Post tag slice.
25
+ */
26
+ var tagUrisAtom = Atom.family((db) => db.query(Filter.type(Tag.Tag)).atom.pipe(Atom.map((tags) => ({
27
+ starredUri: uriForTag(tags, SYSTEM_TAGS.starred.key),
28
+ archivedUri: uriForTag(tags, SYSTEM_TAGS.archived.key)
29
+ }))));
30
+ /**
31
+ * This Post's `{ starred, archived }`, sliced off its source Subscription's `tags`. Fires only
32
+ * when this Post's tag membership flips — sibling Posts' tag mutations are discarded without
33
+ * propagating. Re-initialises its tag subscription when tag uris change (first star ever in the space).
34
+ */
35
+ var postTagsAtom = Atom.family((post) => Atom.make((get) => {
36
+ const ref = post.source;
37
+ if (!ref) return EMPTY_TAG_SLICE;
38
+ const subscription = get(ref.atom);
39
+ if (!subscription) return EMPTY_TAG_SLICE;
40
+ const tagIndex = get(subscription.tags.atom);
41
+ if (!tagIndex) return EMPTY_TAG_SLICE;
42
+ const db = Obj.getDatabase(subscription);
43
+ const { starredUri, archivedUri } = db ? get(tagUrisAtom(db)) : EMPTY_TAG_URIS;
44
+ return {
45
+ starred: get(TagIndex.atom(tagIndex, post.id, starredUri)),
46
+ archived: get(TagIndex.atom(tagIndex, post.id, archivedUri))
47
+ };
48
+ }).pipe(Atom.keepAlive));
49
+ /** This Post's tag slice (star/archive). */
50
+ var useTagState = (post) => useAtomValue(postTagsAtom(post));
51
+ //#endregion
52
+ //#region src/atoms/magazine-posts.ts
53
+ /**
54
+ * Ordered Posts referenced by the Magazine, derived directly from the `magazine.posts` refs — no
55
+ * query. Fires when the refs array changes (membership/order) or when a ref resolves. Resolving each
56
+ * ref here, rather than a `reference('posts').from(Scope.space(...))` query, avoids the cross-feed
57
+ * fan-out that returned each curated Post once per feed scope.
58
+ */
59
+ var magazinePostsAtom = Atom.family((magazine) => Atom.make((get) => {
60
+ return [...(get(Obj.atomProperty(magazine, "posts")) ?? []).map((ref) => get(ref.atom)).filter((post) => Boolean(post))].sort((postA, postB) => publishedTimestamp(postB.published) - publishedTimestamp(postA.published));
61
+ }));
62
+ /**
63
+ * Whether a Post is visible under a given view. Depends ONLY on the Post's tag slice + view, so
64
+ * read-state changes never wake it (and effect-atom dedupes the boolean). Keyed by a value-equal
65
+ * `Data.tuple([post, view])` rather than a nested family — the intermediate of a nested family is
66
+ * only weakly held and could be GC'd out from under its mounted leaf atoms.
67
+ */
68
+ var postVisibilityAtom = Atom.family((key) => Atom.make((get) => {
69
+ const [post, view] = key;
70
+ const { starred, archived } = get(postTagsAtom(post));
71
+ switch (view) {
72
+ case "archived": return archived;
73
+ case "starred": return !archived && starred;
74
+ default: return !archived;
75
+ }
76
+ }));
77
+ /**
78
+ * Ordered, view-filtered Posts for a Magazine. Re-runs on membership change OR a Post crossing the
79
+ * view's filter boundary (star/archive); never on read-state changes. Keyed by a value-equal
80
+ * `Data.tuple([magazine, view])`.
81
+ */
82
+ var visibleMagazinePostsAtom = Atom.family((key) => Atom.make((get) => {
83
+ const [magazine, view] = key;
84
+ return get(magazinePostsAtom(magazine)).filter((post) => get(postVisibilityAtom(Data.tuple(post, view))));
85
+ }));
86
+ /** Ordered, view-filtered Posts for a Magazine. */
87
+ var useVisibleMagazinePosts = (magazine, view) => useAtomValue(visibleMagazinePostsAtom(Data.tuple(magazine, view)));
88
+ //#endregion
89
+ //#region src/atoms/post-content.ts
90
+ /** This Post's fetched body (newest {@link Subscription.PostContent} entry), or undefined. */
91
+ var postContentAtom = Atom.family((post) => Atom.make((get) => {
92
+ const ref = post.source;
93
+ if (!ref) return;
94
+ const subscription = get(ref.atom);
95
+ if (!subscription) return;
96
+ const db = Obj.getDatabase(subscription);
97
+ const query = queryPostContentForPost(subscription, post);
98
+ if (!db || !query) return;
99
+ return pickLatestPostContent(get(db.query(query).atom));
100
+ }).pipe(Atom.keepAlive));
101
+ /** This Post's fetched body, reactively. */
102
+ var usePostContentAtom = (post) => useAtomValue(postContentAtom(post));
103
+ //#endregion
104
+ //#region src/atoms/post-curation.ts
105
+ var EMPTY_CURATION_SLICE = {
106
+ snippet: void 0,
107
+ imageUrl: void 0
108
+ };
109
+ /**
110
+ * This Post's magazine-scoped curation slice (snippet/imageUrl), sliced off the Magazine's
111
+ * `postState`. Fires only when this Post's slice changes — sibling Posts' curation mutations are
112
+ * discarded without propagating. Keyed by a value-equal `Data.tuple([post, magazine])`.
113
+ */
114
+ var postCurationAtom = Atom.family((key) => Atom.make((get) => {
115
+ const [post, magazine] = key;
116
+ const stateMap = get(magazine.postState.atom);
117
+ if (!stateMap) return EMPTY_CURATION_SLICE;
118
+ const state = get(StateMap.atom(stateMap, post.id));
119
+ return {
120
+ snippet: state.snippet,
121
+ imageUrl: state.imageUrl
122
+ };
123
+ }).pipe(Atom.keepAlive));
124
+ /** This Post's magazine-scoped curation slice. */
125
+ var usePostCuration = (post, magazine) => useAtomValue(postCurationAtom(Data.tuple(post, magazine)));
126
+ //#endregion
127
+ //#region src/atoms/post-read.ts
128
+ var EMPTY_READ_SLICE = { readAt: void 0 };
129
+ /**
130
+ * This Post's `readAt`, sliced off its source Subscription's `postState`. Fires only when this
131
+ * Post's `readAt` changes — sibling Posts' mutations are discarded without propagating.
132
+ */
133
+ var postReadAtom = Atom.family((post) => Atom.make((get) => {
134
+ const ref = post.source;
135
+ if (!ref) return EMPTY_READ_SLICE;
136
+ const subscription = get(ref.atom);
137
+ if (!subscription) return EMPTY_READ_SLICE;
138
+ const stateMap = get(subscription.postState.atom);
139
+ if (!stateMap) return EMPTY_READ_SLICE;
140
+ return { readAt: get(StateMap.atom(stateMap, post.id)).readAt };
141
+ }).pipe(Atom.keepAlive));
142
+ /** This Post's read slice. */
143
+ var useReadState = (post) => useAtomValue(postReadAtom(post));
144
+ //#endregion
145
+ //#region src/atoms/post-display.ts
146
+ /**
147
+ * Aggregate display data for one tile within a magazine. Fires only on this Post's
148
+ * read/star/feed-name/curation changes. Keyed by a value-equal `Data.tuple([post, magazine])`.
149
+ *
150
+ * Snippet and imageUrl precedence: agent-written postState > RSS-derived description fallback.
151
+ */
152
+ var postDisplayAtom = Atom.family((key) => Atom.make((get) => {
153
+ const [post, magazine] = key;
154
+ const snapshot = get(Obj.atom(post));
155
+ const ref = post.source;
156
+ const subscription = ref ? get(ref.atom) : void 0;
157
+ const feedName = subscription ? get(Obj.atomProperty(subscription, "name")) || get(Obj.atomProperty(subscription, "url")) : void 0;
158
+ const { readAt } = get(postReadAtom(post));
159
+ const { starred } = get(postTagsAtom(post));
160
+ const curation = get(postCurationAtom(Data.tuple(post, magazine)));
161
+ return {
162
+ post: snapshot,
163
+ feedName,
164
+ read: readAt !== void 0,
165
+ starred,
166
+ snippet: curation.snippet ?? getSnippet(snapshot),
167
+ imageUrl: curation.imageUrl ?? getImageUrl(snapshot)
168
+ };
169
+ }).pipe(Atom.keepAlive));
170
+ /** Aggregate per-Post display data for a magazine tile. */
171
+ var useMagazinePostData = (post, magazine) => useAtomValue(postDisplayAtom(Data.tuple(post, magazine)));
172
+ //#endregion
173
+ export { postContentAtom, postCurationAtom, postDisplayAtom, postReadAtom, postTagsAtom, useMagazinePostData, usePostContentAtom, usePostCuration, useReadState, useTagState, useVisibleMagazinePosts, visibleMagazinePostsAtom };
174
+
175
+ //# sourceMappingURL=atoms.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atoms.mjs","names":[],"sources":["../../src/atoms/post-tags.ts","../../src/atoms/magazine-posts.ts","../../src/atoms/post-content.ts","../../src/atoms/post-curation.ts","../../src/atoms/post-read.ts","../../src/atoms/post-display.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { Atom, useAtomValue } from '@effect-atom/atom-react';\n\nimport { type Database, Filter, Obj, Tag } from '@dxos/echo';\nimport { TagIndex } from '@dxos/schema';\n\nimport { Subscription } from '../types';\n\n/** Per-Post tag slice (star/archive membership). */\nexport type TagSlice = { starred: boolean; archived: boolean };\n\nconst EMPTY_TAG_SLICE: TagSlice = { starred: false, archived: false };\nconst EMPTY_TAG_URIS = { starredUri: undefined, archivedUri: undefined } as const;\n\n/** Resolve the uri of a system Tag by foreign key (mirrors `useSystemTags`). */\nconst uriForTag = (tags: readonly Tag.Tag[], key: { source: string; id: string }): string | undefined => {\n const match = tags.find((tag) => Obj.getKeys(tag, key.source).some((foreignKey) => foreignKey.id === key.id));\n return match ? Obj.getURI(match).toString() : undefined;\n};\n\n/**\n * Resolved uris of the `starred` / `archived` system Tags, keyed by database. Fires only when the\n * Tag set changes (rare — on first star/archive). Shared by every per-Post tag slice.\n */\nconst tagUrisAtom = Atom.family((db: Database.Database) =>\n db.query(Filter.type(Tag.Tag)).atom.pipe(\n Atom.map((tags) => ({\n starredUri: uriForTag(tags, Subscription.SYSTEM_TAGS.starred.key),\n archivedUri: uriForTag(tags, Subscription.SYSTEM_TAGS.archived.key),\n })),\n ),\n);\n\n/**\n * This Post's `{ starred, archived }`, sliced off its source Subscription's `tags`. Fires only\n * when this Post's tag membership flips — sibling Posts' tag mutations are discarded without\n * propagating. Re-initialises its tag subscription when tag uris change (first star ever in the space).\n */\nexport const postTagsAtom = Atom.family((post: Subscription.Post) =>\n Atom.make<TagSlice>((get) => {\n const ref = post.source;\n if (!ref) {\n return EMPTY_TAG_SLICE;\n }\n const subscription = get(ref.atom);\n if (!subscription) {\n return EMPTY_TAG_SLICE;\n }\n const tagIndex = get(subscription.tags.atom);\n if (!tagIndex) {\n return EMPTY_TAG_SLICE;\n }\n const db = Obj.getDatabase(subscription);\n const { starredUri, archivedUri } = db ? get(tagUrisAtom(db)) : EMPTY_TAG_URIS;\n return {\n starred: get(TagIndex.atom(tagIndex, post.id, starredUri)),\n archived: get(TagIndex.atom(tagIndex, post.id, archivedUri)),\n };\n }).pipe(Atom.keepAlive),\n);\n\n/** This Post's tag slice (star/archive). */\nexport const useTagState = (post: Subscription.Post): TagSlice => useAtomValue(postTagsAtom(post));\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { Atom, useAtomValue } from '@effect-atom/atom-react';\nimport * as Data from 'effect/Data';\n\nimport { Obj } from '@dxos/echo';\n\nimport { type Magazine, type Subscription } from '../types';\nimport { publishedTimestamp } from '../util/date';\nimport { postTagsAtom } from './post-tags';\n\n/**\n * Tile filter mode. Mutually exclusive — `default` shows everything except archived,\n * `starred` filters to starred posts, `archived` shows only archived posts.\n */\nexport type MagazineView = 'default' | 'starred' | 'archived';\n\n/**\n * Ordered Posts referenced by the Magazine, derived directly from the `magazine.posts` refs — no\n * query. Fires when the refs array changes (membership/order) or when a ref resolves. Resolving each\n * ref here, rather than a `reference('posts').from(Scope.space(...))` query, avoids the cross-feed\n * fan-out that returned each curated Post once per feed scope.\n */\nconst magazinePostsAtom = Atom.family((magazine: Magazine.Magazine) =>\n Atom.make<Subscription.Post[]>((get) => {\n const refs = get(Obj.atomProperty(magazine, 'posts')) ?? [];\n const posts = refs.map((ref) => get(ref.atom)).filter((post): post is Subscription.Post => Boolean(post));\n return [...posts].sort((postA, postB) => publishedTimestamp(postB.published) - publishedTimestamp(postA.published));\n }),\n);\n\n/**\n * Whether a Post is visible under a given view. Depends ONLY on the Post's tag slice + view, so\n * read-state changes never wake it (and effect-atom dedupes the boolean). Keyed by a value-equal\n * `Data.tuple([post, view])` rather than a nested family — the intermediate of a nested family is\n * only weakly held and could be GC'd out from under its mounted leaf atoms.\n */\nconst postVisibilityAtom = Atom.family((key: readonly [Subscription.Post, MagazineView]) =>\n Atom.make<boolean>((get) => {\n const [post, view] = key;\n const { starred, archived } = get(postTagsAtom(post));\n switch (view) {\n case 'archived':\n return archived;\n case 'starred':\n return !archived && starred;\n default:\n return !archived;\n }\n }),\n);\n\n/**\n * Ordered, view-filtered Posts for a Magazine. Re-runs on membership change OR a Post crossing the\n * view's filter boundary (star/archive); never on read-state changes. Keyed by a value-equal\n * `Data.tuple([magazine, view])`.\n */\nexport const visibleMagazinePostsAtom = Atom.family((key: readonly [Magazine.Magazine, MagazineView]) =>\n Atom.make<Subscription.Post[]>((get) => {\n const [magazine, view] = key;\n const posts = get(magazinePostsAtom(magazine));\n return posts.filter((post) => get(postVisibilityAtom(Data.tuple(post, view))));\n }),\n);\n\n/** Ordered, view-filtered Posts for a Magazine. */\nexport const useVisibleMagazinePosts = (magazine: Magazine.Magazine, view: MagazineView): Subscription.Post[] =>\n useAtomValue(visibleMagazinePostsAtom(Data.tuple(magazine, view)));\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { Atom, useAtomValue } from '@effect-atom/atom-react';\n\nimport { Obj } from '@dxos/echo';\n\nimport { Subscription } from '../types';\n\n/** This Post's fetched body (newest {@link Subscription.PostContent} entry), or undefined. */\nexport const postContentAtom = Atom.family((post: Subscription.Post) =>\n Atom.make<Subscription.PostContent | undefined>((get) => {\n const ref = post.source;\n if (!ref) {\n return undefined;\n }\n const subscription = get(ref.atom);\n if (!subscription) {\n return undefined;\n }\n const db = Obj.getDatabase(subscription);\n const query = Subscription.queryPostContentForPost(subscription, post);\n if (!db || !query) {\n return undefined;\n }\n const entries = get(db.query(query).atom);\n return Subscription.pickLatestPostContent(entries);\n }).pipe(Atom.keepAlive),\n);\n\n/** This Post's fetched body, reactively. */\nexport const usePostContentAtom = (post: Subscription.Post): Subscription.PostContent | undefined =>\n useAtomValue(postContentAtom(post));\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { Atom, useAtomValue } from '@effect-atom/atom-react';\nimport * as Data from 'effect/Data';\n\nimport { StateMap } from '@dxos/schema';\n\nimport { Magazine, type Subscription } from '../types';\n\n/** Per-Post magazine-scoped curation slice (agent-written snippet/hero image). */\nexport type CurationSlice = { snippet: string | undefined; imageUrl: string | undefined };\n\nconst EMPTY_CURATION_SLICE: CurationSlice = { snippet: undefined, imageUrl: undefined };\n\n/**\n * This Post's magazine-scoped curation slice (snippet/imageUrl), sliced off the Magazine's\n * `postState`. Fires only when this Post's slice changes — sibling Posts' curation mutations are\n * discarded without propagating. Keyed by a value-equal `Data.tuple([post, magazine])`.\n */\nexport const postCurationAtom = Atom.family((key: readonly [Subscription.Post, Magazine.Magazine]) =>\n Atom.make<CurationSlice>((get) => {\n const [post, magazine] = key;\n const stateMap = get(magazine.postState.atom);\n if (!stateMap) {\n return EMPTY_CURATION_SLICE;\n }\n const state = get(StateMap.atom<Magazine.PostState>(stateMap, post.id));\n return { snippet: state.snippet, imageUrl: state.imageUrl };\n }).pipe(Atom.keepAlive),\n);\n\n/** This Post's magazine-scoped curation slice. */\nexport const usePostCuration = (post: Subscription.Post, magazine: Magazine.Magazine): CurationSlice =>\n useAtomValue(postCurationAtom(Data.tuple(post, magazine)));\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { Atom, useAtomValue } from '@effect-atom/atom-react';\n\nimport { StateMap } from '@dxos/schema';\n\nimport { Subscription } from '../types';\n\n/** Per-Post read slice. */\nexport type ReadSlice = { readAt: string | undefined };\n\nconst EMPTY_READ_SLICE: ReadSlice = { readAt: undefined };\n\n/**\n * This Post's `readAt`, sliced off its source Subscription's `postState`. Fires only when this\n * Post's `readAt` changes — sibling Posts' mutations are discarded without propagating.\n */\nexport const postReadAtom = Atom.family((post: Subscription.Post) =>\n Atom.make<ReadSlice>((get) => {\n const ref = post.source;\n if (!ref) {\n return EMPTY_READ_SLICE;\n }\n const subscription = get(ref.atom);\n if (!subscription) {\n return EMPTY_READ_SLICE;\n }\n const stateMap = get(subscription.postState.atom);\n if (!stateMap) {\n return EMPTY_READ_SLICE;\n }\n const state = get(StateMap.atom<Subscription.PostState>(stateMap, post.id));\n return { readAt: state.readAt };\n }).pipe(Atom.keepAlive),\n);\n\n/** This Post's read slice. */\nexport const useReadState = (post: Subscription.Post): ReadSlice => useAtomValue(postReadAtom(post));\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { Atom, useAtomValue } from '@effect-atom/atom-react';\nimport * as Data from 'effect/Data';\n\nimport { Obj } from '@dxos/echo';\n\nimport { type Magazine, type Subscription } from '../types';\nimport { getImageUrl, getSnippet } from '../util/post-content';\nimport { postCurationAtom } from './post-curation';\nimport { postReadAtom } from './post-read';\nimport { postTagsAtom } from './post-tags';\n\n/** Aggregate per-Post display data folded from the Post + its source Subscription's per-Post slice. */\nexport type MagazinePostData = {\n post: Obj.Snapshot<Subscription.Post>;\n feedName: string | undefined;\n read: boolean;\n starred: boolean;\n snippet: string;\n imageUrl: string | undefined;\n};\n\n/**\n * Aggregate display data for one tile within a magazine. Fires only on this Post's\n * read/star/feed-name/curation changes. Keyed by a value-equal `Data.tuple([post, magazine])`.\n *\n * Snippet and imageUrl precedence: agent-written postState > RSS-derived description fallback.\n */\nexport const postDisplayAtom = Atom.family((key: readonly [Subscription.Post, Magazine.Magazine]) =>\n Atom.make<MagazinePostData>((get) => {\n const [post, magazine] = key;\n const snapshot = get(Obj.atom(post));\n const ref = post.source;\n const subscription = ref ? get(ref.atom) : undefined;\n // `name`/`url` via property atoms — fire only on name/url change (e.g. after first sync).\n const feedName = subscription\n ? get(Obj.atomProperty(subscription, 'name')) || get(Obj.atomProperty(subscription, 'url'))\n : undefined;\n const { readAt } = get(postReadAtom(post));\n const { starred } = get(postTagsAtom(post));\n // Agent-written snippet/imageUrl (granular per-Post slice); fall back to description derivation.\n const curation = get(postCurationAtom(Data.tuple(post, magazine)));\n return {\n post: snapshot,\n feedName,\n read: readAt !== undefined,\n starred,\n snippet: curation.snippet ?? getSnippet(snapshot),\n imageUrl: curation.imageUrl ?? getImageUrl(snapshot),\n };\n }).pipe(Atom.keepAlive),\n);\n\n/** Aggregate per-Post display data for a magazine tile. */\nexport const useMagazinePostData = (post: Subscription.Post, magazine: Magazine.Magazine): MagazinePostData =>\n useAtomValue(postDisplayAtom(Data.tuple(post, magazine)));\n"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { Capability } from "@dxos/app-framework";
2
+ //#region src/capabilities/index.ts
3
+ var AppGraphBuilder = Capability.lazy("AppGraphBuilder", () => import("./chunk-app-graph-builder.mjs"));
4
+ var RoutineTemplates = Capability.lazy("RoutineTemplates", () => import("./chunk-routine-templates.mjs"));
5
+ var SkillDefinition = Capability.lazy("SkillDefinition", () => import("./chunk-skill-definition.mjs").then((n) => n.n));
6
+ var CreateObject = Capability.lazy("CreateObject", () => import("./chunk-create-object.mjs"));
7
+ var OperationHandler = Capability.lazy("OperationHandler", () => import("./chunk-operation-handler.mjs").then((n) => n.n));
8
+ var ReactSurface = Capability.lazy("ReactSurface", () => import("./chunk-react-surface.mjs"));
9
+ //#endregion
10
+ export { AppGraphBuilder, CreateObject, OperationHandler, ReactSurface, RoutineTemplates, SkillDefinition };
11
+
12
+ //# sourceMappingURL=capabilities.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.mjs","names":[],"sources":["../../src/capabilities/index.ts"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport { Capability } from '@dxos/app-framework';\nimport type { OperationHandlerSet } from '@dxos/compute';\n\nexport const AppGraphBuilder = Capability.lazy('AppGraphBuilder', () => import('./app-graph-builder'));\nexport const RoutineTemplates = Capability.lazy('RoutineTemplates', () => import('./routine-templates'));\nexport const SkillDefinition = Capability.lazy('SkillDefinition', () => import('./skill-definition'));\nexport const CreateObject = Capability.lazy('CreateObject', () => import('./create-object'));\nexport const OperationHandler = Capability.lazy<OperationHandlerSet.OperationHandlerSet>(\n 'OperationHandler',\n () => import('./operation-handler'),\n);\nexport const ReactSurface = Capability.lazy('ReactSurface', () => import('./react-surface'));\n"],"mappings":";;AAOA,IAAa,kBAAkB,WAAW,KAAK,yBAAyB,OAAO,gCAAsB;AACrG,IAAa,mBAAmB,WAAW,KAAK,0BAA0B,OAAO,gCAAsB;AACvG,IAAa,kBAAkB,WAAW,KAAK,yBAAyB,OAAO,+BAAqB,CAAA,MAAA,MAAA,EAAA,CAAA,CAAA;AACpG,IAAa,eAAe,WAAW,KAAK,sBAAsB,OAAO,4BAAkB;AAC3F,IAAa,mBAAmB,WAAW,KACzC,0BACM,OAAO,gCACf,CAAA,MAAA,MAAA,EAAA,CAAA,CAAA;AACA,IAAa,eAAe,WAAW,KAAK,sBAAsB,OAAO,4BAAkB"}
@@ -0,0 +1,70 @@
1
+ import { Filter, Obj, Query, Ref, Scope } from "@dxos/echo";
2
+ import { meta } from "#meta";
3
+ import { FeedOperation, Subscription } from "#types";
4
+ import { useCallback, useState } from "react";
5
+ import { Panel } from "@dxos/react-ui";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { useOperationInvoker } from "@dxos/app-framework/ui";
8
+ import { useObject, useQuery } from "@dxos/echo-react";
9
+ import { PostStack } from "#components";
10
+ import { Menu, MenuBuilder, useMenuBuilder } from "@dxos/react-ui-menu";
11
+ //#region src/containers/FeedArticle/FeedToolbar.tsx
12
+ /**
13
+ * Reactive feed toolbar built from the menu action-graph idiom: a single Sync action. Matches the
14
+ * magazine/post toolbars; the feed's name is surfaced by the plank chrome, not duplicated here.
15
+ */
16
+ var FeedToolbar = ({ attendableId, onSync }) => {
17
+ const menuActions = useMenuBuilder(() => MenuBuilder.make().action("sync", {
18
+ label: ["sync-feed.label", { ns: meta.profile.key }],
19
+ icon: "ph--arrows-clockwise--regular",
20
+ iconOnly: true
21
+ }, onSync).build(), [onSync]);
22
+ return /* @__PURE__ */ jsx(Menu.Root, {
23
+ ...menuActions,
24
+ attendableId,
25
+ children: /* @__PURE__ */ jsx(Panel.Toolbar, {
26
+ asChild: true,
27
+ children: /* @__PURE__ */ jsx(Menu.Toolbar, {})
28
+ })
29
+ });
30
+ };
31
+ FeedToolbar.displayName = "FeedToolbar";
32
+ //#endregion
33
+ //#region src/containers/FeedArticle/FeedArticle.tsx
34
+ var FeedArticle = ({ role, subject, attendableId }) => {
35
+ const { invokePromise } = useOperationInvoker();
36
+ const [currentPostId, setCurrentPostId] = useState();
37
+ const [subscription] = useObject(subject);
38
+ const [postFeed] = useObject(subscription?.feed);
39
+ const posts = useQuery(Obj.getDatabase(subscription), postFeed ? Query.select(Filter.type(Subscription.Post)).from(Scope.feed(Obj.getURI(postFeed))) : Query.select(Filter.nothing()));
40
+ const handleAction = useCallback((action) => {
41
+ if (action.type === "current") setCurrentPostId(action.postId);
42
+ }, []);
43
+ const handleSync = useCallback(() => {
44
+ invokePromise(FeedOperation.SyncFeed, { feed: Ref.make(subject) }, {
45
+ spaceId: Obj.getDatabase(subject)?.spaceId,
46
+ notify: { error: ["sync-feed-error.title", { ns: meta.profile.key }] }
47
+ });
48
+ }, [subject, invokePromise]);
49
+ return /* @__PURE__ */ jsxs(Panel.Root, {
50
+ role,
51
+ classNames: "dx-document",
52
+ children: [/* @__PURE__ */ jsx(FeedToolbar, {
53
+ attendableId,
54
+ onSync: handleSync
55
+ }), /* @__PURE__ */ jsx(Panel.Content, {
56
+ asChild: true,
57
+ children: /* @__PURE__ */ jsx(PostStack, {
58
+ id: subscription?.id ?? subject.id,
59
+ posts,
60
+ currentId: currentPostId,
61
+ onAction: handleAction
62
+ })
63
+ })]
64
+ });
65
+ };
66
+ FeedArticle.displayName = "FeedArticle";
67
+ //#endregion
68
+ export { FeedArticle as default };
69
+
70
+ //# sourceMappingURL=chunk-FeedArticle.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-FeedArticle.mjs","names":[],"sources":["../../src/containers/FeedArticle/FeedToolbar.tsx","../../src/containers/FeedArticle/FeedArticle.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport React from 'react';\n\nimport { Panel } from '@dxos/react-ui';\nimport { Menu, MenuBuilder, useMenuBuilder } from '@dxos/react-ui-menu';\n\nimport { meta } from '#meta';\n\nexport type FeedToolbarProps = {\n attendableId?: string;\n onSync: () => void;\n};\n\n/**\n * Reactive feed toolbar built from the menu action-graph idiom: a single Sync action. Matches the\n * magazine/post toolbars; the feed's name is surfaced by the plank chrome, not duplicated here.\n */\nexport const FeedToolbar = ({ attendableId, onSync }: FeedToolbarProps) => {\n const menuActions = useMenuBuilder(\n () =>\n MenuBuilder.make()\n .action(\n 'sync',\n {\n label: ['sync-feed.label', { ns: meta.profile.key }],\n icon: 'ph--arrows-clockwise--regular',\n iconOnly: true,\n },\n onSync,\n )\n .build(),\n [onSync],\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\nFeedToolbar.displayName = 'FeedToolbar';\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport React, { useCallback, useState } from 'react';\n\nimport { useOperationInvoker } from '@dxos/app-framework/ui';\nimport { type AppSurface } from '@dxos/app-toolkit/ui';\nimport { Filter, Obj, Query, Ref, Scope } from '@dxos/echo';\nimport { useObject, useQuery } from '@dxos/echo-react';\nimport { Panel } from '@dxos/react-ui';\n\nimport { PostStack, type PostStackAction } from '#components';\nimport { meta } from '#meta';\nimport { FeedOperation } from '#types';\nimport { Subscription } from '#types';\n\nimport { FeedToolbar } from './FeedToolbar';\n\nexport type FeedArticleProps = AppSurface.ObjectArticleProps<Subscription.Subscription>;\n\nexport const FeedArticle = ({ role, subject, attendableId }: FeedArticleProps) => {\n const { invokePromise } = useOperationInvoker();\n const [currentPostId, setCurrentPostId] = useState<string>();\n const [subscription] = useObject(subject);\n // Subscribe to the backing queue via its Ref — `.target` alone does not re-render when the\n // feed loads after navigation (same pitfall as plugin-inbox MailboxArticle).\n const [postFeed] = useObject(subscription?.feed);\n const db = Obj.getDatabase(subscription);\n const posts = useQuery(\n db,\n postFeed\n ? Query.select(Filter.type(Subscription.Post)).from(Scope.feed(Obj.getURI(postFeed)))\n : Query.select(Filter.nothing()),\n );\n\n const handleAction = useCallback((action: PostStackAction) => {\n if (action.type === 'current') {\n setCurrentPostId(action.postId);\n }\n }, []);\n\n const handleSync = useCallback(() => {\n // Failures surface as a toast via `notify`; invokePromise resolves with `{ error }`, never throws.\n void invokePromise(\n FeedOperation.SyncFeed,\n { feed: Ref.make(subject) },\n {\n spaceId: Obj.getDatabase(subject)?.spaceId,\n notify: { error: ['sync-feed-error.title', { ns: meta.profile.key }] },\n },\n );\n }, [subject, invokePromise]);\n\n return (\n <Panel.Root role={role} classNames='dx-document'>\n <FeedToolbar attendableId={attendableId} onSync={handleSync} />\n <Panel.Content asChild>\n <PostStack\n id={subscription?.id ?? subject.id}\n posts={posts}\n currentId={currentPostId}\n onAction={handleAction}\n />\n </Panel.Content>\n </Panel.Root>\n );\n};\n\nFeedArticle.displayName = 'FeedArticle';\n"],"mappings":";;;;;;;;;;;;;;;AAoBA,IAAa,eAAe,EAAE,cAAc,aAA+B;CACzE,MAAM,cAAc,qBAEhB,YAAY,KAAK,CAAA,CACd,OACC,QACA;EACE,OAAO,CAAC,mBAAmB,EAAE,IAAI,KAAK,QAAQ,IAAI,CAAC;EACnD,MAAM;EACN,UAAU;CACZ,GACA,MACF,CAAA,CACC,MAAM,GACX,CAAC,MAAM,CACT;CAEA,OACE,oBAAC,KAAK,MAAN;EAAW,GAAI;EAA2B;YACxC,oBAAC,MAAM,SAAP;GAAe,SAAA;aACb,oBAAC,KAAK,SAAN,CAAe,CAAA;EACF,CAAA;CACN,CAAA;AAEf;AAEA,YAAY,cAAc;;;ACzB1B,IAAa,eAAe,EAAE,MAAM,SAAS,mBAAqC;CAChF,MAAM,EAAE,kBAAkB,oBAAoB;CAC9C,MAAM,CAAC,eAAe,oBAAoB,SAAiB;CAC3D,MAAM,CAAC,gBAAgB,UAAU,OAAO;CAGxC,MAAM,CAAC,YAAY,UAAU,cAAc,IAAI;CAE/C,MAAM,QAAQ,SADH,IAAI,YAAY,YAEzB,GACA,WACI,MAAM,OAAO,OAAO,KAAK,aAAa,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,KAAK,IAAI,OAAO,QAAQ,CAAC,CAAC,IAClF,MAAM,OAAO,OAAO,QAAQ,CAAC,CACnC;CAEA,MAAM,eAAe,aAAa,WAA4B;EAC5D,IAAI,OAAO,SAAS,WAClB,iBAAiB,OAAO,MAAM;CAElC,GAAG,CAAC,CAAC;CAEL,MAAM,aAAa,kBAAkB;EAEnC,cACE,cAAc,UACd,EAAE,MAAM,IAAI,KAAK,OAAO,EAAE,GAC1B;GACE,SAAS,IAAI,YAAY,OAAO,CAAC,EAAE;GACnC,QAAQ,EAAE,OAAO,CAAC,yBAAyB,EAAE,IAAI,KAAK,QAAQ,IAAI,CAAC,EAAE;EACvE,CACF;CACF,GAAG,CAAC,SAAS,aAAa,CAAC;CAE3B,OACE,qBAAC,MAAM,MAAP;EAAkB;EAAM,YAAW;YAAnC,CACE,oBAAC,aAAD;GAA2B;GAAc,QAAQ;EAAa,CAAA,GAC9D,oBAAC,MAAM,SAAP;GAAe,SAAA;aACb,oBAAC,WAAD;IACE,IAAI,cAAc,MAAM,QAAQ;IACzB;IACP,WAAW;IACX,UAAU;GACX,CAAA;EACY,CAAA,CACL;;AAEhB;AAEA,YAAY,cAAc"}
@@ -0,0 +1,78 @@
1
+ import { Filter, Obj, Query, Ref } from "@dxos/echo";
2
+ import { Operation, Trigger } from "@dxos/compute";
3
+ import { meta } from "#meta";
4
+ import { GraphPath, LayoutOperation } from "@dxos/app-toolkit";
5
+ import { FeedOperation } from "#types";
6
+ import { useCallback, useMemo, useState } from "react";
7
+ import { IconButton, Input, useTranslation } from "@dxos/react-ui";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ import { Form } from "@dxos/react-ui-form";
10
+ import { useOperationInvoker } from "@dxos/app-framework/ui";
11
+ import { useObject, useQuery } from "@dxos/echo-react";
12
+ //#region src/containers/FeedProperties/FeedProperties.tsx
13
+ var FeedProperties = ({ subject }) => {
14
+ const { t } = useTranslation(meta.profile.key);
15
+ const { invokePromise } = useOperationInvoker();
16
+ const db = useMemo(() => Obj.getDatabase(subject), [subject]);
17
+ const [pending, setPending] = useState(false);
18
+ const childTriggers = useQuery(db, Query.select(Filter.and(Filter.type(Trigger.Trigger), Filter.childOf(subject, { transitive: false }))).debugLabel("plugin-magazine.FeedProperties.syncTrigger"));
19
+ const syncTrigger = useMemo(() => childTriggers.find((trigger) => trigger.spec?.kind === "timer"), [childTriggers]);
20
+ const [syncEnabled, setSyncEnabled] = useObject(syncTrigger, "enabled");
21
+ const handleToggleSync = useCallback(async () => {
22
+ if (!db) return;
23
+ if (syncTrigger) {
24
+ setSyncEnabled((enabled) => !enabled);
25
+ return;
26
+ }
27
+ setPending(true);
28
+ try {
29
+ const operation = await ensureSyncFeedOperation(db);
30
+ db.add(Trigger.make({
31
+ [Obj.Parent]: subject,
32
+ enabled: true,
33
+ spec: Trigger.specTimer("*/5 * * * *"),
34
+ runnable: Ref.make(operation),
35
+ input: { feed: db.makeRef(Obj.getURI(subject)) }
36
+ }));
37
+ } finally {
38
+ setPending(false);
39
+ }
40
+ }, [
41
+ syncTrigger,
42
+ db,
43
+ subject,
44
+ setSyncEnabled
45
+ ]);
46
+ const handleViewTrigger = useCallback(() => {
47
+ if (!db) return;
48
+ invokePromise(LayoutOperation.Open, {
49
+ subject: [GraphPath.getSpacePath(db.spaceId, "settings", "org.dxos.plugin.routine.routines")],
50
+ workspace: GraphPath.getSpacePath(db.spaceId)
51
+ });
52
+ }, [invokePromise, db]);
53
+ return /* @__PURE__ */ jsx(Form.Section, { children: /* @__PURE__ */ jsxs(Input.Root, { children: [/* @__PURE__ */ jsx(Input.Label, { children: t("feed-sync.label") }), /* @__PURE__ */ jsxs("div", {
54
+ className: "flex flex-row items-center",
55
+ children: [/* @__PURE__ */ jsx(Input.Switch, {
56
+ checked: syncEnabled ?? false,
57
+ disabled: pending,
58
+ onCheckedChange: () => {
59
+ handleToggleSync();
60
+ }
61
+ }), syncTrigger && /* @__PURE__ */ jsx(IconButton, {
62
+ iconOnly: true,
63
+ icon: "ph--gear--regular",
64
+ label: t("view-trigger.label"),
65
+ onClick: handleViewTrigger
66
+ })]
67
+ })] }) });
68
+ };
69
+ var ensureSyncFeedOperation = async (db) => {
70
+ const [existingOperation] = await db.query(Filter.and(Filter.type(Operation.PersistentOperation), Filter.key(FeedOperation.SyncFeed.meta.key))).run();
71
+ if (existingOperation) return existingOperation;
72
+ return db.add(Operation.serialize(FeedOperation.SyncFeed));
73
+ };
74
+ FeedProperties.displayName = "FeedProperties";
75
+ //#endregion
76
+ export { FeedProperties as default };
77
+
78
+ //# sourceMappingURL=chunk-FeedProperties.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-FeedProperties.mjs","names":[],"sources":["../../src/containers/FeedProperties/FeedProperties.tsx"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport React, { useCallback, useMemo, useState } from 'react';\n\nimport { useOperationInvoker } from '@dxos/app-framework/ui';\nimport { GraphPath, LayoutOperation } from '@dxos/app-toolkit';\nimport { type AppSurface } from '@dxos/app-toolkit/ui';\nimport { Operation, Trigger } from '@dxos/compute';\nimport { Filter, Obj, Query, Ref } from '@dxos/echo';\nimport { useObject, useQuery } from '@dxos/echo-react';\nimport { IconButton, Input, useTranslation } from '@dxos/react-ui';\nimport { Form } from '@dxos/react-ui-form';\n\nimport { meta } from '#meta';\nimport { FeedOperation, Subscription } from '#types';\n\nexport type FeedPropertiesProps = AppSurface.ObjectPropertiesProps<Subscription.Subscription>;\n\nexport const FeedProperties = ({ subject }: FeedPropertiesProps) => {\n const { t } = useTranslation(meta.profile.key);\n const { invokePromise } = useOperationInvoker();\n const db = useMemo(() => Obj.getDatabase(subject), [subject]);\n const [pending, setPending] = useState(false);\n\n const childTriggers = useQuery(\n db,\n Query.select(Filter.and(Filter.type(Trigger.Trigger), Filter.childOf(subject, { transitive: false }))).debugLabel(\n 'plugin-magazine.FeedProperties.syncTrigger',\n ),\n );\n\n const syncTrigger = useMemo(() => childTriggers.find((trigger) => trigger.spec?.kind === 'timer'), [childTriggers]);\n\n const [syncEnabled, setSyncEnabled] = useObject(syncTrigger, 'enabled');\n\n const handleToggleSync = useCallback(async () => {\n if (!db) {\n return;\n }\n\n if (syncTrigger) {\n setSyncEnabled((enabled) => !enabled);\n return;\n }\n\n setPending(true);\n try {\n const operation = await ensureSyncFeedOperation(db);\n db.add(\n Trigger.make({\n [Obj.Parent]: subject,\n enabled: true,\n spec: Trigger.specTimer('*/5 * * * *'),\n runnable: Ref.make(operation),\n input: { feed: db.makeRef(Obj.getURI(subject)) },\n }),\n );\n } finally {\n setPending(false);\n }\n }, [syncTrigger, db, subject, setSyncEnabled]);\n\n const handleViewTrigger = useCallback(() => {\n if (!db) {\n return;\n }\n\n void invokePromise(LayoutOperation.Open, {\n subject: [GraphPath.getSpacePath(db.spaceId, 'settings', 'org.dxos.plugin.routine.routines')],\n workspace: GraphPath.getSpacePath(db.spaceId),\n });\n }, [invokePromise, db]);\n\n return (\n <Form.Section>\n <Input.Root>\n <Input.Label>{t('feed-sync.label')}</Input.Label>\n <div className='flex flex-row items-center'>\n <Input.Switch\n checked={syncEnabled ?? false}\n disabled={pending}\n onCheckedChange={() => {\n void handleToggleSync();\n }}\n />\n {syncTrigger && (\n <IconButton iconOnly icon='ph--gear--regular' label={t('view-trigger.label')} onClick={handleViewTrigger} />\n )}\n </div>\n </Input.Root>\n </Form.Section>\n );\n};\n\nconst ensureSyncFeedOperation = async (db: NonNullable<ReturnType<typeof Obj.getDatabase>>) => {\n const existing = await db\n .query(Filter.and(Filter.type(Operation.PersistentOperation), Filter.key(FeedOperation.SyncFeed.meta.key)))\n .run();\n const [existingOperation] = existing;\n if (existingOperation) {\n return existingOperation;\n }\n\n return db.add(Operation.serialize(FeedOperation.SyncFeed));\n};\n\nFeedProperties.displayName = 'FeedProperties';\n"],"mappings":";;;;;;;;;;;;AAoBA,IAAa,kBAAkB,EAAE,cAAmC;CAClE,MAAM,EAAE,MAAM,eAAe,KAAK,QAAQ,GAAG;CAC7C,MAAM,EAAE,kBAAkB,oBAAoB;CAC9C,MAAM,KAAK,cAAc,IAAI,YAAY,OAAO,GAAG,CAAC,OAAO,CAAC;CAC5D,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAE5C,MAAM,gBAAgB,SACpB,IACA,MAAM,OAAO,OAAO,IAAI,OAAO,KAAK,QAAQ,OAAO,GAAG,OAAO,QAAQ,SAAS,EAAE,YAAY,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,WACrG,4CACF,CACF;CAEA,MAAM,cAAc,cAAc,cAAc,MAAM,YAAY,QAAQ,MAAM,SAAS,OAAO,GAAG,CAAC,aAAa,CAAC;CAElH,MAAM,CAAC,aAAa,kBAAkB,UAAU,aAAa,SAAS;CAEtE,MAAM,mBAAmB,YAAY,YAAY;EAC/C,IAAI,CAAC,IACH;EAGF,IAAI,aAAa;GACf,gBAAgB,YAAY,CAAC,OAAO;GACpC;EACF;EAEA,WAAW,IAAI;EACf,IAAI;GACF,MAAM,YAAY,MAAM,wBAAwB,EAAE;GAClD,GAAG,IACD,QAAQ,KAAK;KACV,IAAI,SAAS;IACd,SAAS;IACT,MAAM,QAAQ,UAAU,aAAa;IACrC,UAAU,IAAI,KAAK,SAAS;IAC5B,OAAO,EAAE,MAAM,GAAG,QAAQ,IAAI,OAAO,OAAO,CAAC,EAAE;GACjD,CAAC,CACH;EACF,UAAU;GACR,WAAW,KAAK;EAClB;CACF,GAAG;EAAC;EAAa;EAAI;EAAS;CAAc,CAAC;CAE7C,MAAM,oBAAoB,kBAAkB;EAC1C,IAAI,CAAC,IACH;EAGF,cAAmB,gBAAgB,MAAM;GACvC,SAAS,CAAC,UAAU,aAAa,GAAG,SAAS,YAAY,kCAAkC,CAAC;GAC5F,WAAW,UAAU,aAAa,GAAG,OAAO;EAC9C,CAAC;CACH,GAAG,CAAC,eAAe,EAAE,CAAC;CAEtB,OACE,oBAAC,KAAK,SAAN,EAAA,UACE,qBAAC,MAAM,MAAP,EAAA,UAAA,CACE,oBAAC,MAAM,OAAP,EAAA,UAAc,EAAE,iBAAiB,EAAe,CAAA,GAChD,qBAAC,OAAD;EAAK,WAAU;YAAf,CACE,oBAAC,MAAM,QAAP;GACE,SAAS,eAAe;GACxB,UAAU;GACV,uBAAuB;IACrB,iBAAsB;GACxB;EACD,CAAA,GACA,eACC,oBAAC,YAAD;GAAY,UAAA;GAAS,MAAK;GAAoB,OAAO,EAAE,oBAAoB;GAAG,SAAS;EAAoB,CAAA,CAE1G;GACK,EAAA,CAAA,EACA,CAAA;AAElB;AAEA,IAAM,0BAA0B,OAAO,OAAwD;CAI7F,MAAM,CAAC,qBAAqB,MAHL,GACpB,MAAM,OAAO,IAAI,OAAO,KAAK,UAAU,mBAAmB,GAAG,OAAO,IAAI,cAAc,SAAS,KAAK,GAAG,CAAC,CAAC,CAAA,CACzG,IAAI;CAEP,IAAI,mBACF,OAAO;CAGT,OAAO,GAAG,IAAI,UAAU,UAAU,cAAc,QAAQ,CAAC;AAC3D;AAEA,eAAe,cAAc"}