@blogmcp/community 0.1.0 → 0.2.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 (68) hide show
  1. package/README.md +108 -11
  2. package/dist/blogmcp-community.global.js +8 -8
  3. package/dist/chunks/{mount-DzjwnHyx.js → mount-WadUnaTa.js} +3909 -2070
  4. package/dist/core/attrs.d.ts +27 -1
  5. package/dist/core/badge.d.ts +30 -0
  6. package/dist/core/centre.d.ts +28 -0
  7. package/dist/core/digest-token.d.ts +2 -0
  8. package/dist/core/discussion.d.ts +36 -5
  9. package/dist/core/format.d.ts +7 -0
  10. package/dist/core/onboarding.d.ts +42 -2
  11. package/dist/core/outcomes.d.ts +2 -1
  12. package/dist/core/panel.d.ts +17 -1
  13. package/dist/core/profile.d.ts +40 -0
  14. package/dist/core/push.d.ts +63 -0
  15. package/dist/core/realtime.d.ts +35 -0
  16. package/dist/core/settings.d.ts +53 -0
  17. package/dist/core/text.d.ts +13 -0
  18. package/dist/element.d.ts +22 -1
  19. package/dist/global.d.ts +7 -4
  20. package/dist/index.d.ts +10 -2
  21. package/dist/index.js +34 -11
  22. package/dist/mount.d.ts +40 -0
  23. package/dist/react.d.ts +14 -3
  24. package/dist/react.js +24 -4
  25. package/dist/ui/BadgeView.vue.d.ts +13 -0
  26. package/dist/ui/DiscussionView.vue.d.ts +11 -0
  27. package/dist/ui/LinkedText.vue.d.ts +1 -0
  28. package/dist/ui/MemberSettingsView.vue.d.ts +13 -0
  29. package/dist/ui/MessageComposer.vue.d.ts +2 -0
  30. package/dist/ui/NotificationCentreView.vue.d.ts +12 -0
  31. package/dist/ui/OnboardingSteps.vue.d.ts +1 -1
  32. package/dist/ui/ProfileView.vue.d.ts +13 -0
  33. package/dist/ui/WelcomeView.vue.d.ts +12 -0
  34. package/dist/vue.d.ts +114 -3
  35. package/dist/vue.js +109 -5
  36. package/package.json +3 -3
  37. package/src/core/attrs.ts +41 -2
  38. package/src/core/badge.ts +120 -0
  39. package/src/core/centre.ts +136 -0
  40. package/src/core/digest-token.ts +26 -0
  41. package/src/core/discussion.ts +203 -18
  42. package/src/core/format.ts +22 -0
  43. package/src/core/onboarding.ts +165 -18
  44. package/src/core/outcomes.ts +13 -1
  45. package/src/core/panel.ts +39 -4
  46. package/src/core/profile.ts +144 -0
  47. package/src/core/push.ts +174 -0
  48. package/src/core/realtime.ts +39 -0
  49. package/src/core/settings.ts +363 -0
  50. package/src/core/text.ts +32 -0
  51. package/src/element.ts +62 -6
  52. package/src/global.ts +18 -4
  53. package/src/index.ts +36 -1
  54. package/src/mount.ts +108 -1
  55. package/src/react.ts +47 -3
  56. package/src/ui/BadgeView.vue +51 -0
  57. package/src/ui/DiscussionView.vue +43 -3
  58. package/src/ui/LinkedText.vue +11 -5
  59. package/src/ui/MemberSettingsView.vue +175 -0
  60. package/src/ui/MessageComposer.vue +15 -4
  61. package/src/ui/MessageItem.vue +24 -6
  62. package/src/ui/MyCommunityView.vue +9 -0
  63. package/src/ui/NotificationCentreView.vue +159 -0
  64. package/src/ui/OnboardingSteps.vue +17 -6
  65. package/src/ui/ProfileView.vue +91 -0
  66. package/src/ui/WelcomeView.vue +72 -0
  67. package/src/ui/styles.css +16 -0
  68. package/src/vue.ts +129 -3
package/README.md CHANGED
@@ -4,13 +4,36 @@ The community elements for your own site, over `@blogmcp/sdk/community`:
4
4
 
5
5
  - `<blogmcp-discussion>`: the discussion under an article. A two-level thread, the composer, reactions,
