@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
@@ -0,0 +1,234 @@
1
+ /**
2
+ * `<blogmcp-topic conversation-id>`'s brain (m3-contracts.md §3.1–§3.5, §4.5, §5.3): the topic's thread — the
3
+ * discussion's brain over the SDK's topic store (createTopicThreadController: the composer, replies, edits with the
4
+ * title of the opening post, reactions, reports, blocks, the onboarding, the brakes, live updates or the head poll) —
5
+ * and what the topic has of its own:
6
+ *
7
+ * - its header (title, state, pin, category) from `forum/topics/get`, a title read through topicTitleOf (deleted, or
8
+ * withheld from this viewer);
9
+ * - the follow button (the topic's `following`; `me/follows/set` by its conversation id);
10
+ * - for a moderator of its category (`viewer.moderator`): close or reopen it, pin or unpin it, move it into another
11
+ * category they moderate, and on a message — remove it (a rule and the facts; the statement comes back) or pass it to
12
+ * the team (`moderation/escalate`, a category of the illegal line). kb-api re-checks the appointment on every call:
13
+ * a refusal is `notFound`, and the topic is read again.
14
+ */
15
+ import { reactive } from 'vue'
16
+ import {
17
+ ESCALATION_CATEGORIES,
18
+ communityRulesOf,
19
+ isCategoryModerator,
20
+ type CaseCategory,
21
+ type CommunityCallOptions,
22
+ type CommunityClient,
23
+ type CommunityFailure,
24
+ type CommunityRule,
25
+ type DiscussionMessage,
26
+ type ForumCategory,
27
+ type Topic,
28
+ } from '@blogmcp/sdk/community'
29
+ import { createTopicThreadController, type TopicThreadController, type TopicThreadControllerOptions } from './discussion'
30
+ import { createFollow, type FollowController } from './follow'
31
+ import type { TextRef } from './i18n'
32
+ import { failureText } from './outcomes'
33
+ import { codePointLength, normalizeBody } from './text'
34
+
35
+ /** The facts of a moderator's removal, as an appeal's text (kb-api's `facts` limit is its own; the composer's is safe). */
36
+ const FACTS_MAX = 4000
37
+
38
+ export type ModeratorDialogKind = 'remove' | 'escalate' | 'move'
39
+
40
+ /** A moderator's open dialog: a message's removal or escalation, or the topic's move. */
41
+ export interface ModeratorDialog {
42
+ kind: ModeratorDialogKind
43
+ /** The message (remove, escalate). */
44
+ seq: number | null
45
+ ruleId: string
46
+ facts: string
47
+ category: CaseCategory | ''
48
+ categoryId: string
49
+ sending: boolean
50
+ failure: CommunityFailure | null
51
+ /** A removal's statement of reasons, as kb-api rendered it. */
52
+ statement: string | null
53
+ done: boolean
54
+ }
55
+
56
+ export interface TopicExtrasUi {
57
+ /** The rules of the tenant's current rules text (a moderator's removal cites one), once read. */
58
+ rules: CommunityRule[] | null
59
+ /** The categories a moderator may move the topic into (theirs, but this one), once read. */
60
+ moveTargets: ForumCategory[] | null
61
+ dialog: ModeratorDialog | null
62
+ busy: Record<string, boolean>
63
+ announcement: TextRef | null
64
+ announceId: number
65
+ }
66
+
67
+ export interface TopicController {
68
+ readonly thread: TopicThreadController
69
+ readonly follow: FollowController | null
70
+ readonly extras: TopicExtrasUi
71
+ start(): Promise<void>
72
+ /** The viewer moderates the topic's category (kb-api's `viewer.moderator`, me/get's appointments beside it). */
73
+ isModerator(): boolean
74
+ /** A moderator's message actions apply (another member's live message). */
75
+ canModerate(m: DiscussionMessage): boolean
76
+ setState(state: 'open' | 'locked'): Promise<void>
77
+ setPinned(pinned: boolean): Promise<void>
78
+ openDialog(kind: ModeratorDialogKind, seq?: number | null): void
79
+ closeDialog(): void
80
+ submitDialog(): Promise<void>
81
+ dispose(): void
82
+ }
83
+
84
+ export function createTopicController(o: TopicThreadControllerOptions): TopicController {
85
+ const { client, lang } = o
86
+ const conversationId = o.conversationId.toLowerCase()
87
+ const thread = createTopicThreadController(o)
88
+ const abort = new AbortController()
89
+ const call: CommunityCallOptions = { signal: abort.signal }
90
+ let disposed = false
91
+ const follow: FollowController | null = createFollow({ client, target: { conversationId }, lang })
92
+
93
+ const extras = reactive<TopicExtrasUi>({ rules: null, moveTargets: null, dialog: null, busy: {}, announcement: null, announceId: 0 }) as TopicExtrasUi
94
+
95
+ const announce = (text: TextRef): void => {
96
+ extras.announcement = text
97
+ extras.announceId += 1
98
+ }
99
+
100
+ const topic = () => thread.state.thread.topic
101
+
102
+ // The follow button follows the header: a re-read (another tab, a resync) may change it.
103
+ let lastFollowing: boolean | null = null
104
+ const unsubscribe = thread.store.subscribe((s) => {
105
+ const following = s.topic?.following
106
+ if (typeof following !== 'boolean' || following === lastFollowing) return
107
+ if (lastFollowing === null && follow && follow.state.phase === 'idle') void follow.load(following)
108
+ else follow?.sync(following)
109
+ lastFollowing = following
110
+ })
111
+
112
+ function isModerator(): boolean {
113
+ const t = topic()
114
+ if (!t?.category) return false
115
+ return thread.state.thread.viewer.moderator === true || (thread.state.me !== null && isCategoryModerator(thread.state.me, t.category.id))
116
+ }
117
+
118
+ const canModerate = (m: DiscussionMessage): boolean =>
119
+ isModerator() && !thread.isOwn(m) && !m.pending && m.seq > 0 && m.body !== null && m.state !== 'deleted' && m.author.kind === 'member'
120
+
121
+ /** A moderator's change answered the topic: the store takes the header; a refusal reads the topic again. */
122
+ async function topicChange(key: string, run: () => Promise<{ ok: true; value: { topic: Topic } } | CommunityFailure>): Promise<void> {
123
+ if (extras.busy[key]) return
124
+ extras.busy[key] = true
125
+ const r = await run()
126
+ delete extras.busy[key]
127
+ if (disposed) return
128
+ if (r.ok) {
129
+ thread.store.applyTopic(r.value.topic)
130
+ announce({ key: 'moderation.done' })
131
+ return
132
+ }
133
+ announce(failureText(r, lang))
134
+ // The appointment or the topic changed under the page (kb-api's notFound): what is true now.
135
+ if (r.code === 'notFound') void thread.refresh()
136
+ }
137
+
138
+ async function loadRules(): Promise<void> {
139
+ if (extras.rules !== null) return
140
+ const r = await client.legal.current({ kind: 'rules', locale: lang }, call)
141
+ if (!disposed && r.ok) extras.rules = communityRulesOf(r.value.bodyJson)
142
+ }
143
+
144
+ async function loadMoveTargets(): Promise<void> {
145
+ if (extras.moveTargets !== null) return
146
+ const r = await client.forum.categories.list(call)
147
+ if (disposed || !r.ok) return
148
+ const here = topic()?.category?.id
149
+ const me = thread.state.me
150
+ extras.moveTargets = r.value.categories.filter((c) => c.kind === 'topics' && c.id !== here && me !== null && isCategoryModerator(me, c.id))
151
+ }
152
+
153
+ return {
154
+ thread,
155
+ follow,
156
+ extras,
157
+ async start() {
158
+ await thread.start()
159
+ },
160
+ isModerator,
161
+ canModerate,
162
+ setState: (state) => topicChange(`state:${state}`, () => client.moderation.setTopicState({ conversationId, state }, call)),
163
+ setPinned: (pinned) => topicChange(`pin:${pinned}`, () => client.moderation.pinTopic({ conversationId, pinned }, call)),
164
+ openDialog(kind, seq = null) {
165
+ extras.dialog = { kind, seq, ruleId: '', facts: '', category: '', categoryId: '', sending: false, failure: null, statement: null, done: false }
166
+ if (kind === 'remove') void loadRules()
167
+ if (kind === 'move') void loadMoveTargets()
168
+ },
169
+ closeDialog() {
170
+ extras.dialog = null
171
+ },
172
+ async submitDialog() {
173
+ const d = extras.dialog
174
+ if (!d || d.sending || d.done) return
175
+ if (d.kind === 'move') {
176
+ if (d.categoryId === '') return
177
+ d.sending = true
178
+ const r = await client.moderation.moveTopic({ conversationId, categoryId: d.categoryId }, call)
179
+ d.sending = false
180
+ if (disposed) return
181
+ if (r.ok) {
182
+ thread.store.applyTopic(r.value.topic)
183
+ extras.dialog = null
184
+ extras.moveTargets = null
185
+ announce({ key: 'moderation.done' })
186
+ return
187
+ }
188
+ d.failure = r
189
+ return
190
+ }
191
+ const m = d.seq === null ? null : thread.state.thread.messages.flatMap((x) => [x, ...x.replies]).find((x) => x.seq === d.seq)
192
+ if (!m) return
193
+ if (d.kind === 'escalate') {
194
+ if (d.category === '' || !ESCALATION_CATEGORIES.includes(d.category)) return
195
+ d.sending = true
196
+ const r = await client.moderation.escalate({ conversationId, seq: m.seq, category: d.category }, call)
197
+ d.sending = false
198
+ if (disposed) return
199
+ if (r.ok) {
200
+ d.done = true
201
+ announce({ key: 'moderation.escalated' })
202
+ return
203
+ }
204
+ d.failure = r
205
+ if (r.code === 'notFound') void thread.refresh()
206
+ return
207
+ }
208
+ // remove: a rule of the current rules text and the facts, as the statement will state them.
209
+ const facts = normalizeBody(d.facts)
210
+ if (d.ruleId === '' || facts === '' || codePointLength(facts) > FACTS_MAX) return
211
+ d.sending = true
212
+ const r = await client.moderation.remove({ conversationId, seq: m.seq, ruleId: d.ruleId, facts, locale: lang }, call)
213
+ d.sending = false
214
+ if (disposed) return
215
+ if (r.ok) {
216
+ d.done = true
217
+ d.statement = r.value.statement
218
+ announce({ key: 'moderation.removed' })
219
+ void thread.refresh()
220
+ return
221
+ }
222
+ d.failure = r
223
+ if (r.code === 'notFound') void thread.refresh()
224
+ },
225
+ dispose() {
226
+ if (disposed) return
227
+ disposed = true
228
+ abort.abort()
229
+ unsubscribe()
230
+ follow?.dispose()
231
+ thread.dispose()
232
+ },
233
+ }
234
+ }
package/src/element.ts CHANGED
@@ -1,19 +1,43 @@
1
1
  /// <reference path="./shims.d.ts" />
