@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
@@ -30,6 +30,36 @@ export interface CommunityRoutes {
30
30
  * becomes a link to it (m1-contracts.md §7.6). No default — without it the note stays text.
31
31
  */
32
32
  consultationPath?: string;
33
+ /**
34
+ * M2 (m2-contracts.md §3.10): a member's public profile; `{handle}` is the handle (`/komunita/u/{handle}`) — a
35
+ * template, as statementPath is. A mention in a message links to it.
36
+ */
37
+ profilePath?: string;
38
+ /** M2: the member's notification settings (where the digest's unsubscribe link lands with `#u=`). */
39
+ settingsPath?: string;
40
+ /** M2: the notification centre (the badge links to it). */
41
+ notificationsPath?: string;
42
+ /** M2: the welcome page after joining. */
43
+ welcomePath?: string;
44
+ /** M2: the discussions overview. */
45
+ discussionsPath?: string;
46
+ /**
47
+ * M2 T10: an article's page; `{slug}` is the article's slug (`/blog/articles/{slug}`, the storefront `/blog/{slug}`)
48
+ * — a template, as profilePath is. The notification centre links a reply's or a mention's message on it (the
49
+ * message's anchor in the fragment). No default: without it those rows stay unlinked.
50
+ */
51
+ articlePath?: string;
52
+ /** M3 (m3-contracts.md §5.1, D-M3-12): the forum — its categories, their latest topics, the search box. */
53
+ forumPath?: string;
54
+ /** M3: a category's page; `{slug}` is the category's slug (`/komunita/c/{slug}`) — a template, as profilePath is. */
55
+ categoryPath?: string;
56
+ /**
57
+ * M3: a topic's page; `{id}` is its conversation id (`/komunita/t/{id}`) — a template; a message's anchor goes in the
58
+ * fragment (`#bcm-msg-<seq>`, topicMessageLink).
59
+ */
60
+ topicPath?: string;
61
+ /** M3: the forum's search (the forum's search box goes there with the query in the fragment, `#q=…`). */
62
+ searchPath?: string;
33
63
  }
34
64
  /** A path on the host's own site (the SDK's links.ts rule). */
35
65
  export declare function isSitePath(path: unknown): path is string;
@@ -42,7 +72,8 @@ export declare function parseRoutes(value: unknown): CommunityRoutes;
42
72
  */
43
73
  export declare function siteBaseOf(value: unknown): string | null;
44
74
  /**
45
- * The pages with the managed blog's defaults filled in under the site's base (statementPath stays a template).
75
+ * The pages with the managed blog's defaults filled in under the site's base (statementPath, profilePath, M3's
76
+ * categoryPath and topicPath stay templates; articlePath, a template too, has no default).
46
77
  * A page the host or the tenant names is its own full path.
47
78
  */