6
6
  reports, blocks, the inline onboarding, and the states of a member's own messages ("awaiting review",
7
- removed, deleted);
7
+ removed, deleted). Where the tenant asks for the art. 9 consent, the onboarding asks it the way the tenant set
8
+ it in kb-api: one consent, or one per controller the consent text names, and a refusal that still lets the
9
+ member write in public where the tenant allows it. Your site passes nothing for this;
8
10
  - `<blogmcp-notice-form>`: the notice form for anyone, signed in or not (DSA art. 16), with Cloudflare
9
11
  Turnstile;
10
12
  - `<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
13
+ reports, notifications, the consent (to withdraw it, or to give one the member refused), the export and the
14
+ profile's deletion. It also renders a statement
12
15
  page (`decision-id`) and a notifier's status page (`#t=<receipt>` in the address).
13
16
 
17
+ With the community module (M2) there are five more:
18
+
19
+ - `<blogmcp-notification-centre>`: the member's notifications, newest first. Older ones load a page at a time,
20
+ „Jen nepřečtené" filters them, each one or all can be marked read, and each row links its subject where your
21
+ site has a page for it (a decision's statement, the settings, „Moje komunita"). It links your settings page;
22
+ - `<blogmcp-badge>`: the unread count for your header, a link to the centre. It asks once who is viewing, then
23
+ the count every 60 s while the tab is visible and again when it is shown again. It renders nothing for a
24
+ visitor, a reader who never joined, a module that is off, or nothing unread;
25
+ - `<blogmcp-member-settings>`: the six notification switches (replies, mentions, topics, browser notifications,
26
+ the daily e-mail digest, the quiet night 22:00–7:00), each saved on its own; this browser's notifications;
27
+ and the digest e-mail's unsubscribe link (below);
28
+ - `<blogmcp-profile handle="…">`: a member's public profile: name, avatar, since when they are a member, the
29
+ bio as text and the trust level as a label („Nový člen", „Člen", „Stálý člen", „Moderátor"), never a number;
30
+ - `<blogmcp-welcome>`: the page after joining: the rules in summary and three links (the discussions, the
31
+ settings, the member's own profile; a visitor gets your sign-in link instead).
32
+
33
+ `<blogmcp-discussion>` also links every `@handle` in a comment to that member's profile page (for a signed-in
34
+ reader; an anonymous one reads the mention as text), and takes live updates through its `realtime` property
35
+ (below).
36
+
14
37
  Each element lives in a shadow root and renders text only. A member's links pass through `sanitizeUrl` and
15
38
  carry `rel="nofollow ugc noopener noreferrer"`. Every text comes from the SDK's catalogue
16
39
  (`communityMessage`), in Czech or English.
@@ -74,21 +97,23 @@ import { KbDiscussion } from '@blogmcp/community/react' // a client component
74
97
  <KbDiscussion articleId={article.id} apiBase="/api/community" signInHref="/sign-in" />
75
98
  ```
76
99
 
77
- `KbNoticeForm` and `KbMyCommunity` work the same way. Each wrapper mounts its element after hydration and
100
+ `KbNoticeForm`, `KbMyCommunity`, `KbNotificationCentre`, `KbBadge`, `KbMemberSettings`, `KbProfile` and
101
+ `KbWelcome` work the same way. Each wrapper mounts its element after hydration and
78
102
  removes it on unmount. It renders a single empty `<div data-blogmcp-community="…">` as the mount point: the
79
103
  discussion is part of the page's flow, so the element goes where the wrapper is.
80
104
 
81
- `mountDiscussion(o)`, `mountNoticeForm(o)` and `mountMyCommunity(o)` do the same from any client-side hook.
82
- Each returns the unmount function.
105
+ `mountDiscussion(o)`, `mountNoticeForm(o)`, `mountMyCommunity(o)`, `mountNotificationCentre(o)`, `mountBadge(o)`,
106
+ `mountMemberSettings(o)`, `mountProfile(o)` and `mountWelcome(o)` do the same from any client-side hook. Each
107
+ returns the unmount function.
83
108
 
84
109
  **Plain HTML**
85
110
 
86
111
  ```html
87
112
  <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>
113
+ <script src="https://cdn.jsdelivr.net/npm/@blogmcp/community@0.2.0/dist/blogmcp-community.global.js" nonce="…"></script>
89
114
  ```
90
115
 
91
- The script build (`@blogmcp/community/global`) registers all three elements. It exposes
116
+ The script build (`@blogmcp/community/global`) registers all eight elements. It exposes
92
117
  `window.BlogmcpCommunity` and gives the elements its own `nonce`.
93
118
 
94
119
  ## Attributes
@@ -108,10 +133,22 @@ The script build (`@blogmcp/community/global`) registers all three elements. It
108
133
  | | `turnstile-site-key` | your Turnstile site key; without it no widget renders |
109
134
  | `<blogmcp-my-community>` | `decision-id` | open this statement of reasons |
110
135
  | | `sign-in-href` | your sign-in page |
136
+ | `<blogmcp-notification-centre>` | `sign-in-href` | your sign-in page (a visitor's link) |
137
+ | `<blogmcp-badge>` | `poll-ms` | how often to read the count while the tab is visible (default 60000, and never less — a smaller value reads as 60000; 0 = once, and when the tab is shown again) |
138
+ | `<blogmcp-member-settings>` | `sw-path` | your service worker route (default `${site-base}/sw.js`) |
139
+ | | `sign-in-href` | your sign-in page |
140
+ | `<blogmcp-profile>` | `handle` | the member's handle (an `@` in front is fine) |
141
+ | | `sign-in-href` | your sign-in page (profiles are for signed-in readers) |
142
+ | `<blogmcp-welcome>` | `sign-in-href` | your sign-in page (in the profile link's place, for a visitor) |
111
143
 
112
144
  `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
145
+ `reportPath`, `noticesPath` and, with the module, `profilePath` (with `{handle}`), `settingsPath`,
146
+ `notificationsPath`, `welcomePath` and `discussionsPath`. Each is a full path on your site, used as it is. A page you leave out
147
+ falls back to the managed blog's pages: `${site-base}/community/…`. `articlePath` (with `{slug}`; no default)
148
+ is your article page — the managed blogs' `/blog/articles/{slug}`, a shop's `/blog/{slug}`: the notification
149
+ centre links a reply or a mention to its message there (`…/<slug>#bcm-msg-<seq>`), and without it those rows stay
150
+ text. The discussion gives every message the anchor `bcm-msg-<seq>` and, opened at one, scrolls to it itself once
151
+ its first page is in (the rows live in a shadow root, where the browser's own jump cannot reach). The tenant's own statement and „Moje
115
152
  komunita" paths, set in its community settings, come before that fallback.
116
153
  `consultationPath` (no default) turns the regulated preset's adverse-event note into a link to your
117
154
  consultation page.
@@ -120,6 +157,62 @@ as soon as it reads it.
120
157
 
121
158
  A `transport` property replaces the HTTP layer, for tests.
122
159
 
160
+ ## Live updates (`realtime`)
161
+
162
+ The discussion keeps itself fresh by polling. For live updates, import the SDK's realtime entry yourself and pass
163
+ its `createRealtimeSession` as the discussion's `realtime` property (a property, never an attribute). Your bundle
164
+ then needs `centrifuge` (≥ 5.7), the entry's peer dependency; the elements never import either, so a site
165
+ without it ships no socket code.
166
+
167
+ ```ts
168
+ import { createRealtimeSession } from '@blogmcp/sdk/community/realtime'
169
+ import { KbDiscussion } from '@blogmcp/community/react'
170
+
171
+ <KbDiscussion articleId={article.id} apiBase="/api/community" realtime={createRealtimeSession} />
172
+ // plain: document.querySelector('blogmcp-discussion').realtime = createRealtimeSession
173
+ ```
174
+
175
+ The element starts the session over its own discussion after the first page is read, and stops it when it
176
+ unmounts. It starts none for an anonymous reader, nor while the community's live updates are off (the module is
177
+ `read_only` or off, or the instance runs no realtime): the discussion polls then, with no socket. A session that
178
+ falls back to polling — kb-api answers `REALTIME_OFF`, or the server ends the connection for good — makes the
179
+ discussion say once that live updates are off; it is never reopened. When the session was revoked (4501), the
180
+ session asks kb-api for a fresh token once first: a member whose own password change ended the old session keeps
181
+ the page live under the new one; a refusal, or a second 4501, falls back for good. While the socket cannot get
182
+ through (the subscription is not live 15 s after connecting, or after the connection dropped), the discussion polls
183
+ meanwhile and says so once; Centrifuge keeps reconnecting, and the sentence goes when it is live again. An article
184
+ without comments yet has no channel to subscribe to: it polls for the first one and is never called off for that.
185
+
186
+ The composer knows the member's mention limit (`me/get`'s `caps.mentionsPerMessage`, by trust level): a comment
187
+ that mentions more distinct `@handle`s says so as it is typed and is not sent (kb-api refuses it too).
188
+
189
+ The URL to connect to comes from kb-api's token answer (the instance's own host, `/rt/…`), never from your
190
+ configuration.
191
+
192
+ ## Browser notifications and the digest's unsubscribe link
193
+
194
+ `<blogmcp-member-settings>` shows this browser's notifications only while the community offers them. „Zapnout
195
+ upozornění" (the member's click; nothing is registered on page load) registers **your** service worker at
196
+ `sw-path`, asks the member's permission, subscribes the browser and hands the subscription to kb-api; „Vypnout"
197
+ ends it. Serve the worker from `@blogmcp/sdk/community/sw` (`communityServiceWorkerSource`, as
198
+ `COMMUNITY_SW_CONTENT_TYPE`, `Cache-Control: no-cache`) at that route, and the manifest from
199
+ `@blogmcp/sdk/community/manifest`. Inside Facebook's or Instagram's app the element offers „Otevřít v prohlížeči"
200
+ with the page's URL instead; on an iPhone or iPad it explains that the site must be added to the home screen first.
201
+ After a key rotation (or kb-api's „switch it on again" notice) it asks the member to switch them on again. The
202
+ switch reads on only for a subscription this member made in this browser (a shared browser shows another member's
203
+ as off) and that kb-api still holds: with the permission granted, the element asks kb-api on load whether it still
204
+ holds this browser's endpoint (`me/push/status` — a read: no worker registered, no permission asked, nothing
205
+ subscribed). kb-api drops a subscription after a sign-out everywhere or a dead endpoint without telling the page;
206
+ the switch then reads off and stays off until the member presses „Zapnout upozornění" — a sign-out everywhere never
207
+ brings notifications back on its own. When kb-api cannot answer (the status, or the current key's `me/push/vapid` that is checked first), the switch
208
+ reads off too, without a sentence. The element also clears `#u=` from the address when
209
+ it is misconfigured and renders nothing.
210
+
211
+ The digest e-mail links to your settings page with the unsubscribe token in the fragment (`#u=…`). The element
212
+ reads it once, clears it from the address bar, and sends it only when the member clicks „Odhlásit denní souhrn"
213
+ (a mail scanner that opens the link unsubscribes nobody). That works without a session. Route your settings page
214
+ (`settingsPath`) where kb-api's e-mails point.
215
+
123
216
  Events (bubbling, composed):
124
217
  - `bcm-posted`, `{ state: 'visible' | 'held' }`: the reader's comment went out;
125
218
  - `bcm-notice-submitted`, `{ contactKept }`: a notice was received.
@@ -134,7 +227,9 @@ The properties are `--bcm-bg`, `-fg`, `-muted`, `-line`, `-soft`, `-accent`, `-a
134
227
  ## Content Security Policy
135
228
 
136
229
  - 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.
230
+ - `connect-src 'self'` is enough: the elements only talk to your own base. With live updates on a headless
231
+ site, add your instance's host (`https://<instance host>` and `wss://<instance host>`): the socket goes there.
232
+ - Browser notifications need `worker-src 'self'` (and `manifest-src 'self'` for the manifest).
138
233
  - The notice form with Turnstile needs `script-src` and `frame-src` for `https://challenges.cloudflare.com`.
139
234
  Only a page with the notice form loads that script.
140
235
 
@@ -143,5 +238,7 @@ The properties are `--bcm-bg`, `-fg`, `-muted`, `-line`, `-soft`, `-accent`, `-a
143
238
  ```bash
144
239
  npm run build # ESM + the <script> build (+ size budget, 200 KiB gzip)
145
240
  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)
241
+ npm run test:e2e # Playwright: desktop + iPhone profile + a plain bundler, mocked community API (ports 8286/8287);
242
+ # m2.e2e.ts drives the centre (its link to a reply's message), the badge, the settings,
243
+ # a profile, the welcome page and the composer's mention limit
147
244
  ```