2
2
  /**
3
- * The three custom elements (Vue inside a shadow root each), m1-contracts.md §10.4:
3
+ * The custom elements (Vue inside a shadow root each), m1-contracts.md §10.4 and m2-contracts.md §3.10:
4
4
  *
5
- * <blogmcp-discussion> article-id, api-base, site-base, lang, sign-in-href, routes, poll-ms, nonce
6
- * <blogmcp-notice-form> api-base, site-base, lang, turnstile-site-key, target | article-id, routes, nonce
7
- * <blogmcp-my-community> api-base, site-base, lang, decision-id, routes, sign-in-href, nonce
5
+ * <blogmcp-discussion> article-id, api-base, site-base, lang, sign-in-href, routes, poll-ms, nonce
6
+ * (+ the `realtime` property: a session factory the host passes, M2)
7
+ * <blogmcp-notice-form> api-base, site-base, lang, turnstile-site-key, target | article-id, routes, nonce
8
+ * <blogmcp-my-community> api-base, site-base, lang, decision-id, routes, sign-in-href, nonce
9
+ * <blogmcp-notification-centre> api-base, site-base, lang, routes, sign-in-href, nonce (M2)
10
+ * <blogmcp-badge> api-base, site-base, lang, routes, poll-ms, nonce (M2)
11
+ * <blogmcp-member-settings> api-base, site-base, lang, sw-path, routes, sign-in-href, nonce (M2)
12
+ * <blogmcp-profile> handle, api-base, site-base, lang, routes, sign-in-href, nonce (M2)
13
+ * <blogmcp-welcome> api-base, site-base, lang, routes, sign-in-href, nonce (M2)
14
+ * <blogmcp-forum> api-base, site-base, lang, routes, sign-in-href, nonce (M3)
15
+ * <blogmcp-forum-category> slug, api-base, site-base, lang, routes, sign-in-href, nonce (M3)
16
+ * <blogmcp-topic> conversation-id, api-base, site-base, lang, routes, sign-in-href, poll-ms,
17
+ * nonce (+ the `realtime` property, as the discussion's) (M3)
18
+ * <blogmcp-forum-search> api-base, site-base, lang, routes, sign-in-href, nonce (M3)
19
+ * <blogmcp-follow-button> target-kind, target-id, following, api-base, site-base, lang, nonce (M3)
20
+ * <blogmcp-forum-moderation> category-id, api-base, site-base, lang, routes, nonce (M3)
8
21
  *
9
22
  * Each carries ONE inline stylesheet in its shadow root, with the CSP nonce honoured: the one given when the
10
23
  * elements are defined (the script build passes `document.currentScript.nonce`), else the element's own
11
24
  * `nonce` — read from the property, because a page with a CSP hides the attribute once it is connected.
12
25
  */