48
79
  export declare function routesWithDefaults(routes: CommunityRoutes, tenant?: {
@@ -55,6 +86,16 @@ export declare function routesWithDefaults(routes: CommunityRoutes, tenant?: {
55
86
  reportPath: string;
56
87
  noticesPath: string;
57
88
  consultationPath: string | null;
89
+ profilePath: string;
90
+ settingsPath: string;
91
+ notificationsPath: string;
92
+ welcomePath: string;
93
+ discussionsPath: string;
94
+ articlePath: string | null;
95
+ forumPath: string;
96
+ categoryPath: string;
97
+ topicPath: string;
98
+ searchPath: string;
58
99
  };
59
100
  /** A notice target of one of kb-api's four shapes, or null (the form then has nothing to report). */
60
101
  export declare function parseTarget(value: unknown): NoticeTarget | null;
@@ -0,0 +1,30 @@
1
+ import { type CommunityClient } from '@blogmcp/sdk/community';
2
+ /** The badge's poll (the contract's 60 s). */
3
+ export declare const BADGE_INTERVAL_MS = 60000;
4
+ /** A count above it reads `99+`. */
5
+ export declare const BADGE_MAX = 99;
6
+ export interface BadgeUi {
7
+ /** `hidden`: nothing to show (a visitor, no member, a dark module, or not decided yet). */
8
+ phase: 'pending' | 'hidden' | 'shown';
9
+ count: number;
10
+ }
11
+ export interface BadgeController {
12
+ readonly state: BadgeUi;
13
+ start(): Promise<void>;
14
+ /** Read the count now (coalesced with one on its way). */
15
+ refresh(): Promise<void>;
16
+ dispose(): void;
17
+ }
18
+ export interface BadgeOptions {
19
+ client: CommunityClient;
20
+ /**
21
+ * Default 60 s, and never more often (a smaller value above 0 reads as 60 s: the contract's rate per open tab);
22
+ * 0 or less = no poll (the count is read once and on visibility).
23
+ */
24
+ intervalMs?: number;
25
+ /** Whether the page is visible now (default: document.visibilityState). */
26
+ visible?: () => boolean;
27
+ }
28
+ /** The count as the badge shows it. */
29
+ export declare const badgeText: (count: number) => string;
30
+ export declare function createBadge(o: BadgeOptions): BadgeController;
@@ -0,0 +1,28 @@
1
+ import { type CommunityClient, type CommunityFailure, type NotificationItem } from '@blogmcp/sdk/community';
2
+ import type { TextRef } from './i18n';
3
+ export type CentrePhase = 'idle' | 'loading' | 'ready' | 'failed' | 'signIn';
4
+ export interface CentreUi {
5
+ phase: CentrePhase;
6
+ failure: CommunityFailure | null;
7
+ /** Known codes only (a code a newer kb-api sends is skipped, never shown raw). */
8
+ items: NotificationItem[];
9
+ nextCursor: string | null;
10
+ unreadOnly: boolean;
11
+ loadingMore: boolean;
12
+ busy: Record<string, boolean>;
13
+ announcement: TextRef | null;
14
+ announceId: number;
15
+ }
16
+ export interface NotificationCentreController {
17
+ readonly state: CentreUi;
18
+ load(): Promise<void>;
19
+ loadMore(): Promise<void>;
20
+ setUnreadOnly(on: boolean): Promise<void>;
21
+ markRead(id: string): Promise<void>;
22
+ markAllRead(): Promise<void>;
23
+ unread(): number;
24
+ dispose(): void;
25
+ }
26
+ export declare function createNotificationCentre(o: {
27
+ client: CommunityClient;
28
+ }): NotificationCentreController;
@@ -0,0 +1,2 @@
1
+ /** The token this element's page carries, or null (none, or a cut link). */
2
+ export declare function digestTokenForElement(owner: object | null): string | null;
@@ -4,13 +4,41 @@
4
4
  * with its optimistic rows, the Russmedia confirmation and the cooldowns, reactions, blocks, reports, and the
5
5
  * inline onboarding. The element renders `controller.state`; tests drive it directly over a fake transport.
6
6
  *
7
+ * M3 (m3-contracts.md §5.3): the same brain under `<blogmcp-topic>` — a forum topic IS a discussion (its opening post is
8
+ * seq 1, its replies two levels deep), so createTopicThreadController runs every rule above over the SDK's topic store,
9
+ * `forum/topics/get` and `forum/topics/head`, posts by the topic's conversation id, and adds what a topic has of its
10
+ * own: a viewer the forum is not open to (`membersOnly`, with its `needs`) and a topic the viewer may not see
11
+ * (`notFound`) as states of the element, a write's `notFound` read again (settleTopicFailure: the forum's gate answers
12
+ * every refusal of a topic's id that way, A11), a closed topic, the forum's own brake beside the module's, the title of
13
+ * the opening post in its edit, and the art. 9 consent every forum write needs (U14). The article's own reactions are
14
+ * an article's only.
15
+ *
7
16
  * Every request goes through the SDK client (the host's relative base); nothing here builds a path.
8
17
  */
9
18
  import { type ShallowRef } from 'vue';
10
- import { type CaseCategory, type CommunityClient, type CommunityFailure, type CommunityRule, type DiscussionMessage, type DiscussionState, type DiscussionStore, type MyState, type ReactionTarget, type ViewerNeeds } from '@blogmcp/sdk/community';
19
+ import { type CaseCategory, type CommunityClient, type CommunityFailure, type CommunityRule, type DiscussionMessage, type DiscussionState, type DiscussionStore, type DiscussionViewer, type MyState, type ReactionTarget, type TopicStore, type TopicStoreState, type ViewerNeeds } from '@blogmcp/sdk/community';
11
20
  import type { Lang, TextRef } from './i18n';
12
21
  import { type OnboardingModel } from './onboarding';
13
- export type DiscussionPhase = 'loading' | 'ready' | 'failed' | 'hidden';
22
+ import type { DiscussionRealtime, TopicRealtime } from './realtime';
23
+ /**
24
+ * `membersOnly` (M3, a topic): the forum is not open to this viewer — `state.failure.needs` says what a member session
25
+ * still lacks (none: a visitor, sign in); `notFound` (M3, a topic): the topic is not there for this viewer.
26
+ */
27
+ export type DiscussionPhase = 'loading' | 'ready' | 'failed' | 'hidden' | 'membersOnly' | 'notFound';
28
+ /** What a discussion and a topic have in common, as the controller and the views read them (the SDK's two states). */
29
+ export interface ThreadState {
30
+ readonly conversationId: string | null;
31
+ readonly count: number;
32
+ readonly lastSeq: number;
33
+ readonly lastChangeAt: string | null;
34
+ readonly messages: readonly DiscussionMessage[];
35
+ readonly nextCursor: number | null;
36
+ readonly reactionSet: readonly string[];
37
+ readonly preset: string | null;
38
+ readonly viewer: DiscussionViewer;
39
+ readonly loaded: boolean;
40
+ readonly stale: boolean;
41
+ }
14
42
  /** One composer's text and what happened to it. */
15
43
  export interface Draft {
16
44
  body: string;
@@ -34,6 +62,11 @@ export interface InlineDraft extends Draft {
34
62
  seq: number;
35
63
  /** A reply to a reply names whom it answers. */
36
64
  replyingTo: string | null;
65
+ /**
66
+ * M3: the topic's title, on its author's edit of the opening post (seq 1) — sent with the body when it changed (one
67
+ * edit of the opening post, §2.5); null everywhere else.
68
+ */
69
+ title: string | null;
37
70
  }
38
71
  export interface ReportDraft {
39
72
  conversationId: string;
@@ -52,21 +85,34 @@ export interface ReportDraft {
52
85
  * onboarding waits); `ageRefused` (the age band declared in it was below the minimum: reading only, the
53
86
  * onboarding is not offered again); `sanction` (a restriction, suspension or ban in effect); `notMoved` (the
54
87
  * discussion takes no writes yet — one sentence); `restricted` (the member is read-only or deleting their
55
- * profile); `pending` (being decided).
88
+ * profile); `braked` (M2: the community is paused — `me/get`'s `module.brake`, or a write refused BRAKED: reading
89
+ * goes on, every member write waits — one sentence); `pending` (being decided).
90
+ *
91
+ * A module that is `read_only` changes nothing here (m2-contracts.md §3.1: the discussions under articles keep
92
+ * taking writes; only live updates, push and the Pro surfaces stop) — the discussion polls instead of a socket.
56
93
  */
57
- export type ComposerMode = 'write' | 'signIn' | 'onboarding' | 'ageRefused' | 'sanction' | 'notMoved' | 'restricted' | 'pending';
58
- export interface DiscussionUi {
94
+ export type ComposerMode = 'write' | 'signIn' | 'onboarding' | 'ageRefused' | 'sanction' | 'notMoved' | 'restricted' | 'braked' | 'pending'
95
+ /** M3, a topic: closed (`locked`, `archived`, or a state this SDK does not know) — the composer gives way to a sentence. */
96
+ | 'locked'
97
+ /** M3, a topic: the module is `read_only` — the forum is one of its features and takes no writes meanwhile. */
98
+ | 'readOnly'
99
+ /** M3, a topic: the viewer reads only (the team at the keyboard: it writes through the KB admin) — nothing is shown. */
100
+ | 'none';
101
+ export interface ThreadUi<S extends ThreadState> {
59
102
  phase: DiscussionPhase;
60
- /** Why the first load failed (`failed`). */
103
+ /** Why the first load failed (`failed`), or why the topic is not shown (`membersOnly`, `notFound`). */
61
104
  failure: CommunityFailure | null;
62
- thread: DiscussionState;
105
+ thread: S;
63
106
  loadingMore: boolean;
64
107
  refreshing: boolean;
65
108
  /** A head poll saw messages the page does not show yet. */
66
109
  newMessages: boolean;
67
110
  me: MyState | null;
68
- /** What a write or the member's state taught: no writes yet, or none for this member. */
69
- block: 'notMoved' | 'restricted' | null;
111
+ /**
112
+ * What a write or the member's state taught: no writes yet, none for this member, or a pause (BRAKED); M3, a topic:
113
+ * the module is `read_only` (the forum is a Pro surface).
114
+ */
115
+ block: 'notMoved' | 'restricted' | 'braked' | 'readOnly' | null;
70
116
  composer: Draft;
71
117
  inline: InlineDraft | null;
72
118
  report: ReportDraft | null;
@@ -82,10 +128,22 @@ export interface DiscussionUi {
82
128
  * (§7.4.2), so the element does: the onboarding is not offered again while it lives.
83
129
  */
84
130
  refusedAge: number | null;
131
+ /**
132
+ * M2 T9: the onboarding just finished with the art. 9 consent refused (`public_write`): the composer says once,
133
+ * in view, what the refusal leaves (writing in the public discussion). Not kept beyond this element.
134
+ */
135
+ art9RefusedNote: boolean;
136
+ /**
137
+ * M2 (§3.5): the live updates' session fell back to polling (live updates are off here, or the server cut the
138
+ * socket for good — 4501): the discussion says so once and keeps polling; it never opens the socket again itself.
139
+ */
140
+ realtimeOff: boolean;
85
141
  }
86
- export interface DiscussionControllerOptions {
142
+ /** `<blogmcp-discussion>`'s state: an article's discussion. */
143
+ export type DiscussionUi = ThreadUi<DiscussionState>;
144
+ /** What both controllers take besides what their thread is (an article, a topic). */
145
+ interface ThreadControllerOptions {
87
146
  client: CommunityClient;
88
- articleId: string;
89
147
  lang: Lang;
90
148
  /** Head poll interval (default 20 s; 0 = no polling). */
91
149
  pollMs?: number;
@@ -97,9 +155,23 @@ export interface DiscussionControllerOptions {
97
155
  /** The onboarding's age band was refused: the element keeps it for its next discussions. */
98
156
  onAgeRefused?(minAge: number): void;
99
157
  }
100
- export interface DiscussionController {
101
- readonly state: DiscussionUi;
102
- readonly store: DiscussionStore;
158
+ export interface DiscussionControllerOptions extends ThreadControllerOptions {
159
+ articleId: string;
160
+ /**
161
+ * M2 (§3.5, §3.10): the host's live-updates session factory (`createRealtimeSession` of
162
+ * `@blogmcp/sdk/community/realtime`). Absent: the head poll, as before.
163
+ */
164
+ realtime?: DiscussionRealtime | null;
165
+ }
166
+ /** M3: `<blogmcp-topic>`'s thread — a forum topic by its conversation id. */
167
+ export interface TopicThreadControllerOptions extends ThreadControllerOptions {
168
+ conversationId: string;
169
+ /** The host's live-updates session factory over the topic's store (`createRealtimeSession`). Absent: the head poll. */
170
+ realtime?: TopicRealtime | null;
171
+ }
172
+ /** What a thread's controller does — the article's discussion's and a topic's alike; the views read it. */
173
+ export interface ThreadController<S extends ThreadState = ThreadState> {
174
+ readonly state: ThreadUi<S>;
103
175
  readonly onboarding: ShallowRef<OnboardingModel | null>;
104
176
  /** The first load, then the poll. */
105
177
  start(): Promise<void>;
@@ -135,10 +207,43 @@ export interface DiscussionController {
135
207
  openReport(m: DiscussionMessage): void;
136
208
  closeReport(): void;
137
209
  submitReport(): Promise<void>;
138
- beginOnboarding(): void;
210
+ /**
211
+ * The onboarding inline; `offerArt9` asks the art. 9 consent also of a member who refused it (`public_write`, M2 T9) —
212
+ * the give-consent link on their own message held until the consent. M3, a topic: the art. 9 step is asked before a
213
+ * write whatever the tenant asks elsewhere (`forceArt9`, implied), but not to read the forum (`toRead`).
214
+ */
215
+ beginOnboarding(options?: OnboardingStart): void;
139
216
  cancelOnboarding(): void;
140
217
  /** Seconds a draft still waits (0 when it may go). */
141
218
  waitLeft(draft: Draft): number;
219
+ /**
220
+ * M2 T10: the distinct `@handle`s one message of the viewer may carry (me/get's `caps.mentionsPerMessage`), or null
221
+ * when nothing is known to limit it (no member yet, an older kb-api, a module whose mentions notify nobody).
222
+ */
223
+ mentionCap(): number | null;
224
+ /** M3: the edit of this message carries the topic's title (its author's opening post of a topic). */
225
+ editsTitle(m: Pick<DiscussionMessage, 'seq' | 'author'>): boolean;
142
226
  dispose(): void;
143
227
  }
228
+ /** `<blogmcp-discussion>`'s controller: an article's discussion over the SDK's discussion store. */
229
+ export interface DiscussionController extends ThreadController<DiscussionState> {
230
+ readonly store: DiscussionStore;
231
+ }
232
+ /** M3: `<blogmcp-topic>`'s thread over the SDK's topic store (its header is `state.thread.topic`). */
233
+ export interface TopicThreadController extends ThreadController<TopicStoreState> {
234
+ readonly store: TopicStore;
235
+ }
236
+ /** How the inline onboarding starts (beginOnboarding). */
237
+ export interface OnboardingStart {
238
+ /** The give-consent flow (M2 T9): the art. 9 step for a member who refused it, no refusal offered. */
239
+ offerArt9?: boolean;
240
+ /** M3: the art. 9 step whatever the tenant asks elsewhere — a forum write's refusal named it (U14). */
241
+ forceArt9?: boolean;
242
+ /** M3, a topic: the onboarding the forum asks only to be read (the members-only state) — no art. 9 step forced. */
243
+ toRead?: boolean;
244
+ }
245
+ /** `<blogmcp-discussion>`'s controller over one article's discussion. */
144
246
  export declare function createDiscussionController(o: DiscussionControllerOptions): DiscussionController;
247
+ /** M3: `<blogmcp-topic>`'s thread over one forum topic (m3-contracts.md §3.1–§3.3, §5.3). */
248
+ export declare function createTopicThreadController(o: TopicThreadControllerOptions): TopicThreadController;
249
+ export {};
@@ -0,0 +1,26 @@
1
+ import { type CommunityClient, type CommunityFailure, type FollowTarget } from '@blogmcp/sdk/community';
2
+ import type { Lang, TextRef } from './i18n';
3
+ export type FollowPhase = 'idle' | 'loading' | 'ready' | 'signIn' | 'unavailable' | 'failed';
4
+ export interface FollowUi {
5
+ phase: FollowPhase;
6
+ following: boolean;
7
+ busy: boolean;
8
+ /** The last refusal of a click (the state stays as it was), or null. */
9
+ failure: CommunityFailure | null;
10
+ announcement: TextRef | null;
11
+ announceId: number;
12
+ }
13
+ export interface FollowController {
14
+ readonly state: FollowUi;
15
+ /** The state from what the page knows (`known`), else the member's follows. */
16
+ load(known?: boolean | null): Promise<void>;
17
+ /** A newer `following` the page learnt (a topic re-read): taken unless a click is on its way. */
18
+ sync(following: boolean): void;
19
+ toggle(): Promise<void>;
20
+ dispose(): void;
21
+ }
22
+ export declare function createFollow(o: {
23
+ client: CommunityClient;
24
+ target: FollowTarget;
25
+ lang: Lang;
26
+ }): FollowController;
@@ -5,6 +5,13 @@
5
5
  import { type Lang } from './i18n';
6
6
  /** An ISO timestamp as a date (and time); '' for anything that is no date. */
7
7
  export declare function formatDate(lang: Lang, iso: string | null | undefined, withTime?: boolean): string;
8
+ /**
9
+ * A month as `profile/get`'s `memberSince` gives it (`YYYY-MM`, UTC) in the reader's language, in the form a date
10
+ * takes inside a sentence („Členem od října 2026", "Member since October 2026"): Intl formats the first of the month
11
+ * and the day is left out — a month on its own would be the nominative („říjen"). The value as it came when Intl has
12
+ * no words, '' for anything that is no month.
13
+ */
14
+ export declare function formatMonth(lang: Lang, ym: string | null | undefined): string;
8
15
  /** Whether an ISO timestamp lies in the past (an appeal window, a sanction's end). */
9
16
  export declare function isPast(iso: string | null | undefined, now?: number): boolean;
10
17
  /**
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `<blogmcp-forum-category slug>`'s brain (m3-contracts.md §3.2, §3.3, §5.3): one category — its row of
3
+ * `forum/categories/list` (its description, who opens topics there, its mode, whether the member follows it) and its
4
+ * topics (`forum/topics/list` by the slug, the activity or the newest order, the forum's keyset cursor) — and the
5
+ * new-topic composer where the category takes topics from members (`forum/topics/create`: the title and the opening
6
+ * post as one text; the Russmedia confirmation, the cooldowns, the onboarding with the art. 9 consent a forum write
7
+ * needs, a pre-moderated topic waiting for review). In „Články" (kind `article_discussions`) the rows are the article
8
+ * discussions, and nothing is created there.
9
+ */
10
+ import { type ShallowRef } from 'vue';
11
+ import { type CommunityClient, type CommunityFailure, type ForumCategory, type ForumSort, type KnownTopicItem, type MyState } from '@blogmcp/sdk/community';
12
+ import type { Draft } from './discussion';
13
+ import type { Lang, TextRef } from './i18n';
14
+ import { type OnboardingModel } from './onboarding';
15
+ export type CategoryPhase = 'loading' | 'ready' | 'failed' | 'hidden' | 'membersOnly' | 'notFound';
16
+ /** The new topic's draft: the composer's, with its title. */
17
+ export interface TopicDraft extends Draft {
18
+ title: string;
19
+ }
20
+ export interface CategoryUi {
21
+ phase: CategoryPhase;
22
+ failure: CommunityFailure | null;
23
+ me: MyState | null;
24
+ /** The category's row of the list (null when the list did not name it: the page header stays). */
25
+ category: ForumCategory | null;
26
+ /** The page's own header of the category (its id, slug, name, kind). */
27
+ header: {
28
+ id: string;
29
+ slug: string;
30
+ name: string;
31
+ kind: string;
32
+ } | null;
33
+ items: KnownTopicItem[];
34
+ nextCursor: string | null;
35
+ sort: ForumSort;
36
+ loadingMore: boolean;
37
+ /** The list is read again (another order). */
38
+ sorting: boolean;
39
+ composer: TopicDraft;
40
+ /** The topic the member just opened (its link), and whether it waits for review. */
41
+ created: {
42
+ conversationId: string;
43
+ held: boolean;
44
+ } | null;
45
+ /** kb-api said the category takes no topic from the member (TOPIC_CREATION_CLOSED). */
46
+ creationClosed: boolean;
47
+ /**
48
+ * A write was refused BRAKED — by the forum's own brake (`surface: 'forum'`) or the module's (none named): the composer
49
+ * gives way to the pause until the category is read again.
50
+ */
51
+ brakedBy: 'forum' | 'module' | null;
52
+ clock: number;
53
+ announcement: TextRef | null;
54
+ announceId: number;
55
+ }
56
+ export interface CategoryController {
57
+ readonly state: CategoryUi;
58
+ readonly onboarding: ShallowRef<OnboardingModel | null>;
59
+ load(): Promise<void>;
60
+ loadMore(): Promise<void>;
61
+ setSort(sort: ForumSort): Promise<void>;
62
+ /** The member may open a topic here now (the composer shows). */
63
+ canCreate(): boolean;
64
+ /** Why the composer does not show where it would: the team's category, a pause, read_only — or null. */
65
+ notCreating(): 'team' | 'paused' | 'readOnly' | null;
66
+ create(): Promise<void>;
67
+ setConfirmed(code: string, checked: boolean): void;
68
+ sendConfirmed(): Promise<void>;
69
+ beginOnboarding(options?: {
70
+ forceArt9?: boolean;
71
+ }): void;
72
+ cancelOnboarding(): void;
73
+ waitLeft(): number;
74
+ dispose(): void;
75
+ }
76
+ export declare function createCategory(o: {
77
+ client: CommunityClient;
78
+ slug: string;
79
+ lang: Lang;
80
+ now?: () => number;
81
+ onCreated?(topic: {
82
+ conversationId: string;
83
+ held: boolean;
84
+ }): void;
85
+ }): CategoryController;
@@ -0,0 +1,35 @@
1
+ import { type CommunityClient, type CommunityFailure, type ForumCategory, type ForumSearchResult, type KnownForumSpace } from '@blogmcp/sdk/community';
2
+ export type SearchPhase = 'idle' | 'searching' | 'ready' | 'failed' | 'hidden' | 'membersOnly';
3
+ export interface SearchUi {
4
+ phase: SearchPhase;
5
+ failure: CommunityFailure | null;
6
+ query: string;
7
+ /** '' every space. */
8
+ space: KnownForumSpace | '';
9
+ /** '' every category. */
10
+ categoryId: string;
11
+ /** The categories to narrow by (read once). */
12
+ categories: ForumCategory[];
13
+ /** The query is too short or too long to ask (said, nothing sent). */
14
+ invalid: 'short' | 'long' | null;
15
+ results: ForumSearchResult[];
16
+ page: number;
17
+ hasMore: boolean;
18
+ loadingMore: boolean;
19
+ }
20
+ export interface SearchController {
21
+ readonly state: SearchUi;
22
+ /** The categories for the filter (and the forum's own refusals: a visitor learns the forum is for members). */
23
+ load(): Promise<void>;
24
+ search(): Promise<void>;
25
+ more(): Promise<void>;
26
+ dispose(): void;
27
+ }
28
+ /** The query a page's fragment carries (`#q=<encoded>`), or null. */
29
+ export declare function queryFromHash(hash: string | null | undefined): string | null;
30
+ /** The search page's link with the query in its fragment (the forum's search box), or null. */
31
+ export declare function searchLink(searchPath: string, raw: string): string | null;
32
+ export declare function createSearch(o: {
33
+ client: CommunityClient;
34
+ query?: string | null;
35
+ }): SearchController;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * What every forum element reads out of `me/get` beside its own operation (m3-contracts.md §3.5, §4.8, §4.9, §5.3):
3
+ * the team's away mode (one sentence, `state.away`), the pause of the forum's writes — the module's brake (both
4
+ * surfaces: `state.braked`) or the forum's own (`state.forumBraked`; the article discussions go on) —, the module's
5
+ * `read_only` (the forum is one of its features), and what a `membersOnly` refusal asks of the viewer.
6
+ */
7
+ import { type CommunityFailure, type CommunityMessageKey, type MyState } from '@blogmcp/sdk/community';
8
+ export interface ForumNotices {
9
+ /** The team is away: new topics wait for review (`state.away`). */
10
+ away: boolean;
11
+ /** The forum's writes are paused — the module's brake, or the forum's own — or not. */
12
+ paused: 'module' | 'forum' | null;
13
+ /** The module is `read_only`: the forum takes no writes meanwhile (`error.readOnly`). */
14
+ readOnly: boolean;
15
+ }
16
+ /** The notices of `me/get`'s module block, at `now`. */
17
+ export declare function forumNoticesOf(me: MyState | null | undefined, now?: number): ForumNotices;
18
+ /** The sentence of a pause of the forum's writes. */
19
+ export declare const pausedKey: (paused: ForumNotices["paused"]) => CommunityMessageKey | null;
20
+ /**
21
+ * What a `membersOnly` refusal asks of the viewer (§3.1 step 3): to join — a member session that has not finished its
22
+ * onboarding (`needs` `onboarding` or `reconsent_rules`) — or to sign in (a visitor, or a session the tenant does not
23
+ * admit as a member: `sign_in`, or no needs at all).
24
+ */
25
+ export declare function membersOnlyAsk(failure: Pick<CommunityFailure, 'needs'> | null | undefined): 'join' | 'signIn';
26
+ /**
27
+ * Whether the viewer is a member who may take part (an onboarded member's session): me/get names a member and no
28
+ * need to sign in. The team at the keyboard reads the forum but writes through the KB admin.
29
+ */
30
+ export declare const isForumMember: (me: MyState | null | undefined) => boolean;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The forum's words for the discussion's components (m3-contracts.md §5.3): a topic's thread reuses the discussion's
3
+ * view — the same messages, composer, dialogs and refusals — and reads it with these keys in place of the
4
+ * discussion's, so a forum reads „příspěvek" (a post) where an article's discussion reads „komentář" (a comment). Every
5
+ * key is the SDK catalogue's in both languages; a plural maps by its name (test/forum-elements.spec.ts checks both).
6
+ */
7
+ import type { CommunityMessageKey } from '@blogmcp/sdk/community';
8
+ export declare const FORUM_WORDS: Readonly<Partial<Record<CommunityMessageKey, CommunityMessageKey>>>;
@@ -0,0 +1,21 @@
1
+ import { type CommunityClient, type CommunityFailure, type ForumCategory, type KnownTopicItem, type MyState } from '@blogmcp/sdk/community';
2
+ export type ForumPhase = 'loading' | 'ready' | 'failed' | 'hidden' | 'membersOnly';
3
+ /** The categories whose latest topics the index shows, and how many of each (one request a category). */
4
+ export declare const FORUM_LATEST_CATEGORIES = 10;
5
+ export declare const FORUM_LATEST_PER_CATEGORY = 3;
6
+ export interface ForumUi {
7
+ phase: ForumPhase;
8
+ failure: CommunityFailure | null;
9
+ me: MyState | null;
10
+ categories: ForumCategory[];
11
+ /** The latest rows of a category, by its id (a category whose list failed has none). */
12
+ latest: Record<string, KnownTopicItem[]>;
13
+ }
14
+ export interface ForumController {
15
+ readonly state: ForumUi;
16
+ load(): Promise<void>;
17
+ dispose(): void;
18
+ }
19
+ export declare function createForum(o: {
20
+ client: CommunityClient;
21
+ }): ForumController;
@@ -0,0 +1,80 @@
1
+ import { type CaseCategory, type CommunityClient, type CommunityFailure, type CommunityRule, type ForumCategory, type ModerationCase, type ModerationItem, type ModerationRestriction, type MyState } from '@blogmcp/sdk/community';
2
+ import type { Lang, TextRef } from './i18n';
3
+ export type ModerationPhase = 'loading' | 'ready' | 'none' | 'failed' | 'hidden' | 'membersOnly';
4
+ /** A case opened from the queue: its package and the decision being made. */
5
+ export interface OpenCase {
6
+ item: ModerationItem;
7
+ kase: ModerationCase | null;
8
+ loading: boolean;
9
+ restriction: ModerationRestriction | '';
10
+ facts: string;
11
+ /** The rule the restriction rests on ('' the case's own). */
12
+ ground: string;
13
+ /** kb-api asked for the rule (`invalid` on `contractualGround`): the rule must be chosen. */
14
+ needsRule: boolean;
15
+ /** The statement of the previewed decision, for the same choice. */
16
+ preview: string | null;
17
+ previewing: boolean;
18
+ deciding: boolean;
19
+ failure: CommunityFailure | null;
20
+ decided: boolean;
21
+ escalation: {
22
+ category: CaseCategory | '';
23
+ sending: boolean;
24
+ failure: CommunityFailure | null;
25
+ done: boolean;
26
+ } | null;
27
+ }
28
+ /** A pre-moderated message being reviewed. */
29
+ export interface PremodReview {
30
+ item: ModerationItem;
31
+ outcome: 'approve' | 'reject' | '';
32
+ ruleId: string;
33
+ facts: string;
34
+ sending: boolean;
35
+ failure: CommunityFailure | null;
36
+ statement: string | null;
37
+ done: 'approved' | 'rejected' | null;
38
+ /** M3 FX (R3): the author edited the text while it was being reviewed — `item` holds the new one; decide again. */
39
+ changed: boolean;
40
+ }
41
+ export interface ModerationUi {
42
+ phase: ModerationPhase;
43
+ failure: CommunityFailure | null;
44
+ me: MyState | null;
45
+ /** '' every category of the moderator's. */
46
+ categoryId: string;
47
+ /** The moderator's categories (their names), in the forum's order. */
48
+ categories: ForumCategory[];
49
+ items: ModerationItem[];
50
+ nextCursor: string | null;
51
+ loadingMore: boolean;
52
+ rules: CommunityRule[] | null;
53
+ open: OpenCase | null;
54
+ review: PremodReview | null;
55
+ announcement: TextRef | null;
56
+ announceId: number;
57
+ }
58
+ export interface ModerationController {
59
+ readonly state: ModerationUi;
60
+ load(): Promise<void>;
61
+ loadMore(): Promise<void>;
62
+ setCategory(categoryId: string): Promise<void>;
63
+ openCase(item: ModerationItem): Promise<void>;
64
+ closeCase(): void;
65
+ /** Whether the case's decision may be previewed now (a restriction, facts, a rule where one is needed). */
66
+ canPreview(): boolean;
67
+ preview(): Promise<void>;
68
+ decide(): Promise<void>;
69
+ startEscalation(): void;
70
+ escalate(): Promise<void>;
71
+ openReview(item: ModerationItem, outcome: 'approve' | 'reject'): void;
72
+ closeReview(): void;
73
+ review(): Promise<void>;
74
+ dispose(): void;
75
+ }
76
+ export declare function createModeration(o: {
77
+ client: CommunityClient;
78
+ lang: Lang;
79
+ categoryId?: string | null;
80
+ }): ModerationController;