@blogmcp/community 0.1.0 → 0.3.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 (114) hide show
  1. package/README.md +162 -13
  2. package/dist/blogmcp-community.global.js +8 -8
  3. package/dist/chunks/mount--2_V2q3M.js +10063 -0
  4. package/dist/core/attrs.d.ts +42 -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 +120 -15
  9. package/dist/core/follow.d.ts +26 -0
  10. package/dist/core/format.d.ts +7 -0
  11. package/dist/core/forum-category.d.ts +85 -0
  12. package/dist/core/forum-search.d.ts +35 -0
  13. package/dist/core/forum-state.d.ts +30 -0
  14. package/dist/core/forum-words.d.ts +8 -0
  15. package/dist/core/forum.d.ts +21 -0
  16. package/dist/core/moderation.d.ts +80 -0
  17. package/dist/core/onboarding.d.ts +49 -2
  18. package/dist/core/outcomes.d.ts +2 -1
  19. package/dist/core/panel.d.ts +17 -1
  20. package/dist/core/profile.d.ts +45 -0
  21. package/dist/core/push.d.ts +63 -0
  22. package/dist/core/realtime.d.ts +46 -0
  23. package/dist/core/settings.d.ts +53 -0
  24. package/dist/core/text.d.ts +13 -0
  25. package/dist/core/topic.d.ts +47 -0
  26. package/dist/element.d.ts +47 -1
  27. package/dist/global.d.ts +8 -4
  28. package/dist/index.d.ts +20 -3
  29. package/dist/index.js +62 -12
  30. package/dist/mount.d.ts +89 -0
  31. package/dist/react.d.ts +27 -3
  32. package/dist/react.js +48 -4
  33. package/dist/ui/BadgeView.vue.d.ts +13 -0
  34. package/dist/ui/DiscussionView.vue.d.ts +11 -0
  35. package/dist/ui/FollowButtonView.vue.d.ts +17 -0
  36. package/dist/ui/FollowToggle.vue.d.ts +6 -0
  37. package/dist/ui/ForumCategoryView.vue.d.ts +13 -0
  38. package/dist/ui/ForumModerationView.vue.d.ts +12 -0
  39. package/dist/ui/ForumNotices.vue.d.ts +7 -0
  40. package/dist/ui/ForumSearchView.vue.d.ts +11 -0
  41. package/dist/ui/ForumView.vue.d.ts +12 -0
  42. package/dist/ui/LinkedText.vue.d.ts +1 -0
  43. package/dist/ui/MemberSettingsView.vue.d.ts +13 -0
  44. package/dist/ui/MembersOnly.vue.d.ts +15 -0
  45. package/dist/ui/MessageComposer.vue.d.ts +8 -2
  46. package/dist/ui/MessageMenu.vue.d.ts +2 -0
  47. package/dist/ui/ModeratorDialog.vue.d.ts +11 -0
  48. package/dist/ui/NotificationCentreView.vue.d.ts +12 -0
  49. package/dist/ui/OnboardingSteps.vue.d.ts +1 -1
  50. package/dist/ui/ProfileView.vue.d.ts +13 -0
  51. package/dist/ui/TopicRow.vue.d.ts +10 -0
  52. package/dist/ui/TopicView.vue.d.ts +32 -0
  53. package/dist/ui/WelcomeView.vue.d.ts +12 -0
  54. package/dist/ui/context.d.ts +7 -1
  55. package/dist/ui/discussion-context.d.ts +11 -3
  56. package/dist/vue.d.ts +273 -3
  57. package/dist/vue.js +235 -5
  58. package/package.json +3 -3
  59. package/src/core/attrs.ts +61 -2
  60. package/src/core/badge.ts +120 -0
  61. package/src/core/centre.ts +136 -0
  62. package/src/core/digest-token.ts +26 -0
  63. package/src/core/discussion.ts +497 -46
  64. package/src/core/follow.ts +139 -0
  65. package/src/core/format.ts +22 -0
  66. package/src/core/forum-category.ts +323 -0
  67. package/src/core/forum-search.ts +153 -0
  68. package/src/core/forum-state.ts +59 -0
  69. package/src/core/forum-words.ts +38 -0
  70. package/src/core/forum.ts +83 -0
  71. package/src/core/moderation.ts +414 -0
  72. package/src/core/onboarding.ts +177 -18
  73. package/src/core/outcomes.ts +23 -1
  74. package/src/core/panel.ts +39 -4
  75. package/src/core/profile.ts +155 -0
  76. package/src/core/push.ts +174 -0
  77. package/src/core/realtime.ts +52 -0
  78. package/src/core/settings.ts +363 -0
  79. package/src/core/text.ts +32 -0
  80. package/src/core/topic.ts +234 -0
  81. package/src/element.ts +124 -6
  82. package/src/global.ts +40 -4
  83. package/src/index.ts +84 -2
  84. package/src/mount.ts +250 -1
  85. package/src/react.ts +90 -3
  86. package/src/ui/BadgeView.vue +51 -0
  87. package/src/ui/DiscussionView.vue +43 -3
  88. package/src/ui/FollowButtonView.vue +85 -0
  89. package/src/ui/FollowToggle.vue +39 -0
  90. package/src/ui/ForumCategoryView.vue +243 -0
  91. package/src/ui/ForumModerationView.vue +309 -0
  92. package/src/ui/ForumNotices.vue +23 -0
  93. package/src/ui/ForumSearchView.vue +171 -0
  94. package/src/ui/ForumView.vue +128 -0
  95. package/src/ui/LinkedText.vue +11 -5
  96. package/src/ui/MemberSettingsView.vue +175 -0
  97. package/src/ui/MembersOnly.vue +59 -0
  98. package/src/ui/MessageComposer.vue +64 -11
  99. package/src/ui/MessageItem.vue +36 -7
  100. package/src/ui/MessageMenu.vue +3 -2
  101. package/src/ui/ModeratorDialog.vue +102 -0
  102. package/src/ui/MyCommunityView.vue +12 -0
  103. package/src/ui/NoticeStatusView.vue +15 -2
  104. package/src/ui/NotificationCentreView.vue +193 -0
  105. package/src/ui/OnboardingSteps.vue +17 -6
  106. package/src/ui/ProfileView.vue +102 -0
  107. package/src/ui/TopicRow.vue +70 -0
  108. package/src/ui/TopicView.vue +395 -0
  109. package/src/ui/WelcomeView.vue +72 -0
  110. package/src/ui/context.ts +8 -3
  111. package/src/ui/discussion-context.ts +11 -3
  112. package/src/ui/styles.css +30 -0
  113. package/src/vue.ts +271 -3
  114. package/dist/chunks/mount-DzjwnHyx.js +0 -4597
