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