@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,16 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
|
|
7
|
+
import { Capability } from '@dxos/app-framework';
|
|
8
|
+
import { RoutineCapabilities } from '@dxos/plugin-routine';
|
|
9
|
+
|
|
10
|
+
import { magazineCuration } from '../templates/magazine-curation';
|
|
11
|
+
|
|
12
|
+
export default Capability.makeModule(
|
|
13
|
+
Effect.fnUntraced(function* () {
|
|
14
|
+
return [Capability.contributes(RoutineCapabilities.Template, magazineCuration)];
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
|
|
7
|
+
import { Capability } from '@dxos/app-framework';
|
|
8
|
+
import { AppCapabilities } from '@dxos/app-toolkit';
|
|
9
|
+
|
|
10
|
+
import { MagazineSkill } from '#skills';
|
|
11
|
+
|
|
12
|
+
const skillDefinition = () => Effect.succeed([Capability.contributes(AppCapabilities.SkillDefinition, MagazineSkill)]);
|
|
13
|
+
|
|
14
|
+
export default skillDefinition;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { dedupeImagesInMarkdown } from './PostContent';
|
|
8
|
+
|
|
9
|
+
describe('dedupeImagesInMarkdown', () => {
|
|
10
|
+
test('strips an image whose URL matches the hero imageUrl', () => {
|
|
11
|
+
const heroUrl = 'https://example.com/hero.jpg';
|
|
12
|
+
const md = `Some intro paragraph.
|
|
13
|
+
|
|
14
|
+

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