package/README.md CHANGED
@@ -4,13 +4,68 @@ 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", „Důvěryhodný člen"), never a
30
+ number; with the forum, „Moderátor" and the categories the member moderates now (an appointment, not a trust
31
+ level);
32
+ - `<blogmcp-welcome>`: the page after joining: the rules in summary and three links (the discussions, the
33
+ settings, the member's own profile; a visitor gets your sign-in link instead).
34
+
35
+ With the forum (M3) there are six more. The forum is for members only: a visitor reads one sentence and your
36
+ sign-in link, a member short of the onboarding „Vstoupit do komunity" (the onboarding opens in place; a forum write
37
+ needs the art. 9 consent whatever the tenant asks elsewhere).
38
+
39
+ - `<blogmcp-forum>`: the forum's index. The categories in the team's order, each with its count, its latest
40
+ topics and the link to all of them, and a search box that opens your search page with the query in the
41
+ address's fragment (`#q=…`, never the query string);
42
+ - `<blogmcp-forum-category slug="…">`: a category's topics, by activity or newest, pinned first, more a page at
43
+ a time. Where members open topics, the new-topic composer (a title of at most 200 characters, the opening post,
44
+ the link to the rules); a topic that waits for review says so and lists for its author only. In „Články" the
45
+ rows are the article discussions, each with its follow button;
46
+ - `<blogmcp-topic conversation-id="…">`: a topic's page. The header (title, category, closed, pinned, count),
47
+ the thread in the forum's words („příspěvek", where an article's discussion says „komentář"), the composer,
48
+ reactions, reports, the follow button and, for the opening post's author, the title's edit. A moderator of the
49
+ topic's category gets its menu: close or reopen it, pin it, move it, remove a message (with the rule and the
50
+ facts; the author gets a statement of reasons) or pass a message to the team;
51
+ - `<blogmcp-forum-search>`: the forum's search, as text. It reads the query from `#q=…`; a query shorter than two
52
+ characters is said, never sent;
53
+ - `<blogmcp-follow-button target-kind="…" target-id="…">`: follow a topic or an article discussion
54
+ (`conversation`) or a category (`category`). A follow the member may not have reads „not available"; an unfollow
55
+ never fails;
56
+ - `<blogmcp-forum-moderation>`: a category moderator's queue. The package of each case as text, the decision with
57
+ its statement previewed before it is decided, a matter passed to the team, the messages waiting for review
58
+ published or refused. It renders nothing for a member who moderates nothing.
59
+
60
+ A missing title reads one of three ways: „Smazané téma" only when the opening post was deleted, its own words
61
+ while it is withheld from the viewer, and its title with „Čeká na kontrolu" for the author of a topic waiting for
62
+ review. The forum's own brake pauses the forum's writes only, the article discussions' brake the discussions only
63
+ (the module's brake both); while the team is away the forum says so in one sentence.
64
+
65
+ `<blogmcp-discussion>` also links every `@handle` in a comment to that member's profile page (for a signed-in
66
+ reader; an anonymous one reads the mention as text), and takes live updates through its `realtime` property
67
+ (below).
68
+
14
69
  Each element lives in a shadow root and renders text only. A member's links pass through `sanitizeUrl` and
