@blogmcp/community 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -2
  3. package/dist/blogmcp-community.global.js +10 -0
  4. package/dist/chunks/mount-DzjwnHyx.js +4597 -0
  5. package/dist/core/attrs.d.ts +73 -0
  6. package/dist/core/avatars.d.ts +16 -0
  7. package/dist/core/client.d.ts +27 -0
  8. package/dist/core/discussion.d.ts +144 -0
  9. package/dist/core/download.d.ts +4 -0
  10. package/dist/core/format.d.ts +17 -0
  11. package/dist/core/i18n.d.ts +28 -0
  12. package/dist/core/names.d.ts +15 -0
  13. package/dist/core/notice.d.ts +50 -0
  14. package/dist/core/onboarding.d.ts +52 -0
  15. package/dist/core/outcomes.d.ts +28 -0
  16. package/dist/core/panel.d.ts +80 -0
  17. package/dist/core/receipt.d.ts +5 -0
  18. package/dist/core/text.d.ts +22 -0
  19. package/dist/core/turnstile.d.ts +33 -0
  20. package/dist/element.d.ts +26 -0
  21. package/dist/global.d.ts +8 -0
  22. package/dist/index.d.ts +25 -0
  23. package/dist/index.js +22 -0
  24. package/dist/mount.d.ts +65 -0
  25. package/dist/react.d.ts +17 -0
  26. package/dist/react.js +35 -0
  27. package/dist/ui/AuthorAvatar.vue.d.ts +11 -0
  28. package/dist/ui/ConfirmDialog.vue.d.ts +20 -0
  29. package/dist/ui/DiscussionView.vue.d.ts +33 -0
  30. package/dist/ui/LinkedText.vue.d.ts +5 -0
  31. package/dist/ui/MessageComposer.vue.d.ts +24 -0
  32. package/dist/ui/MessageItem.vue.d.ts +6 -0
  33. package/dist/ui/MessageMenu.vue.d.ts +10 -0
  34. package/dist/ui/MyCommunityView.vue.d.ts +16 -0
  35. package/dist/ui/NoticeFormView.vue.d.ts +26 -0
  36. package/dist/ui/NoticeStatusView.vue.d.ts +15 -0
  37. package/dist/ui/OnboardingSteps.vue.d.ts +11 -0
  38. package/dist/ui/ReactionBar.vue.d.ts +13 -0
  39. package/dist/ui/ReportDialog.vue.d.ts +15 -0
  40. package/dist/ui/StatementView.vue.d.ts +15 -0
  41. package/dist/ui/context.d.ts +21 -0
  42. package/dist/ui/discussion-context.d.ts +19 -0
  43. package/dist/ui/types.d.ts +7 -0
  44. package/dist/version.d.ts +1 -0
  45. package/dist/vue.d.ts +89 -0
  46. package/dist/vue.js +97 -0
  47. package/package.json +80 -4
  48. package/src/core/attrs.ts +147 -0
  49. package/src/core/avatars.ts +44 -0
  50. package/src/core/client.ts +54 -0
  51. package/src/core/discussion.ts +691 -0
  52. package/src/core/download.ts +21 -0
  53. package/src/core/format.ts +90 -0
  54. package/src/core/i18n.ts +49 -0
  55. package/src/core/names.ts +51 -0
  56. package/src/core/notice.ts +178 -0
  57. package/src/core/onboarding.ts +345 -0
  58. package/src/core/outcomes.ts +74 -0
  59. package/src/core/panel.ts +411 -0
  60. package/src/core/receipt.ts +28 -0
  61. package/src/core/text.ts +142 -0
  62. package/src/core/turnstile.ts +98 -0
  63. package/src/element.ts +109 -0
  64. package/src/global.ts +12 -0
  65. package/src/index.ts +35 -0
  66. package/src/mount.ts +127 -0
  67. package/src/react.ts +53 -0
  68. package/src/shims.d.ts +11 -0
  69. package/src/ui/AuthorAvatar.vue +26 -0
  70. package/src/ui/ConfirmDialog.vue +38 -0
  71. package/src/ui/DiscussionView.vue +294 -0
  72. package/src/ui/LinkedText.vue +18 -0
  73. package/src/ui/MessageComposer.vue +130 -0
  74. package/src/ui/MessageItem.vue +205 -0
  75. package/src/ui/MessageMenu.vue +67 -0
  76. package/src/ui/MyCommunityView.vue +369 -0
  77. package/src/ui/NoticeFormView.vue +244 -0
  78. package/src/ui/NoticeStatusView.vue +82 -0
  79. package/src/ui/OnboardingSteps.vue +163 -0
  80. package/src/ui/ReactionBar.vue +41 -0
  81. package/src/ui/ReportDialog.vue +88 -0
  82. package/src/ui/StatementView.vue +141 -0
  83. package/src/ui/context.ts +75 -0
  84. package/src/ui/discussion-context.ts +30 -0
  85. package/src/ui/styles.css +243 -0
  86. package/src/ui/types.ts +7 -0
  87. package/src/version.ts +3 -0
  88. package/src/vue.ts +116 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lukas Zubal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,147 @@