13
26
  import { defineCustomElement, type Component } from 'vue'
27
+ import BadgeView from './ui/BadgeView.vue'
14
28
  import DiscussionView from './ui/DiscussionView.vue'
29
+ import FollowButtonView from './ui/FollowButtonView.vue'
30
+ import ForumCategoryView from './ui/ForumCategoryView.vue'
31
+ import ForumModerationView from './ui/ForumModerationView.vue'
32
+ import ForumSearchView from './ui/ForumSearchView.vue'
33
+ import ForumView from './ui/ForumView.vue'
34
+ import MemberSettingsView from './ui/MemberSettingsView.vue'
15
35
  import MyCommunityView from './ui/MyCommunityView.vue'
16
36
  import NoticeFormView from './ui/NoticeFormView.vue'
37
+ import NotificationCentreView from './ui/NotificationCentreView.vue'
38
+ import ProfileView from './ui/ProfileView.vue'
39
+ import TopicView from './ui/TopicView.vue'
40
+ import WelcomeView from './ui/WelcomeView.vue'
17
41
  import css from './ui/styles.css?inline'
18
42
 
19
43
  /**
@@ -36,6 +60,19 @@ function ensureVueFeatureFlags(): void {
36
60
  export const DISCUSSION_ELEMENT_TAG = 'blogmcp-discussion'
37
61
  export const NOTICE_FORM_ELEMENT_TAG = 'blogmcp-notice-form'
38
62
  export const MY_COMMUNITY_ELEMENT_TAG = 'blogmcp-my-community'
63
+ /** M2 (m2-contracts.md §3.10). */
64
+ export const NOTIFICATION_CENTRE_ELEMENT_TAG = 'blogmcp-notification-centre'
65
+ export const BADGE_ELEMENT_TAG = 'blogmcp-badge'
66
+ export const MEMBER_SETTINGS_ELEMENT_TAG = 'blogmcp-member-settings'
67
+ export const PROFILE_ELEMENT_TAG = 'blogmcp-profile'
68
+ export const WELCOME_ELEMENT_TAG = 'blogmcp-welcome'
69
+ /** M3 (m3-contracts.md §5.3): the forum's elements — they live here, not in the SDK (A33). */
70
+ export const FORUM_ELEMENT_TAG = 'blogmcp-forum'
71
+ export const FORUM_CATEGORY_ELEMENT_TAG = 'blogmcp-forum-category'
72
+ export const TOPIC_ELEMENT_TAG = 'blogmcp-topic'
73
+ export const FORUM_SEARCH_ELEMENT_TAG = 'blogmcp-forum-search'
74
+ export const FOLLOW_BUTTON_ELEMENT_TAG = 'blogmcp-follow-button'
75
+ export const FORUM_MODERATION_ELEMENT_TAG = 'blogmcp-forum-moderation'
39
76
 