15
70
  carry `rel="nofollow ugc noopener noreferrer"`. Every text comes from the SDK's catalogue
16
71
  (`communityMessage`), in Czech or English.
@@ -74,21 +129,27 @@ import { KbDiscussion } from '@blogmcp/community/react' // a client component
74
129
  <KbDiscussion articleId={article.id} apiBase="/api/community" signInHref="/sign-in" />
75
130
  ```
76
131
 
77
- `KbNoticeForm` and `KbMyCommunity` work the same way. Each wrapper mounts its element after hydration and
132
+ `KbNoticeForm`, `KbMyCommunity`, `KbNotificationCentre`, `KbBadge`, `KbMemberSettings`, `KbProfile`,
133
+ `KbWelcome` and the forum's `KbForum`, `KbForumCategory`, `KbTopic`, `KbForumSearch`, `KbFollowButton` and
134
+ `KbForumModeration` work the same way. Each wrapper mounts its element after hydration and
78
135
  removes it on unmount. It renders a single empty `<div data-blogmcp-community="…">` as the mount point: the
79
136
  discussion is part of the page's flow, so the element goes where the wrapper is.
80
137
 
81
- `mountDiscussion(o)`, `mountNoticeForm(o)` and `mountMyCommunity(o)` do the same from any client-side hook.
82
- Each returns the unmount function.
138
+ `mountDiscussion(o)`, `mountNoticeForm(o)`, `mountMyCommunity(o)`, `mountNotificationCentre(o)`, `mountBadge(o)`,
139
+ `mountMemberSettings(o)`, `mountProfile(o)`, `mountWelcome(o)` and the forum's `mountForum(o)`,
140
+ `mountForumCategory(o)`, `mountTopic(o)`, `mountForumSearch(o)`, `mountFollowButton(o)` and
141
+ `mountForumModeration(o)` do the same from any client-side hook. Each returns the unmount function. The tag names
142
+ are exported too (`DISCUSSION_ELEMENT_TAG` … `FORUM_MODERATION_ELEMENT_TAG`), and `defineForumElement()` and its
143
+ siblings register one element alone.
83
144
 
84
145
  **Plain HTML**
85
146
 
86
147
  ```html
87
148
  <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>