1
- # Temporary Holding Version
1
+ # @blogmcp/community
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The community elements for your own site, over `@blogmcp/sdk/community`:
4
+
5
+ - `<blogmcp-discussion>`: the discussion under an article. A two-level thread, the composer, reactions,
6
+ reports, blocks, the inline onboarding, and the states of a member's own messages ("awaiting review",
7
+ removed, deleted);
8
+ - `<blogmcp-notice-form>`: the notice form for anyone, signed in or not (DSA art. 16), with Cloudflare
9
+ Turnstile;
10
+ - `<blogmcp-my-community>`: the reader's „Moje komunita" panel. It holds statements of reasons and appeals,
11
+ reports, notifications, the consent, the export and the profile's deletion. It also renders a statement
12
+ page (`decision-id`) and a notifier's status page (`#t=<receipt>` in the address).
13
+
14
+ Each element lives in a shadow root and renders text only. A member's links pass through `sanitizeUrl` and
15
+ carry `rel="nofollow ugc noopener noreferrer"`. Every text comes from the SDK's catalogue
16
+ (`communityMessage`), in Czech or English.
17
+
18
+ ## Your site's side: one relative base
19
+
20
+ The elements never call kb-api. They POST to **your site's** relative base: `api-base`, by default
21
+ `/api/community` under your `site-base` (the root when you set none):
22
+
23
+ - a headless site or the storefront, served at the root: the BFF from `@blogmcp/sdk/community/server`,
24
+ mounted at `/api/community`;
25
+ - a managed blog, served under `/blog`: its own `/api` proxy, so set `site-base="/blog"`. The element then
26
+ posts to `/blog/api/community`, and its default pages are `/blog/community/…`.
27
+
28
+ > **On a managed blog, never `api-base="/api/community"`.** On the tenant's host, `/api/` is kb-api itself, not
29
+ > the blog. A request there skips the blog's proxy, so it carries no reader session. Every reader reads as
30
+ > signed out, every write is refused, and the notice form reaches no route. The defaults have the same trap:
31
+ > without `site-base`, the links go to `/community/…`, outside the blog.
32
+
33
+ ```ts
34
+ // e.g. app/routes/api/community.$.ts (POST only)
35
+ import { createCommunityBff, communityServerEnv, fromWebRequest } from '@blogmcp/sdk/community/server'
36
+
37
+ const bff = createCommunityBff({
38
+ // kb-api as your server reaches it, WITH the `/api` prefix your instance serves it under:
39
+ // KB_API_URL = https://<your instance>/api (or `${process.env.BLOGMCP_API_URL}/api`).
40
+ kbApiUrl: process.env.KB_API_URL!,
41
+ siteUrls: ['https://www.example.com'],
42
+ memberToken: async (http) => /* your session → the member's JWT, or null */ null,
43
+ ...communityServerEnv(process.env), // COMMUNITY_NOTICE_KEY
44
+ })
45
+
46
+ export async function action({ request }: { request: Request }) {
47
+ const { http, respond } = fromWebRequest(request)
48
+ return respond(await bff.handle(http))
49
+ }
50
+ ```
51
+
52
+ An absolute `api-base` (or `site-base`) is refused, and the element renders nothing: kb-api's community
53
+ routes answer no CORS.
54
+
55
+ ## Mount the elements
56
+
57
+ **Vue / Nuxt**
58
+
59
+ ```vue
60
+ <script setup lang="ts">
61
+ import { KbDiscussion } from '@blogmcp/community/vue'
62
+ </script>
63
+ <template>
64
+ <article>…</article>
65
+ <KbDiscussion :article-id="article.id" api-base="/api/community" sign-in-href="/prihlaseni" />
66
+ </template>
67
+ ```
68
+
69
+ **React / Next.js**
70
+
71
+ ```tsx
72
+ import { KbDiscussion } from '@blogmcp/community/react' // a client component
73
+
74
+ <KbDiscussion articleId={article.id} apiBase="/api/community" signInHref="/sign-in" />
75
+ ```
76
+
77
+ `KbNoticeForm` and `KbMyCommunity` work the same way. Each wrapper mounts its element after hydration and
78
+ removes it on unmount. It renders a single empty `<div data-blogmcp-community="…">` as the mount point: the
79
+ discussion is part of the page's flow, so the element goes where the wrapper is.
80
+
81
+ `mountDiscussion(o)`, `mountNoticeForm(o)` and `mountMyCommunity(o)` do the same from any client-side hook.
82
+ Each returns the unmount function.
83
+
84
+ **Plain HTML**
85
+
86
+ ```html
87
+ <blogmcp-discussion article-id="…" api-base="/api/community" sign-in-href="/prihlaseni"></blogmcp-discussion>
88
+ <script src="https://cdn.jsdelivr.net/npm/@blogmcp/community@0.1.0/dist/blogmcp-community.global.js" nonce="…"></script>
89
+ ```
90
+
91
+ The script build (`@blogmcp/community/global`) registers all three elements. It exposes
92
+ `window.BlogmcpCommunity` and gives the elements its own `nonce`.
93
+
94
+ ## Attributes
95
+
96
+ | Element | Attribute | |
97
+ |---|---|---|
98
+ | all | `api-base` | your relative base (default `/api/community` under `site-base`) |
99
+ | all | `site-base` | the path your site is served under (`/blog` on a managed blog); the defaults go under it |
100
+ | all | `lang` | `cs` or `en` (`sk` reads Czech); default `<html lang>` |
101
+ | all | `routes` | JSON with your pages (below) |
102
+ | all | `nonce` | CSP nonce for the element's stylesheet and the Turnstile script |
103
+ | `<blogmcp-discussion>` | `article-id` | the article |
104
+ | | `sign-in-href` | your sign-in page; the current path goes in its fragment |
105
+ | | `poll-ms` | how often to check for new comments while the discussion is in view (default 20000; 0 = off) |
106
+ | `<blogmcp-notice-form>` | `target` | JSON: `{ conversationId, seq }`, `{ memberId }`, `{ handle }` or `{ articleId }` |
107
+ | | `article-id` | or the article; without either, the discussion's report link (`#c=…&s=…`). With no target at all, the element shows where a report starts and links to `rulesPath` instead of the form |
108
+ | | `turnstile-site-key` | your Turnstile site key; without it no widget renders |
109
+ | `<blogmcp-my-community>` | `decision-id` | open this statement of reasons |
110
+ | | `sign-in-href` | your sign-in page |
111
+
112
+ `routes` names your community pages: `statementPath` (with `{id}`), `myCommunityPath`, `rulesPath`,
113
+ `reportPath` and `noticesPath`. Each is a full path on your site, used as it is. A page you leave out
114
+ falls back to the managed blog's pages: `${site-base}/community/…`. The tenant's own statement and „Moje
115
+ komunita" paths, set in its community settings, come before that fallback.
116
+ `consultationPath` (no default) turns the regulated preset's adverse-event note into a link to your
117
+ consultation page.
118
+ The receipt in a status link always goes in the fragment (`#t=…`). The page clears it from the address bar
119
+ as soon as it reads it.
120
+
121
+ A `transport` property replaces the HTTP layer, for tests.
122
+
123
+ Events (bubbling, composed):
124
+ - `bcm-posted`, `{ state: 'visible' | 'held' }`: the reader's comment went out;
125
+ - `bcm-notice-submitted`, `{ contactKept }`: a notice was received.
126
+
127
+ ## Look
128
+
129
+ Theme the elements with the custom properties on them: `blogmcp-discussion { --bcm-accent: #2b7a4b }`.
130
+ The properties are `--bcm-bg`, `-fg`, `-muted`, `-line`, `-soft`, `-accent`, `-accent-fg`, `-danger`,
131
+ `-ok`, `-warn-bg`, `-shadow` and `-z`, plus `--bcm-font` and `--bcm-font-size`. Dark mode follows
132
+ `prefers-color-scheme`.
133
+
134
+ ## Content Security Policy
135
+
136
+ - The styles are one `<style>` in each shadow root, carrying the nonce you pass.
137
+ - `connect-src 'self'` is enough: the elements only talk to your own base.
138
+ - The notice form with Turnstile needs `script-src` and `frame-src` for `https://challenges.cloudflare.com`.
139
+ Only a page with the notice form loads that script.
140
+
141
+ ## Development
142
+
143
+ ```bash
144
+ npm run build # ESM + the <script> build (+ size budget, 200 KiB gzip)
145
+ npm test # vitest (happy-dom): controllers, store, onboarding, outcomes, elements, text-only and closed-path scans
146
+ npm run test:e2e # Playwright: desktop + iPhone profile + a plain bundler, mocked community API (ports 8286/8287)
147
+ ```