@blogmcp/community 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +243 -2
- package/dist/blogmcp-community.global.js +10 -0
- package/dist/chunks/mount-WadUnaTa.js +6436 -0
- package/dist/core/attrs.d.ts +99 -0
- package/dist/core/avatars.d.ts +16 -0
- package/dist/core/badge.d.ts +30 -0
- package/dist/core/centre.d.ts +28 -0
- package/dist/core/client.d.ts +27 -0
- package/dist/core/digest-token.d.ts +2 -0
- package/dist/core/discussion.d.ts +175 -0
- package/dist/core/download.d.ts +4 -0
- package/dist/core/format.d.ts +24 -0
- package/dist/core/i18n.d.ts +28 -0
- package/dist/core/names.d.ts +15 -0
- package/dist/core/notice.d.ts +50 -0
- package/dist/core/onboarding.d.ts +92 -0
- package/dist/core/outcomes.d.ts +29 -0
- package/dist/core/panel.d.ts +96 -0
- package/dist/core/profile.d.ts +40 -0
- package/dist/core/push.d.ts +63 -0
- package/dist/core/realtime.d.ts +35 -0
- package/dist/core/receipt.d.ts +5 -0
- package/dist/core/settings.d.ts +53 -0
- package/dist/core/text.d.ts +35 -0
- package/dist/core/turnstile.d.ts +33 -0
- package/dist/element.d.ts +47 -0
- package/dist/global.d.ts +11 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +45 -0
- package/dist/mount.d.ts +105 -0
- package/dist/react.d.ts +28 -0
- package/dist/react.js +55 -0
- package/dist/ui/AuthorAvatar.vue.d.ts +11 -0
- package/dist/ui/BadgeView.vue.d.ts +13 -0
- package/dist/ui/ConfirmDialog.vue.d.ts +20 -0
- package/dist/ui/DiscussionView.vue.d.ts +44 -0
- package/dist/ui/LinkedText.vue.d.ts +6 -0
- package/dist/ui/MemberSettingsView.vue.d.ts +13 -0
- package/dist/ui/MessageComposer.vue.d.ts +26 -0
- package/dist/ui/MessageItem.vue.d.ts +6 -0
- package/dist/ui/MessageMenu.vue.d.ts +10 -0
- package/dist/ui/MyCommunityView.vue.d.ts +16 -0
- package/dist/ui/NoticeFormView.vue.d.ts +26 -0
- package/dist/ui/NoticeStatusView.vue.d.ts +15 -0
- package/dist/ui/NotificationCentreView.vue.d.ts +12 -0
- package/dist/ui/OnboardingSteps.vue.d.ts +11 -0
- package/dist/ui/ProfileView.vue.d.ts +13 -0
- package/dist/ui/ReactionBar.vue.d.ts +13 -0
- package/dist/ui/ReportDialog.vue.d.ts +15 -0
- package/dist/ui/StatementView.vue.d.ts +15 -0
- package/dist/ui/WelcomeView.vue.d.ts +12 -0
- package/dist/ui/context.d.ts +21 -0
- package/dist/ui/discussion-context.d.ts +19 -0
- package/dist/ui/types.d.ts +7 -0
- package/dist/version.d.ts +1 -0
- package/dist/vue.d.ts +200 -0
- package/dist/vue.js +201 -0
- package/package.json +80 -4
- package/src/core/attrs.ts +186 -0
- package/src/core/avatars.ts +44 -0
- package/src/core/badge.ts +120 -0
- package/src/core/centre.ts +136 -0
- package/src/core/client.ts +54 -0
- package/src/core/digest-token.ts +26 -0
- package/src/core/discussion.ts +876 -0
- package/src/core/download.ts +21 -0
- package/src/core/format.ts +112 -0
- package/src/core/i18n.ts +49 -0
- package/src/core/names.ts +51 -0
- package/src/core/notice.ts +178 -0
- package/src/core/onboarding.ts +492 -0
- package/src/core/outcomes.ts +86 -0
- package/src/core/panel.ts +446 -0
- package/src/core/profile.ts +144 -0
- package/src/core/push.ts +174 -0
- package/src/core/realtime.ts +39 -0
- package/src/core/receipt.ts +28 -0
- package/src/core/settings.ts +363 -0
- package/src/core/text.ts +174 -0
- package/src/core/turnstile.ts +98 -0
- package/src/element.ts +165 -0
- package/src/global.ts +26 -0
- package/src/index.ts +70 -0
- package/src/mount.ts +234 -0
- package/src/react.ts +97 -0
- package/src/shims.d.ts +11 -0
- package/src/ui/AuthorAvatar.vue +26 -0
- package/src/ui/BadgeView.vue +51 -0
- package/src/ui/ConfirmDialog.vue +38 -0
- package/src/ui/DiscussionView.vue +334 -0
- package/src/ui/LinkedText.vue +24 -0
- package/src/ui/MemberSettingsView.vue +175 -0
- package/src/ui/MessageComposer.vue +141 -0
- package/src/ui/MessageItem.vue +223 -0
- package/src/ui/MessageMenu.vue +67 -0
- package/src/ui/MyCommunityView.vue +378 -0
- package/src/ui/NoticeFormView.vue +244 -0
- package/src/ui/NoticeStatusView.vue +82 -0
- package/src/ui/NotificationCentreView.vue +159 -0
- package/src/ui/OnboardingSteps.vue +174 -0
- package/src/ui/ProfileView.vue +91 -0
- package/src/ui/ReactionBar.vue +41 -0
- package/src/ui/ReportDialog.vue +88 -0
- package/src/ui/StatementView.vue +141 -0
- package/src/ui/WelcomeView.vue +72 -0
- package/src/ui/context.ts +75 -0
- package/src/ui/discussion-context.ts +30 -0
- package/src/ui/styles.css +259 -0
- package/src/ui/types.ts +7 -0
- package/src/version.ts +3 -0
- package/src/vue.ts +242 -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,244 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @blogmcp/community
|
|
2
2
|
|
|
3
|
-
|
|
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). 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;
|
|
10
|
+
- `<blogmcp-notice-form>`: the notice form for anyone, signed in or not (DSA art. 16), with Cloudflare
|
|
11
|
+
Turnstile;
|
|
12
|
+
- `<blogmcp-my-community>`: the reader's „Moje komunita" panel. It holds statements of reasons and appeals,
|
|
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
|
|
15
|
+
page (`decision-id`) and a notifier's status page (`#t=<receipt>` in the address).
|
|
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
|
+
|
|
37
|
+
Each element lives in a shadow root and renders text only. A member's links pass through `sanitizeUrl` and
|
|
38
|
+
carry `rel="nofollow ugc noopener noreferrer"`. Every text comes from the SDK's catalogue
|
|
39
|
+
(`communityMessage`), in Czech or English.
|
|
40
|
+
|
|
41
|
+
## Your site's side: one relative base
|
|
42
|
+
|
|
43
|
+
The elements never call kb-api. They POST to **your site's** relative base: `api-base`, by default
|
|
44
|
+
`/api/community` under your `site-base` (the root when you set none):
|
|
45
|
+
|
|
46
|
+
- a headless site or the storefront, served at the root: the BFF from `@blogmcp/sdk/community/server`,
|
|
47
|
+
mounted at `/api/community`;
|
|
48
|
+
- a managed blog, served under `/blog`: its own `/api` proxy, so set `site-base="/blog"`. The element then
|
|
49
|
+
posts to `/blog/api/community`, and its default pages are `/blog/community/…`.
|
|
50
|
+
|
|
51
|
+
> **On a managed blog, never `api-base="/api/community"`.** On the tenant's host, `/api/` is kb-api itself, not
|
|
52
|
+
> the blog. A request there skips the blog's proxy, so it carries no reader session. Every reader reads as
|
|
53
|
+
> signed out, every write is refused, and the notice form reaches no route. The defaults have the same trap:
|
|
54
|
+
> without `site-base`, the links go to `/community/…`, outside the blog.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// e.g. app/routes/api/community.$.ts (POST only)
|
|
58
|
+
import { createCommunityBff, communityServerEnv, fromWebRequest } from '@blogmcp/sdk/community/server'
|
|
59
|
+
|
|
60
|
+
const bff = createCommunityBff({
|
|
61
|
+
// kb-api as your server reaches it, WITH the `/api` prefix your instance serves it under:
|
|
62
|
+
// KB_API_URL = https://<your instance>/api (or `${process.env.BLOGMCP_API_URL}/api`).
|
|
63
|
+
kbApiUrl: process.env.KB_API_URL!,
|
|
64
|
+
siteUrls: ['https://www.example.com'],
|
|
65
|
+
memberToken: async (http) => /* your session → the member's JWT, or null */ null,
|
|
66
|
+
...communityServerEnv(process.env), // COMMUNITY_NOTICE_KEY
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
export async function action({ request }: { request: Request }) {
|
|
70
|
+
const { http, respond } = fromWebRequest(request)
|
|
71
|
+
return respond(await bff.handle(http))
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
An absolute `api-base` (or `site-base`) is refused, and the element renders nothing: kb-api's community
|
|
76
|
+
routes answer no CORS.
|
|
77
|
+
|
|
78
|
+
## Mount the elements
|
|
79
|
+
|
|
80
|
+
**Vue / Nuxt**
|
|
81
|
+
|
|
82
|
+
```vue
|
|
83
|
+
<script setup lang="ts">
|
|
84
|
+
import { KbDiscussion } from '@blogmcp/community/vue'
|
|
85
|
+
</script>
|
|
86
|
+
<template>
|
|
87
|
+
<article>…</article>
|
|
88
|
+
<KbDiscussion :article-id="article.id" api-base="/api/community" sign-in-href="/prihlaseni" />
|
|
89
|
+
</template>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**React / Next.js**
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { KbDiscussion } from '@blogmcp/community/react' // a client component
|
|
96
|
+
|
|
97
|
+
<KbDiscussion articleId={article.id} apiBase="/api/community" signInHref="/sign-in" />
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`KbNoticeForm`, `KbMyCommunity`, `KbNotificationCentre`, `KbBadge`, `KbMemberSettings`, `KbProfile` and
|
|
101
|
+
`KbWelcome` work the same way. Each wrapper mounts its element after hydration and
|
|
102
|
+
removes it on unmount. It renders a single empty `<div data-blogmcp-community="…">` as the mount point: the
|
|
103
|
+
discussion is part of the page's flow, so the element goes where the wrapper is.
|
|
104
|
+
|
|
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.
|
|
108
|
+
|
|
109
|
+
**Plain HTML**
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
<blogmcp-discussion article-id="…" api-base="/api/community" sign-in-href="/prihlaseni"></blogmcp-discussion>
|
|
113
|
+
<script src="https://cdn.jsdelivr.net/npm/@blogmcp/community@0.2.0/dist/blogmcp-community.global.js" nonce="…"></script>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The script build (`@blogmcp/community/global`) registers all eight elements. It exposes
|
|
117
|
+
`window.BlogmcpCommunity` and gives the elements its own `nonce`.
|
|
118
|
+
|
|
119
|
+
## Attributes
|
|
120
|
+
|
|
121
|
+
| Element | Attribute | |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| all | `api-base` | your relative base (default `/api/community` under `site-base`) |
|
|
124
|
+
| all | `site-base` | the path your site is served under (`/blog` on a managed blog); the defaults go under it |
|
|
125
|
+
| all | `lang` | `cs` or `en` (`sk` reads Czech); default `<html lang>` |
|
|
126
|
+
| all | `routes` | JSON with your pages (below) |
|
|
127
|
+
| all | `nonce` | CSP nonce for the element's stylesheet and the Turnstile script |
|
|
128
|
+
| `<blogmcp-discussion>` | `article-id` | the article |
|
|
129
|
+
| | `sign-in-href` | your sign-in page; the current path goes in its fragment |
|
|
130
|
+
| | `poll-ms` | how often to check for new comments while the discussion is in view (default 20000; 0 = off) |
|
|
131
|
+
| `<blogmcp-notice-form>` | `target` | JSON: `{ conversationId, seq }`, `{ memberId }`, `{ handle }` or `{ articleId }` |
|
|
132
|
+
| | `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 |
|
|
133
|
+
| | `turnstile-site-key` | your Turnstile site key; without it no widget renders |
|
|
134
|
+
| `<blogmcp-my-community>` | `decision-id` | open this statement of reasons |
|
|
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) |
|
|
143
|
+
|
|
144
|
+
`routes` names your community pages: `statementPath` (with `{id}`), `myCommunityPath`, `rulesPath`,
|
|
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
|
|
152
|
+
komunita" paths, set in its community settings, come before that fallback.
|
|
153
|
+
`consultationPath` (no default) turns the regulated preset's adverse-event note into a link to your
|
|
154
|
+
consultation page.
|
|
155
|
+
The receipt in a status link always goes in the fragment (`#t=…`). The page clears it from the address bar
|
|
156
|
+
as soon as it reads it.
|
|
157
|
+
|
|
158
|
+
A `transport` property replaces the HTTP layer, for tests.
|
|
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
|
+
|
|
216
|
+
Events (bubbling, composed):
|
|
217
|
+
- `bcm-posted`, `{ state: 'visible' | 'held' }`: the reader's comment went out;
|
|
218
|
+
- `bcm-notice-submitted`, `{ contactKept }`: a notice was received.
|
|
219
|
+
|
|
220
|
+
## Look
|
|
221
|
+
|
|
222
|
+
Theme the elements with the custom properties on them: `blogmcp-discussion { --bcm-accent: #2b7a4b }`.
|
|
223
|
+
The properties are `--bcm-bg`, `-fg`, `-muted`, `-line`, `-soft`, `-accent`, `-accent-fg`, `-danger`,
|
|
224
|
+
`-ok`, `-warn-bg`, `-shadow` and `-z`, plus `--bcm-font` and `--bcm-font-size`. Dark mode follows
|
|
225
|
+
`prefers-color-scheme`.
|
|
226
|
+
|
|
227
|
+
## Content Security Policy
|
|
228
|
+
|
|
229
|
+
- The styles are one `<style>` in each shadow root, carrying the nonce you pass.
|
|
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).
|
|
233
|
+
- The notice form with Turnstile needs `script-src` and `frame-src` for `https://challenges.cloudflare.com`.
|
|
234
|
+
Only a page with the notice form loads that script.
|
|
235
|
+
|
|
236
|
+
## Development
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
npm run build # ESM + the <script> build (+ size budget, 200 KiB gzip)
|
|
240
|
+
npm test # vitest (happy-dom): controllers, store, onboarding, outcomes, elements, text-only and closed-path scans
|
|
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
|
|
244
|
+
```
|