149
+ <script src="https://cdn.jsdelivr.net/npm/@blogmcp/community@0.3.0/dist/blogmcp-community.global.js" nonce="…"></script>
89
150
  ```
90
151
 
91
- The script build (`@blogmcp/community/global`) registers all three elements. It exposes
152
+ The script build (`@blogmcp/community/global`) registers all fourteen elements. It exposes
92
153
  `window.BlogmcpCommunity` and gives the elements its own `nonce`.
93
154
 
94
155
  ## Attributes
@@ -108,10 +169,34 @@ The script build (`@blogmcp/community/global`) registers all three elements. It
108
169
  | | `turnstile-site-key` | your Turnstile site key; without it no widget renders |
109
170
  | `<blogmcp-my-community>` | `decision-id` | open this statement of reasons |
110
171
  | | `sign-in-href` | your sign-in page |
172
+ | `<blogmcp-notification-centre>` | `sign-in-href` | your sign-in page (a visitor's link) |
173
+ | `<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) |
174
+ | `<blogmcp-member-settings>` | `sw-path` | your service worker route (default `${site-base}/sw.js`) |
175
+ | | `sign-in-href` | your sign-in page |
176
+ | `<blogmcp-profile>` | `handle` | the member's handle (an `@` in front is fine) |
177
+ | | `sign-in-href` | your sign-in page (profiles are for signed-in readers) |
178
+ | `<blogmcp-welcome>` | `sign-in-href` | your sign-in page (in the profile link's place, for a visitor) |
179
+ | `<blogmcp-forum>` | `sign-in-href` | your sign-in page (a visitor's link) |
180
+ | `<blogmcp-forum-category>` | `slug` | the category's slug |
181
+ | | `sign-in-href` | your sign-in page |
182
+ | `<blogmcp-topic>` | `conversation-id` | the topic (its conversation id) |
183
+ | | `sign-in-href` | your sign-in page |
184
+ | | `poll-ms` | how often to check for new posts while the topic is in view (default 20000; 0 = off) |
185
+ | `<blogmcp-forum-search>` | `sign-in-href` | your sign-in page |
186
+ | `<blogmcp-follow-button>` | `target-kind` | `conversation` (a topic, an article discussion) or `category` |
187
+ | | `target-id` | its id |
188
+ | | `following` | `true` / `false` when your page knows it (left out: the element reads the member's follows) |
189
+ | `<blogmcp-forum-moderation>` | `category-id` | one of the moderator's categories only (left out: all of them) |
111
190
 
112
191
  `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
192
+ `reportPath`, `noticesPath` and, with the module, `profilePath` (with `{handle}`), `settingsPath`,
193
+ `notificationsPath`, `welcomePath` and `discussionsPath`, and with the forum `forumPath`, `categoryPath` (with
194
+ `{slug}`), `topicPath` (with `{id}`, the topic's conversation id) and `searchPath`. Each is a full path on your site, used as it is. A page you leave out
195
+ falls back to the managed blog's pages: `${site-base}/community/…`. `articlePath` (with `{slug}`; no default)
196
+ is your article page — the managed blogs' `/blog/articles/{slug}`, a shop's `/blog/{slug}`: the notification
197
+ centre links a reply or a mention to its message there (`…/<slug>#bcm-msg-<seq>`), and without it those rows stay
198
+ text. The discussion gives every message the anchor `bcm-msg-<seq>` and, opened at one, scrolls to it itself once
199
+ 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
200
  komunita" paths, set in its community settings, come before that fallback.
116
201
  `consultationPath` (no default) turns the regulated preset's adverse-event note into a link to your
117
202
  consultation page.
@@ -120,8 +205,67 @@ as soon as it reads it.
120
205
 
121
206
  A `transport` property replaces the HTTP layer, for tests.
122
207
 
208
+ ## Live updates (`realtime`)
209
+
210
+ The discussion keeps itself fresh by polling. For live updates, import the SDK's realtime entry yourself and pass
211
+ its `createRealtimeSession` as the discussion's `realtime` property (a property, never an attribute) — or the
212
+ topic's: `<blogmcp-topic>` takes it the same way (a change of the topic's header, its title or state, reads the
213
+ topic again). Your bundle
214
+ then needs `centrifuge` (≥ 5.7), the entry's peer dependency; the elements never import either, so a site
215
+ without it ships no socket code.
216
+
217
+ ```ts
218
+ import { createRealtimeSession } from '@blogmcp/sdk/community/realtime'
219
+ import { KbDiscussion } from '@blogmcp/community/react'
220
+
221
+ <KbDiscussion articleId={article.id} apiBase="/api/community" realtime={createRealtimeSession} />
222
+ // plain: document.querySelector('blogmcp-discussion').realtime = createRealtimeSession
223
+ ```
224
+
225
+ The element starts the session over its own discussion after the first page is read, and stops it when it
226
+ unmounts. It starts none for an anonymous reader, nor while the community's live updates are off (the module is
227
+ `read_only` or off, or the instance runs no realtime): the discussion polls then, with no socket. A session that
228
+ falls back to polling — kb-api answers `REALTIME_OFF`, or the server ends the connection for good — makes the
229
+ discussion say once that live updates are off; it is never reopened. When the session was revoked (4501), the
230
+ session asks kb-api for a fresh token once first: a member whose own password change ended the old session keeps
231
+ the page live under the new one; a refusal, or a second 4501, falls back for good. While the socket cannot get
232
+ through (the subscription is not live 15 s after connecting, or after the connection dropped), the discussion polls
233
+ meanwhile and says so once; Centrifuge keeps reconnecting, and the sentence goes when it is live again. An article
234
+ without comments yet has no channel to subscribe to: it polls for the first one and is never called off for that.
235
+
236
+ The composer knows the member's mention limit (`me/get`'s `caps.mentionsPerMessage`, by trust level): a comment
237
+ that mentions more distinct `@handle`s says so as it is typed and is not sent (kb-api refuses it too).
238
+
239
+ The URL to connect to comes from kb-api's token answer (the instance's own host, `/rt/…`), never from your
240
+ configuration.
241
+
242
+ ## Browser notifications and the digest's unsubscribe link
243
+
244
+ `<blogmcp-member-settings>` shows this browser's notifications only while the community offers them. „Zapnout
245
+ upozornění" (the member's click; nothing is registered on page load) registers **your** service worker at
246
+ `sw-path`, asks the member's permission, subscribes the browser and hands the subscription to kb-api; „Vypnout"
247
+ ends it. Serve the worker from `@blogmcp/sdk/community/sw` (`communityServiceWorkerSource`, as
248
+ `COMMUNITY_SW_CONTENT_TYPE`, `Cache-Control: no-cache`) at that route, and the manifest from
249
+ `@blogmcp/sdk/community/manifest`. Inside Facebook's or Instagram's app the element offers „Otevřít v prohlížeči"
250
+ with the page's URL instead; on an iPhone or iPad it explains that the site must be added to the home screen first.
251
+ After a key rotation (or kb-api's „switch it on again" notice) it asks the member to switch them on again. The
252
+ switch reads on only for a subscription this member made in this browser (a shared browser shows another member's
253
+ as off) and that kb-api still holds: with the permission granted, the element asks kb-api on load whether it still
254
+ holds this browser's endpoint (`me/push/status` — a read: no worker registered, no permission asked, nothing
255
+ subscribed). kb-api drops a subscription after a sign-out everywhere or a dead endpoint without telling the page;
256
+ the switch then reads off and stays off until the member presses „Zapnout upozornění" — a sign-out everywhere never
257
+ 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
258
+ reads off too, without a sentence. The element also clears `#u=` from the address when
259
+ it is misconfigured and renders nothing.
260
+
261
+ The digest e-mail links to your settings page with the unsubscribe token in the fragment (`#u=…`). The element
262
+ reads it once, clears it from the address bar, and sends it only when the member clicks „Odhlásit denní souhrn"
263
+ (a mail scanner that opens the link unsubscribes nobody). That works without a session. Route your settings page
264
+ (`settingsPath`) where kb-api's e-mails point.
265
+
123
266
  Events (bubbling, composed):