40
77
  export interface DefineCommunityElementOptions {
41
78
  /** Another tag name for the element. */
@@ -47,7 +84,22 @@ export interface DefineCommunityElementOptions {
47
84
  export interface DefineCommunityElementsOptions {
48
85
  /** CSP nonce for the elements' `<style>`. */
49
86
  nonce?: string
50
- tags?: { discussion?: string; noticeForm?: string; myCommunity?: string }
87
+ tags?: {
88
+ discussion?: string
89
+ noticeForm?: string
90
+ myCommunity?: string
91
+ notificationCentre?: string
92
+ badge?: string
93
+ memberSettings?: string
94
+ profile?: string
95
+ welcome?: string
96
+ forum?: string
97
+ forumCategory?: string
98
+ topic?: string
99
+ forumSearch?: string
100
+ followButton?: string
101
+ forumModeration?: string
102
+ }
51
103
  }
52
104
 
53
105
  /** The element's own nonce: the property (a CSP hides the attribute once connected), else the attribute. */
@@ -101,9 +153,75 @@ export function defineMyCommunityElement(opts: DefineCommunityElementOptions = {
101
153
  define(opts.tag ?? MY_COMMUNITY_ELEMENT_TAG, MyCommunityView, opts.nonce)
102
154
  }
103
155
 
104
- /** Register all three (once). Browser only. */
156
+ /** Register `<blogmcp-notification-centre>` (once). Browser only. */
157
+ export function defineNotificationCentreElement(opts: DefineCommunityElementOptions = {}): void {
158
+ define(opts.tag ?? NOTIFICATION_CENTRE_ELEMENT_TAG, NotificationCentreView, opts.nonce)
159
+ }
160
+
161
+ /** Register `<blogmcp-badge>` (once). Browser only. */
162
+ export function defineBadgeElement(opts: DefineCommunityElementOptions = {}): void {
163
+ define(opts.tag ?? BADGE_ELEMENT_TAG, BadgeView, opts.nonce)
164
+ }
165
+
166
+ /** Register `<blogmcp-member-settings>` (once). Browser only. */
167
+ export function defineMemberSettingsElement(opts: DefineCommunityElementOptions = {}): void {
168
+ define(opts.tag ?? MEMBER_SETTINGS_ELEMENT_TAG, MemberSettingsView, opts.nonce)
169
+ }
170
+
171
+ /** Register `<blogmcp-profile>` (once). Browser only. */
172
+ export function defineProfileElement(opts: DefineCommunityElementOptions = {}): void {
173
+ define(opts.tag ?? PROFILE_ELEMENT_TAG, ProfileView, opts.nonce)
174
+ }
175
+
176
+ /** Register `<blogmcp-welcome>` (once). Browser only. */
177
+ export function defineWelcomeElement(opts: DefineCommunityElementOptions = {}): void {
178
+ define(opts.tag ?? WELCOME_ELEMENT_TAG, WelcomeView, opts.nonce)
179
+ }
180
+
181
+ /** M3: register `<blogmcp-forum>` (once). Browser only. */
182
+ export function defineForumElement(opts: DefineCommunityElementOptions = {}): void {
183
+ define(opts.tag ?? FORUM_ELEMENT_TAG, ForumView, opts.nonce)
184
+ }
185
+
186
+ /** M3: register `<blogmcp-forum-category>` (once). Browser only. */
187
+ export function defineForumCategoryElement(opts: DefineCommunityElementOptions = {}): void {
188
+ define(opts.tag ?? FORUM_CATEGORY_ELEMENT_TAG, ForumCategoryView, opts.nonce)
189
+ }
190
+
191
+ /** M3: register `<blogmcp-topic>` (once). Browser only. */
192
+ export function defineTopicElement(opts: DefineCommunityElementOptions = {}): void {
193
+ define(opts.tag ?? TOPIC_ELEMENT_TAG, TopicView, opts.nonce)
194
+ }
195
+
196
+ /** M3: register `<blogmcp-forum-search>` (once). Browser only. */
197
+ export function defineForumSearchElement(opts: DefineCommunityElementOptions = {}): void {
198
+ define(opts.tag ?? FORUM_SEARCH_ELEMENT_TAG, ForumSearchView, opts.nonce)
199
+ }
200
+
201
+ /** M3: register `<blogmcp-follow-button>` (once). Browser only. */
202
+ export function defineFollowButtonElement(opts: DefineCommunityElementOptions = {}): void {
203
+ define(opts.tag ?? FOLLOW_BUTTON_ELEMENT_TAG, FollowButtonView, opts.nonce)
204
+ }
205
+
206
+ /** M3: register `<blogmcp-forum-moderation>` (once). Browser only. */
207
+ export function defineForumModerationElement(opts: DefineCommunityElementOptions = {}): void {
208
+ define(opts.tag ?? FORUM_MODERATION_ELEMENT_TAG, ForumModerationView, opts.nonce)
209
+ }
210
+
211
+ /** Register all fourteen (once). Browser only. */
105
212
  export function defineCommunityElements(opts: DefineCommunityElementsOptions = {}): void {
106
213
  defineDiscussionElement({ tag: opts.tags?.discussion, nonce: opts.nonce })
107
214
  defineNoticeFormElement({ tag: opts.tags?.noticeForm, nonce: opts.nonce })
108
215
  defineMyCommunityElement({ tag: opts.tags?.myCommunity, nonce: opts.nonce })
216
+ defineNotificationCentreElement({ tag: opts.tags?.notificationCentre, nonce: opts.nonce })
217
+ defineBadgeElement({ tag: opts.tags?.badge, nonce: opts.nonce })
218
+ defineMemberSettingsElement({ tag: opts.tags?.memberSettings, nonce: opts.nonce })
219
+ defineProfileElement({ tag: opts.tags?.profile, nonce: opts.nonce })
220
+ defineWelcomeElement({ tag: opts.tags?.welcome, nonce: opts.nonce })
221
+ defineForumElement({ tag: opts.tags?.forum, nonce: opts.nonce })
222
+ defineForumCategoryElement({ tag: opts.tags?.forumCategory, nonce: opts.nonce })
223
+ defineTopicElement({ tag: opts.tags?.topic, nonce: opts.nonce })
224
+ defineForumSearchElement({ tag: opts.tags?.forumSearch, nonce: opts.nonce })
225
+ defineFollowButtonElement({ tag: opts.tags?.followButton, nonce: opts.nonce })
226
+ defineForumModerationElement({ tag: opts.tags?.forumModeration, nonce: opts.nonce })
109
227
  }
package/src/global.ts CHANGED
@@ -1,12 +1,48 @@
1
1
  /**
2
- * The `<script>` build: registers `<blogmcp-discussion>`, `<blogmcp-notice-form>` and `<blogmcp-my-community>`
3
- * with the script tag's own CSP nonce, and exposes `window.BlogmcpCommunity`.
2
+ * The `<script>` build: registers every element — `<blogmcp-discussion>`, `<blogmcp-notice-form>`,
3
+ * `<blogmcp-my-community>`, (M2) `<blogmcp-notification-centre>`, `<blogmcp-badge>`, `<blogmcp-member-settings>`,
4
+ * `<blogmcp-profile>`, `<blogmcp-welcome>` and (M3) `<blogmcp-forum>`, `<blogmcp-forum-category>`, `<blogmcp-topic>`,
5
+ * `<blogmcp-forum-search>`, `<blogmcp-follow-button>`, `<blogmcp-forum-moderation>` — with the script tag's own CSP
6
+ * nonce, and exposes `window.BlogmcpCommunity`. Live updates are not in it: a site that wants them bundles
7
+ * `@blogmcp/sdk/community/realtime` and sets the discussion's (or the topic's) `realtime` property.
4
8
  */
5
9
  import { defineCommunityElements } from './element'
6
- import { mountDiscussion, mountMyCommunity, mountNoticeForm } from './mount'
10
+ import {
11
+ mountBadge,
12
+ mountDiscussion,
13
+ mountFollowButton,
14
+ mountForum,
15
+ mountForumCategory,
16
+ mountForumModeration,
17
+ mountForumSearch,
18
+ mountMemberSettings,
19
+ mountMyCommunity,
20
+ mountNoticeForm,
21
+ mountNotificationCentre,
22
+ mountProfile,
23
+ mountTopic,
24
+ mountWelcome,
25
+ } from './mount'
7
26
  import { VERSION } from './version'
8
27
 
9
28
  const script = typeof document !== 'undefined' ? (document.currentScript as HTMLScriptElement | null) : null
10
29
  defineCommunityElements({ nonce: script?.nonce || undefined })
11
30
 
12
- export { defineCommunityElements, mountDiscussion, mountMyCommunity, mountNoticeForm, VERSION }
31
+ export {
32
+ defineCommunityElements,
33
+ mountDiscussion,
34
+ mountMyCommunity,
35
+ mountNoticeForm,
36
+ mountNotificationCentre,
37
+ mountBadge,
38
+ mountMemberSettings,
39
+ mountProfile,
40
+ mountWelcome,
41
+ mountForum,
42
+ mountForumCategory,
43
+ mountTopic,
44
+ mountForumSearch,
45
+ mountFollowButton,
46
+ mountForumModeration,
47
+ VERSION,
48
+ }
package/src/index.ts CHANGED
@@ -7,6 +7,12 @@
7
7
  * - `<blogmcp-discussion>` the discussion under an article;
8
8
  * - `<blogmcp-notice-form>` the DSA notice form, for anyone;
9
9
  * - `<blogmcp-my-community>` the reader's „Moje komunita", a statement page, a notifier's status page;
10
+ * - M2: `<blogmcp-notification-centre>` the notifications, `<blogmcp-badge>` the header's unread count,
11
+ * `<blogmcp-member-settings>` the switches, push and the digest's unsubscribe, `<blogmcp-profile>` a member's
12
+ * public profile, `<blogmcp-welcome>` the welcome page; the discussion's `realtime` property (live updates);
13
+ * - M3, the forum: `<blogmcp-forum>` its index, `<blogmcp-forum-category slug>` a category, `<blogmcp-topic
14
+ * conversation-id>` a topic (the discussion's view, with its `realtime` property), `<blogmcp-forum-search>`,
15
+ * `<blogmcp-follow-button target-kind target-id>`, `<blogmcp-forum-moderation>` a category moderator's queue;
10
16
  * - `defineCommunityElements()` and one `define…Element()` each — the custom elements;
11
17
  * - `mountDiscussion(…)`, `mountNoticeForm(…)`, `mountMyCommunity(…)` — mount one from code;
12
18
  * - `@blogmcp/community/vue`, `@blogmcp/community/react` — `<KbDiscussion>`, `<KbNoticeForm>`, `<KbMyCommunity>`;
@@ -17,14 +23,90 @@ export {
17
23
  defineDiscussionElement,
18
24
  defineNoticeFormElement,
19
25
  defineMyCommunityElement,
26
+ defineNotificationCentreElement,
27
+ defineBadgeElement,
28
+ defineMemberSettingsElement,
29
+ defineProfileElement,
30
+ defineWelcomeElement,
31
+ defineForumElement,
32
+ defineForumCategoryElement,
33
+ defineTopicElement,
34
+ defineForumSearchElement,
35
+ defineFollowButtonElement,
36
+ defineForumModerationElement,
20
37
  DISCUSSION_ELEMENT_TAG,
21
38
  NOTICE_FORM_ELEMENT_TAG,
22
39
  MY_COMMUNITY_ELEMENT_TAG,
40
+ NOTIFICATION_CENTRE_ELEMENT_TAG,
41
+ BADGE_ELEMENT_TAG,
42
+ MEMBER_SETTINGS_ELEMENT_TAG,
43
+ PROFILE_ELEMENT_TAG,
44
+ WELCOME_ELEMENT_TAG,
45
+ FORUM_ELEMENT_TAG,
46
+ FORUM_CATEGORY_ELEMENT_TAG,
47
+ TOPIC_ELEMENT_TAG,
48
+ FORUM_SEARCH_ELEMENT_TAG,
49
+ FOLLOW_BUTTON_ELEMENT_TAG,
50
+ FORUM_MODERATION_ELEMENT_TAG,
23
51
  type DefineCommunityElementOptions,
24
52
  type DefineCommunityElementsOptions,
25
53
  } from './element'
26
- export { mountDiscussion, mountNoticeForm, mountMyCommunity, type MountDiscussionOptions, type MountNoticeFormOptions, type MountMyCommunityOptions } from './mount'
27
- export { createDiscussionController, type DiscussionController, type DiscussionControllerOptions, type DiscussionUi, type ComposerMode, type Draft } from './core/discussion'
54
+ export {
55
+ mountDiscussion,
56
+ mountNoticeForm,
57
+ mountMyCommunity,
58
+ mountNotificationCentre,
59
+ mountBadge,
60
+ mountMemberSettings,
61
+ mountProfile,
62
+ mountWelcome,
63
+ mountForum,
64
+ mountForumCategory,
65
+ mountTopic,
66
+ mountForumSearch,
67
+ mountFollowButton,
68
+ mountForumModeration,
69
+ type MountDiscussionOptions,
70
+ type MountNoticeFormOptions,
71
+ type MountMyCommunityOptions,
72
+ type MountNotificationCentreOptions,
73
+ type MountBadgeOptions,
74
+ type MountMemberSettingsOptions,
75
+ type MountProfileOptions,
76
+ type MountWelcomeOptions,
77
+ type MountForumOptions,
78
+ type MountForumCategoryOptions,
79
+ type MountTopicOptions,
80
+ type MountForumSearchOptions,
81
+ type MountFollowButtonOptions,
82
+ type MountForumModerationOptions,
83
+ } from './mount'
84
+ export { createNotificationCentre, type NotificationCentreController, type CentreUi } from './core/centre'
85
+ export { createBadge, badgeText, BADGE_INTERVAL_MS, type BadgeController, type BadgeOptions, type BadgeUi } from './core/badge'
86
+ export { createMemberSettings, SETTING_KEYS, type MemberSettingsController, type MemberSettingsOptions, type SettingsUi, type SettingKey } from './core/settings'
87
+ export { createProfile, createWelcome, type ProfileController, type WelcomeController } from './core/profile'
88
+ export type { DiscussionRealtime, DiscussionRealtimeOptions, DiscussionRealtimeSession, DiscussionRealtimeMode, TopicRealtime, TopicRealtimeOptions } from './core/realtime'
89
+ export {
90
+ createDiscussionController,
91
+ createTopicThreadController,
92
+ type DiscussionController,
93
+ type DiscussionControllerOptions,
94
+ type DiscussionUi,
95
+ type ComposerMode,
96
+ type Draft,
97
+ type ThreadController,
98
+ type ThreadState,
99
+ type ThreadUi,
100
+ type TopicThreadController,
101
+ type TopicThreadControllerOptions,
102
+ } from './core/discussion'
103
+ // M3: the forum's brains (framework-free but for Vue's reactivity, as the others), for a host that renders its own view.
104
+ export { createForum, type ForumController, type ForumUi } from './core/forum'
105
+ export { createCategory, type CategoryController, type CategoryUi, type TopicDraft } from './core/forum-category'
106
+ export { createTopicController, type TopicController, type ModeratorDialog } from './core/topic'
107
+ export { createSearch, queryFromHash, searchLink, type SearchController, type SearchUi } from './core/forum-search'
108
+ export { createFollow, type FollowController, type FollowUi } from './core/follow'
109
+ export { createModeration, type ModerationController, type ModerationUi } from './core/moderation'
28
110
  export { createNoticeForm, type NoticeFormController, type NoticeFormOptions, type NoticeFormUi } from './core/notice'
29
111
  export { createPanel, type PanelController, type PanelOptions, type PanelUi } from './core/panel'
30
112
  export { createOnboarding, type OnboardingModel, type OnboardingOptions, type OnboardingStep } from './core/onboarding'