124
- - `bcm-posted`, `{ state: 'visible' | 'held' }`: the reader's comment went out;
267
+ - `bcm-posted`, `{ state: 'visible' | 'held' }`: the reader's comment (or, in a topic, post) went out;
268
+ - `bcm-topic-created`, `{ conversationId, state: 'visible' | 'held' }`: the member opened a topic;
125
269
  - `bcm-notice-submitted`, `{ contactKept }`: a notice was received.
126
270
 
127
271
  ## Look
@@ -134,7 +278,9 @@ The properties are `--bcm-bg`, `-fg`, `-muted`, `-line`, `-soft`, `-accent`, `-a
134
278
  ## Content Security Policy
135
279
 
136
280
  - 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.
281
+ - `connect-src 'self'` is enough: the elements only talk to your own base. With live updates on a headless
282
+ site, add your instance's host (`https://<instance host>` and `wss://<instance host>`): the socket goes there.
283
+ - Browser notifications need `worker-src 'self'` (and `manifest-src 'self'` for the manifest).
138
284
  - The notice form with Turnstile needs `script-src` and `frame-src` for `https://challenges.cloudflare.com`.
139
285
  Only a page with the notice form loads that script.
140
286
 
@@ -142,6 +288,9 @@ The properties are `--bcm-bg`, `-fg`, `-muted`, `-line`, `-soft`, `-accent`, `-a
142
288
 
143
289
  ```bash
144
290
  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)
291
+ npm test # vitest (happy-dom): controllers, store, onboarding, outcomes, elements, the forum's six, text-only and closed-path scans
292
+ npm run test:e2e # Playwright: desktop + iPhone profile + a plain bundler, mocked community API (ports 8286/8287);
293
+ # m2.e2e.ts drives the centre (its link to a reply's message), the badge, the settings,
294
+ # a profile, the welcome page and the composer's mention limit; plain-bundler.e2e.ts mounts
295
+ # M1's, M2's and the forum's elements from dist/ (a topic opened from a category included)
147
296
  ```