@blogmcp/community 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +243 -2
  3. package/dist/blogmcp-community.global.js +10 -0
  4. package/dist/chunks/mount-WadUnaTa.js +6436 -0
  5. package/dist/core/attrs.d.ts +99 -0
  6. package/dist/core/avatars.d.ts +16 -0
  7. package/dist/core/badge.d.ts +30 -0
  8. package/dist/core/centre.d.ts +28 -0
  9. package/dist/core/client.d.ts +27 -0
  10. package/dist/core/digest-token.d.ts +2 -0
  11. package/dist/core/discussion.d.ts +175 -0
  12. package/dist/core/download.d.ts +4 -0
  13. package/dist/core/format.d.ts +24 -0
  14. package/dist/core/i18n.d.ts +28 -0
  15. package/dist/core/names.d.ts +15 -0
  16. package/dist/core/notice.d.ts +50 -0
  17. package/dist/core/onboarding.d.ts +92 -0
  18. package/dist/core/outcomes.d.ts +29 -0
  19. package/dist/core/panel.d.ts +96 -0
  20. package/dist/core/profile.d.ts +40 -0
  21. package/dist/core/push.d.ts +63 -0
  22. package/dist/core/realtime.d.ts +35 -0
  23. package/dist/core/receipt.d.ts +5 -0
  24. package/dist/core/settings.d.ts +53 -0
  25. package/dist/core/text.d.ts +35 -0
  26. package/dist/core/turnstile.d.ts +33 -0
  27. package/dist/element.d.ts +47 -0
  28. package/dist/global.d.ts +11 -0
  29. package/dist/index.d.ts +33 -0
  30. package/dist/index.js +45 -0
  31. package/dist/mount.d.ts +105 -0
  32. package/dist/react.d.ts +28 -0
  33. package/dist/react.js +55 -0
  34. package/dist/ui/AuthorAvatar.vue.d.ts +11 -0
  35. package/dist/ui/BadgeView.vue.d.ts +13 -0
  36. package/dist/ui/ConfirmDialog.vue.d.ts +20 -0
  37. package/dist/ui/DiscussionView.vue.d.ts +44 -0
  38. package/dist/ui/LinkedText.vue.d.ts +6 -0
  39. package/dist/ui/MemberSettingsView.vue.d.ts +13 -0
  40. package/dist/ui/MessageComposer.vue.d.ts +26 -0
  41. package/dist/ui/MessageItem.vue.d.ts +6 -0
  42. package/dist/ui/MessageMenu.vue.d.ts +10 -0
  43. package/dist/ui/MyCommunityView.vue.d.ts +16 -0
  44. package/dist/ui/NoticeFormView.vue.d.ts +26 -0
  45. package/dist/ui/NoticeStatusView.vue.d.ts +15 -0
  46. package/dist/ui/NotificationCentreView.vue.d.ts +12 -0
  47. package/dist/ui/OnboardingSteps.vue.d.ts +11 -0
  48. package/dist/ui/ProfileView.vue.d.ts +13 -0
  49. package/dist/ui/ReactionBar.vue.d.ts +13 -0
  50. package/dist/ui/ReportDialog.vue.d.ts +15 -0
  51. package/dist/ui/StatementView.vue.d.ts +15 -0
  52. package/dist/ui/WelcomeView.vue.d.ts +12 -0
  53. package/dist/ui/context.d.ts +21 -0
  54. package/dist/ui/discussion-context.d.ts +19 -0
  55. package/dist/ui/types.d.ts +7 -0
  56. package/dist/version.d.ts +1 -0
  57. package/dist/vue.d.ts +200 -0
  58. package/dist/vue.js +201 -0
  59. package/package.json +80 -4
  60. package/src/core/attrs.ts +186 -0
  61. package/src/core/avatars.ts +44 -0
  62. package/src/core/badge.ts +120 -0
  63. package/src/core/centre.ts +136 -0
  64. package/src/core/client.ts +54 -0
  65. package/src/core/digest-token.ts +26 -0
  66. package/src/core/discussion.ts +876 -0
  67. package/src/core/download.ts +21 -0
  68. package/src/core/format.ts +112 -0
  69. package/src/core/i18n.ts +49 -0
  70. package/src/core/names.ts +51 -0
  71. package/src/core/notice.ts +178 -0
  72. package/src/core/onboarding.ts +492 -0
  73. package/src/core/outcomes.ts +86 -0
  74. package/src/core/panel.ts +446 -0
  75. package/src/core/profile.ts +144 -0
  76. package/src/core/push.ts +174 -0
  77. package/src/core/realtime.ts +39 -0
  78. package/src/core/receipt.ts +28 -0
  79. package/src/core/settings.ts +363 -0
  80. package/src/core/text.ts +174 -0
  81. package/src/core/turnstile.ts +98 -0
  82. package/src/element.ts +165 -0
  83. package/src/global.ts +26 -0
  84. package/src/index.ts +70 -0
  85. package/src/mount.ts +234 -0
  86. package/src/react.ts +97 -0
  87. package/src/shims.d.ts +11 -0
  88. package/src/ui/AuthorAvatar.vue +26 -0
  89. package/src/ui/BadgeView.vue +51 -0
  90. package/src/ui/ConfirmDialog.vue +38 -0
  91. package/src/ui/DiscussionView.vue +334 -0
  92. package/src/ui/LinkedText.vue +24 -0
  93. package/src/ui/MemberSettingsView.vue +175 -0
  94. package/src/ui/MessageComposer.vue +141 -0
  95. package/src/ui/MessageItem.vue +223 -0
  96. package/src/ui/MessageMenu.vue +67 -0
  97. package/src/ui/MyCommunityView.vue +378 -0
  98. package/src/ui/NoticeFormView.vue +244 -0
  99. package/src/ui/NoticeStatusView.vue +82 -0
  100. package/src/ui/NotificationCentreView.vue +159 -0
  101. package/src/ui/OnboardingSteps.vue +174 -0
  102. package/src/ui/ProfileView.vue +91 -0
  103. package/src/ui/ReactionBar.vue +41 -0
  104. package/src/ui/ReportDialog.vue +88 -0
  105. package/src/ui/StatementView.vue +141 -0
  106. package/src/ui/WelcomeView.vue +72 -0
  107. package/src/ui/context.ts +75 -0
  108. package/src/ui/discussion-context.ts +30 -0
  109. package/src/ui/styles.css +259 -0
  110. package/src/ui/types.ts +7 -0
  111. package/src/version.ts +3 -0
  112. package/src/vue.ts +242 -0
@@ -0,0 +1,99 @@
1
+ /**
2
+ * What the elements read from their attributes: the host's page routes (`routes`, JSON), the notice form's
3
+ * target (`target`, JSON, or `article-id`), and the link the discussion gives a reader without a session
4
+ * to report a message on the host's notice page.
5
+ *
6
+ * Every path is the HOST's (m1-contracts.md §10.2: the elements take the host's routes as attributes) and
7
+ * must be a path on its own site — one leading `/`, never `//` or `/\`, no blank, control character or
8
+ * backslash; anything else is dropped and the managed blog's default (COMMUNITY_ROUTES) applies.
9
+ *
10
+ * The defaults are router-relative (the SDK's COMMUNITY_ROUTES: "the blog prepends its own base"). A site served
11
+ * under a path — a managed blog under `/blog`, whose host's own `/api/` goes straight to kb-api — names it in
12
+ * `site-base`, and the defaults (the pages and the api-base) are taken under it; what the host names itself
13
+ * (`routes`, `api-base`, the tenant's MyState.links) is a full site path already and is used as it is.
14
+ */
15
+ import { type NoticeTarget } from '@blogmcp/sdk/community';
16
+ /** The host's community pages, as the `routes` attribute names them (all optional). */
17
+ export interface CommunityRoutes {
18
+ /** A statement of reasons; `{id}` is the decision's id (`/komunita/rozhodnuti/{id}`). */
19
+ statementPath?: string;
20
+ /** The „Moje komunita" panel. */
21
+ myCommunityPath?: string;
22
+ /** The rules page (rendered by the host without JavaScript). */
23
+ rulesPath?: string;
24
+ /** The notice form. */
25
+ reportPath?: string;
26
+ /** A notifier's status page (the receipt goes in its fragment). */
27
+ noticesPath?: string;
28
+ /**
29
+ * The site's consultation page (drhoubicka's „poradna"): the regulated preset's `report_to_manufacturer` note
30
+ * becomes a link to it (m1-contracts.md §7.6). No default — without it the note stays text.
31
+ */
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
+ }
53
+ /** A path on the host's own site (the SDK's links.ts rule). */
54
+ export declare function isSitePath(path: unknown): path is string;
55
+ /** The `routes` attribute: the site paths it names; anything malformed is left out. */
56
+ export declare function parseRoutes(value: unknown): CommunityRoutes;
57
+ /**
58
+ * The `site-base` attribute: the path the site is served under, without a trailing `/` (`/blog`; '' for the root,
59
+ * also when absent) — or null when it is no plain path (a URL, a query, a dot segment): the element then renders
60
+ * nothing, as for an absolute api-base.
61
+ */
62
+ export declare function siteBaseOf(value: unknown): string | null;
63
+ /**
64
+ * The pages with the managed blog's defaults filled in under the site's base (statementPath and profilePath stay
65
+ * templates; articlePath, a template too, has no default).
66
+ * A page the host or the tenant names is its own full path.
67
+ */
68
+ export declare function routesWithDefaults(routes: CommunityRoutes, tenant?: {
69
+ statementPath?: string | null;
70
+ myCommunityPath?: string | null;
71
+ } | null, siteBase?: string): {
72
+ statementPath: string;
73
+ myCommunityPath: string;
74
+ rulesPath: string;
75
+ reportPath: string;
76
+ noticesPath: string;
77
+ consultationPath: string | null;
78
+ profilePath: string;
79
+ settingsPath: string;
80
+ notificationsPath: string;
81
+ welcomePath: string;
82
+ discussionsPath: string;
83
+ articlePath: string | null;
84
+ };
85
+ /** A notice target of one of kb-api's four shapes, or null (the form then has nothing to report). */
86
+ export declare function parseTarget(value: unknown): NoticeTarget | null;
87
+ export type NoticeTargetKind = 'message' | 'profile' | 'article';
88
+ export declare function targetKind(target: NoticeTarget): NoticeTargetKind;
89
+ /**
90
+ * The notice page's link for a message, for a reader without a session (who cannot use the member's
91
+ * report): `${reportPath}#c=<conversationId>&s=<seq>`. The ids are public; they travel in the fragment so
92
+ * the notice page stays one address (no crawlable variants — the storefront's rule for the sign-in link).
93
+ */
94
+ export declare function reportLink(reportPath: string, target: {
95
+ conversationId: string;
96
+ seq: number;
97
+ }): string | null;
98
+ /** The target the discussion's report link carries (`#c=…&s=…`), or `#a=<articleId>`; null for anything else. */
99
+ export declare function targetFromHash(hash: string): NoticeTarget | null;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The avatar presets a member picks from (kb-api stores the id, `^[a-z0-9_]{1,64}$`, and knows no list).
3
+ * Each is one emoji: a screen reader names it in the reader's own language, so the picker needs no words of
4
+ * ours. An id this list does not know (a newer element's) shows the name's initial instead.
5
+ */
6
+ export interface AvatarPreset {
7
+ readonly id: string;
8
+ readonly emoji: string;
9
+ }
10
+ export declare const AVATAR_PRESETS: readonly AvatarPreset[];
11
+ /** The preset's emoji, or null for none or an unknown id. */
12
+ export declare function avatarEmoji(preset: string | null | undefined): string | null;
13
+ /** The first letter of a name (the avatar of a member without a known preset). */
14
+ export declare function initialOf(name: string | null | undefined): string;
15
+ /** A stable hue for a member id, so the same member keeps the same initial's colour (decoration only). */
16
+ export declare function hueOf(id: string | null | undefined): number;
@@ -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,27 @@
1
+ /**
2
+ * The client an element talks through: the SDK's `createCommunityClient` over the HOST's relative base
3
+ * (`api-base`; default `/api/community` under the site's `site-base` — a headless site's BFF, the storefront's,
4
+ * or with `site-base="/blog"` a managed blog's own proxy). Never kb-api directly: its community namespaces answer
5
+ * no CORS (m1-contracts.md §7.1), and the client refuses an absolute base. On a managed blog the tenant host's
6
+ * `/api/` IS kb-api, so `/api/community` there would skip the blog's proxy and its session. An element with an
7
+ * unusable base warns once and renders nothing.
8
+ */
9
+ import { type CommunityClient, type CommunityTransport } from '@blogmcp/sdk/community';
10
+ /** The base the hosts serve the community under, below their site base (§12; the BFF's default basePath). */
11
+ export declare const DEFAULT_API_BASE = "/api/community";
12
+ /**
13
+ * One console line per problem and page, for the site's developer — never a value from the page. The
14
+ * message starts with `[blogmcp-community]` (test/text-only.spec.ts tells developer lines from reader text
15
+ * by it).
16
+ */
17
+ export declare function warnOnce(key: string, message: `[blogmcp-community] ${string}`): void;
18
+ /**
19
+ * The `site-base` attribute as a path prefix ('' at the site's root), or null — the developer told once — when it
20
+ * is no path on this site.
21
+ */
22
+ export declare function siteBaseFor(value: string | null | undefined): string | null;
23
+ /**
24
+ * The client for an element's `api-base` (and an injected transport), or null when the base is not a relative
25
+ * path. Without an `api-base`: `/api/community` under the site's base.
26
+ */
27
+ export declare function communityClientFor(apiBase: string | null | undefined, transport?: CommunityTransport | null, siteBase?: string): CommunityClient | null;
@@ -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;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * `<blogmcp-discussion>`'s brain, without any view (m1-contracts.md §10.4): the SDK's discussion store fed by
3
+ * `discussions/list` pages and the `discussions/head` poll, the composer (a new message, a reply, an edit)
4
+ * with its optimistic rows, the Russmedia confirmation and the cooldowns, reactions, blocks, reports, and the
5
+ * inline onboarding. The element renders `controller.state`; tests drive it directly over a fake transport.
6
+ *
7
+ * Every request goes through the SDK client (the host's relative base); nothing here builds a path.
8
+ */
9
+ 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';
11
+ import type { Lang, TextRef } from './i18n';
12
+ import { type OnboardingModel } from './onboarding';
13
+ import type { DiscussionRealtime } from './realtime';
14
+ export type DiscussionPhase = 'loading' | 'ready' | 'failed' | 'hidden';
15
+ /** One composer's text and what happened to it. */
16
+ export interface Draft {
17
+ body: string;
18
+ sending: boolean;
19
+ failure: CommunityFailure | null;
20
+ /** CONFIRMATION_REQUIRED: the codes the author confirms before the same text goes again. */
21
+ confirm: {
22
+ codes: string[];
23
+ checked: string[];
24
+ } | null;
25
+ /** A cooldown: the epoch milliseconds until which this write waits. */
26
+ waitUntil: number | null;
27
+ /** A post's id, kept until kb-api took it: a retry of a write whose answer was lost cannot post twice. */
28
+ clientMsgId: string | null;
29
+ }
30
+ export type InlineKind = 'reply' | 'edit';
31
+ /** The one reply or edit open under a message (opening another closes it). */
32
+ export interface InlineDraft extends Draft {
33
+ kind: InlineKind;
34
+ /** The message edited, or the TOP-LEVEL message a reply goes under (two levels only). */
35
+ seq: number;
36
+ /** A reply to a reply names whom it answers. */
37
+ replyingTo: string | null;
38
+ }
39
+ export interface ReportDraft {
40
+ conversationId: string;
41
+ seq: number;
42
+ category: CaseCategory | '';
43
+ ruleId: string;
44
+ note: string;
45
+ rules: CommunityRule[] | null;
46
+ rulesFailed: boolean;
47
+ sending: boolean;
48
+ failure: CommunityFailure | null;
49
+ done: boolean;
50
+ }
51
+ /**
52
+ * Where the composer stands: `write`; `signIn` (no session that can write); `onboarding` (the inline
53
+ * onboarding waits); `ageRefused` (the age band declared in it was below the minimum: reading only, the
54
+ * onboarding is not offered again); `sanction` (a restriction, suspension or ban in effect); `notMoved` (the
55
+ * discussion takes no writes yet — one sentence); `restricted` (the member is read-only or deleting their
56
+ * profile); `braked` (M2: the community is paused — `me/get`'s `module.brake`, or a write refused BRAKED: reading
57
+ * goes on, every member write waits — one sentence); `pending` (being decided).
58
+ *
59
+ * A module that is `read_only` changes nothing here (m2-contracts.md §3.1: the discussions under articles keep
60
+ * taking writes; only live updates, push and the Pro surfaces stop) — the discussion polls instead of a socket.
61
+ */
62
+ export type ComposerMode = 'write' | 'signIn' | 'onboarding' | 'ageRefused' | 'sanction' | 'notMoved' | 'restricted' | 'braked' | 'pending';
63
+ export interface DiscussionUi {
64
+ phase: DiscussionPhase;
65
+ /** Why the first load failed (`failed`). */
66
+ failure: CommunityFailure | null;
67
+ thread: DiscussionState;
68
+ loadingMore: boolean;
69
+ refreshing: boolean;
70
+ /** A head poll saw messages the page does not show yet. */
71
+ newMessages: boolean;
72
+ me: MyState | null;
73
+ /** What a write or the member's state taught: no writes yet, none for this member, or a pause (BRAKED). */
74
+ block: 'notMoved' | 'restricted' | 'braked' | null;
75
+ composer: Draft;
76
+ inline: InlineDraft | null;
77
+ report: ReportDraft | null;
78
+ /** The last outcome to announce (aria-live), and a counter so the same text is announced again. */
79
+ announcement: TextRef | null;
80
+ announceId: number;
81
+ /** Now, ticking while a cooldown runs. */
82
+ clock: number;
83
+ /** Actions on their way, by key (`react:…`, `delete:<seq>`, `block:<memberId>`). */
84
+ busy: Record<string, boolean>;
85
+ /**
86
+ * The minimum age a declared band fell below (AGE_BELOW_MINIMUM). kb-api keeps nothing of a refused band
87
+ * (§7.4.2), so the element does: the onboarding is not offered again while it lives.
88
+ */
89
+ refusedAge: number | null;
90
+ /**
91
+ * M2 T9: the onboarding just finished with the art. 9 consent refused (`public_write`): the composer says once,
92
+ * in view, what the refusal leaves (writing in the public discussion). Not kept beyond this element.
93
+ */
94
+ art9RefusedNote: boolean;
95
+ /**
96
+ * M2 (§3.5): the live updates' session fell back to polling (live updates are off here, or the server cut the
97
+ * socket for good — 4501): the discussion says so once and keeps polling; it never opens the socket again itself.
98
+ */
99
+ realtimeOff: boolean;
100
+ }
101
+ export interface DiscussionControllerOptions {
102
+ client: CommunityClient;
103
+ articleId: string;
104
+ lang: Lang;
105
+ /** Head poll interval (default 20 s; 0 = no polling). */
106
+ pollMs?: number;
107
+ now?: () => number;
108
+ /** The viewer's message was posted (`held`: it waits for review). */
109
+ onPosted?(state: 'visible' | 'held'): void;
110
+ /** A refusal the element saw before (a discussion started again in another language keeps it). */
111
+ refusedAge?: number | null;
112
+ /** The onboarding's age band was refused: the element keeps it for its next discussions. */
113
+ onAgeRefused?(minAge: number): void;
114
+ /**
115
+ * M2 (§3.5, §3.10): the host's live-updates session factory (`createRealtimeSession` of
116
+ * `@blogmcp/sdk/community/realtime`). Absent: the head poll, as before.
117
+ */
118
+ realtime?: DiscussionRealtime | null;
119
+ }
120
+ export interface DiscussionController {
121
+ readonly state: DiscussionUi;
122
+ readonly store: DiscussionStore;
123
+ readonly onboarding: ShallowRef<OnboardingModel | null>;
124
+ /** The first load, then the poll. */
125
+ start(): Promise<void>;
126
+ refresh(): Promise<void>;
127
+ loadMore(): Promise<void>;
128
+ mode(): ComposerMode;
129
+ needs(): ViewerNeeds;
130
+ isOwn(m: Pick<DiscussionMessage, 'author'>): boolean;
131
+ canReply(m: DiscussionMessage): boolean;
132
+ canEdit(m: DiscussionMessage): boolean;
133
+ canDelete(m: DiscussionMessage): boolean;
134
+ canReact(): boolean;
135
+ /** A member's report (`messages/report`): any signed-in reader, on someone else's message. */
136
+ canReport(m: DiscussionMessage): boolean;
137
+ /** A reader without a session reports through the host's notice page instead (reportLink). */
138
+ canReportAsNotifier(m: DiscussionMessage): boolean;
139
+ canBlock(m: DiscussionMessage): boolean;
140
+ /** The poll may run (the element is on screen). */
141
+ setInView(inView: boolean): void;
142
+ submit(): Promise<void>;
143
+ /** Reply under `seq`'s thread (a reply to a reply goes under its top-level message, naming its author). */
144
+ openReply(seq: number, replyingTo?: string | null): void;
145
+ openEdit(seq: number): void;
146
+ closeInline(): void;
147
+ submitInline(): Promise<void>;
148
+ /** Tick or untick one code of a draft's confirmation. */
149
+ setConfirmed(draft: Draft, code: string, checked: boolean): void;
150
+ /** Send the draft again with every confirmed code (the Russmedia confirmation). */
151
+ sendConfirmed(draft: Draft): Promise<void>;
152
+ remove(seq: number): Promise<boolean>;
153
+ react(target: ReactionTarget, emoji: string): Promise<void>;
154
+ setBlocked(memberId: string, blocked: boolean): Promise<void>;
155
+ openReport(m: DiscussionMessage): void;
156
+ closeReport(): void;
157
+ submitReport(): Promise<void>;
158
+ /**
159
+ * The onboarding inline; `offerArt9` asks the art. 9 consent also of a member who refused it (`public_write`, M2 T9) —
160
+ * the give-consent link on their own message held until the consent.
161
+ */
162
+ beginOnboarding(options?: {
163
+ offerArt9?: boolean;
164
+ }): void;
165
+ cancelOnboarding(): void;
166
+ /** Seconds a draft still waits (0 when it may go). */
167
+ waitLeft(draft: Draft): number;
168
+ /**
169
+ * M2 T10: the distinct `@handle`s one message of the viewer may carry (me/get's `caps.mentionsPerMessage`), or null
170
+ * when nothing is known to limit it (no member yet, an older kb-api, a module whose mentions notify nobody).
171
+ */
172
+ mentionCap(): number | null;
173
+ dispose(): void;
174
+ }
175
+ export declare function createDiscussionController(o: DiscussionControllerOptions): DiscussionController;
@@ -0,0 +1,4 @@
1
+ /** The member's export as a file the browser saves (§8.3: the download is the SDK's). Nothing leaves the page. */
2
+ export declare function saveJson(data: unknown, fileName: string): void;
3
+ /** `<name>-YYYY-MM-DD.json`, the name being the catalogue's (`panel.exportFileName`). */
4
+ export declare function exportFileName(name: string, now?: Date): string;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Dates and a statement's territorial scope as the reader's language writes them (Intl does the words; the
3
+ * catalogue holds the sentence around them).
4
+ */
5
+ import { type Lang } from './i18n';
6
+ /** An ISO timestamp as a date (and time); '' for anything that is no date. */
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;
15
+ /** Whether an ISO timestamp lies in the past (an appeal window, a sanction's end). */
16
+ export declare function isPast(iso: string | null | undefined, now?: number): boolean;
17
+ /**
18
+ * A statement's territorial scope (DSA art. 17(3)(a)) as the reader reads it. kb-api stores `all` — the measure
19
+ * applies without a territorial limit, the catalogue's `statement.scopeAll` — or the regions it is limited to as
20
+ * ISO 3166-1 alpha-2 codes (`CZ,SK`), which Intl names and joins („Česko a Slovensko"; a runtime without Intl reads
21
+ * `CZ, SK`). Any other value — one a newer kb-api sends — is shown as it came, never dropped and never an
22
+ * exception; '' when there is none.
23
+ */
24
+ export declare function formatTerritorialScope(lang: Lang, scope: string | null | undefined): string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The elements' language and their texts (m1-contracts.md §10.4): every string a reader sees comes from
3
+ * the SDK's catalogue, `communityMessage` — one catalogue, one tone test. The elements add no words of
4
+ * their own; test/text-only.spec.ts scans src/ for any.
5
+ */
6
+ import { type CommunityMessageKey } from '@blogmcp/sdk/community';
7
+ export type Lang = 'cs' | 'en';
8
+ /**
9
+ * The review layer's rule: the first candidate that is Czech or Slovak reads Czech, the first that is
10
+ * English reads English; anything else is passed over; English when none decides. The candidates are
11
+ * the `lang` attribute, then `<html lang>`.
12
+ */
13
+ export declare function pickLang(...candidates: Array<string | null | undefined>): Lang;
14
+ /** The page's own language attribute, when there is a page (the elements mount in a browser only). */
15
+ export declare function documentLang(): string | null;
16
+ export type TextParams = Record<string, string | number>;
17
+ export type Translate = (key: CommunityMessageKey, params?: TextParams) => string;
18
+ export declare function translator(lang: Lang): Translate;
19
+ /**
20
+ * A key built from one of kb-api's open codes (`restriction.<code>`, `caseStatus.<status>`, …) when the
21
+ * catalogue has a text for it, else null: a code a newer kb-api sends is skipped, never shown raw.
22
+ */
23
+ export declare function knownKey(key: string): CommunityMessageKey | null;
24
+ /** A message key and its params: what a controller hands the view, which turns it into text. */
25
+ export interface TextRef {
26
+ readonly key: CommunityMessageKey;
27
+ readonly params?: TextParams;
28
+ }
@@ -0,0 +1,15 @@
1
+ /** The display name as kb-api will fold it: NFKC, white space collapsed, trimmed. */
2
+ export declare function normalizeName(name: string): string;
3
+ export declare function isDisplayNameOk(name: string): boolean;
4
+ /** A handle's length as COMMUNITY_HANDLE_RE takes it (3–30), for the hint and the field's maxlength. */
5
+ export declare const HANDLE_MIN = 3;
6
+ export declare const HANDLE_MAX = 30;
7
+ /** A handle as typed: lower case, trimmed; '' when none was typed. */
8
+ export declare function normalizeHandle(handle: string): string;
9
+ export declare function isHandleOk(handle: string): boolean;
10
+ /**
11
+ * The handle kb-api would make of a display name when the member gives none (its stem: fold → ASCII →
12
+ * words joined by `_`, at most 24 characters) — shown as a suggestion; null when the name gives too little.
13
+ * kb-api may still add a number on a clash, or leave out a reserved word.
14
+ */
15
+ export declare function suggestedHandle(displayName: string): string | null;
@@ -0,0 +1,50 @@
1
+ import { type CaseCategory, type CommunityClient, type CommunityFailure, type CommunityRule, type NoticeReceipt, type NoticeTarget } from '@blogmcp/sdk/community';
2
+ import type { Lang } from './i18n';
3
+ /** `off`: no site key (development, a host without the widget); kb-api then decides alone. */
4
+ export type CaptchaState = 'off' | 'loading' | 'ready' | 'unavailable';
5
+ export interface NoticeFormUi {
6
+ target: NoticeTarget | null;
7
+ category: CaseCategory | '';
8
+ ruleId: string;
9
+ rules: CommunityRule[] | null;
10
+ description: string;
11
+ name: string;
12
+ email: string;
13
+ goodFaith: boolean;
14
+ captcha: CaptchaState;
15
+ /** The widget's one-shot token, or null. */
16
+ token: string | null;
17
+ /** Bumped after every failed send: the widget issues a new token. */
18
+ resetSignal: number;
19
+ sending: boolean;
20
+ failure: CommunityFailure | null;
21
+ receipt: NoticeReceipt | null;
22
+ /** The notifier's status page with the receipt in the fragment. */
23
+ statusLink: string | null;
24
+ }
25
+ export interface NoticeFormController {
26
+ readonly state: NoticeFormUi;
27
+ /** The name and e-mail fields apply (not for an endangered child). */
28
+ contactAllowed(): boolean;
29
+ canSubmit(): boolean;
30
+ descriptionLength(): number;
31
+ setCategory(category: CaseCategory | ''): void;
32
+ setToken(token: string | null): void;
33
+ setCaptcha(state: CaptchaState): void;
34
+ submit(): Promise<void>;
35
+ dispose(): void;
36
+ }
37
+ export interface NoticeFormOptions {
38
+ client: Pick<CommunityClient, 'notices' | 'legal'>;
39
+ lang: Lang;
40
+ target: NoticeTarget | null;
41
+ /** A Turnstile site key was given: a token is needed before the form can be sent. */
42
+ captcha: boolean;
43
+ /** The host's status page (the receipt goes in its fragment). */
44
+ noticesPath: string;
45
+ /** The form was sent (the receipt is the page's: it is not passed on). */
46
+ onSubmitted?(answer: {
47
+ contactKept: boolean;
48
+ }): void;
49
+ }
50
+ export declare function createNoticeForm(o: NoticeFormOptions): NoticeFormController;
@@ -0,0 +1,92 @@
1
+ import { type AgeBand, type Art9ConsentLayout, type Art9Refusal, type CommunityClient, type CommunityFailure, type CommunityRule, type CurrentLegalText, type MyState } from '@blogmcp/sdk/community';
2
+ import type { Lang, TextRef } from './i18n';
3
+ export type OnboardingStep = 'age' | 'rules' | 'art9' | 'profile';
4
+ export type OnboardingPhase = 'loading' | 'ready' | 'sending' | 'done' | 'failed' | 'refused';
5
+ /** One consent of the art. 9 step: a controller the text names (or the tenant, when it names none) and its tick. */
6
+ export interface Art9Consent {
7
+ /** The controller's registry ref from the text's `bodyJson`; empty for the tenant of a text that names none. */
8
+ readonly entity: string;
9
+ /** The name the reader sees; empty for the tenant of a text that names none (the step's own sentence then). */
10
+ readonly label: string;
11
+ accepted: boolean;
12
+ }
13
+ export interface OnboardingState {
14
+ /** `refused`: the age band is below the minimum — nothing was stored, the reader may go on reading. */
15
+ phase: OnboardingPhase;
16
+ steps: OnboardingStep[];
17
+ index: number;
18
+ me: MyState | null;
19
+ rulesText: CurrentLegalText | null;
20
+ rules: CommunityRule[];
21
+ art9Text: CurrentLegalText | null;
22
+ art9Paragraphs: string[];
23
+ /** me/get's `settings.art9ConsentLayout` (absent: `joint`). */
24
+ art9Layout: Art9ConsentLayout;
25
+ /** me/get's `settings.art9Refusal` (absent: `read_only`). */
26
+ art9Refusal: Art9Refusal;
27
+ /**
28
+ * The consents the step asks for, each ticked on its own: one under `joint` (or a text naming fewer than two
29
+ * controllers), the text's two controllers under `split`. Empty while there is no art. 9 text.
30
+ */
31
+ art9Controllers: Art9Consent[];
32
+ /** The reader chose „Nesouhlasím" (only where the refusal is offered). */
33
+ art9Refused: boolean;
34
+ /** The me/onboard kb-api took carried the refusal (the done note says what it leaves). */
35
+ art9RefusedSent: boolean;
36
+ /** The member accepted an older rules text; this one asks again. */
37
+ reconsent: boolean;
38
+ ageBand: AgeBand | null;
39
+ rulesAccepted: boolean;
40
+ /** Every consent of the step ticked (setting it ticks or unticks them all). */
41
+ art9Accepted: boolean;
42
+ displayName: string;
43
+ handle: string;
44
+ avatarPreset: string | null;
45
+ failure: CommunityFailure | null;
46
+ /** The fields a refusal named (`displayName`, `handle`, …). */
47
+ fields: string[];
48
+ /** The minimum age, once a refusal told it (a refused `16_17` means 18). */
49
+ minAge: number | null;
50
+ }
51
+ export interface OnboardingModel {
52
+ readonly state: OnboardingState;
53
+ load(): Promise<void>;
54
+ step(): OnboardingStep | null;
55
+ isLast(): boolean;
56
+ canContinue(): boolean;
57
+ /** The next step, or — on the last one — `me/onboard`. */
58
+ next(): Promise<void>;
59
+ back(): void;
60
+ suggestedHandle(): string | null;
61
+ hasFieldError(field: string): boolean;
62
+ /**
63
+ * The tenant takes a refusal of the art. 9 consent here (`art9Refusal = public_write`, not the give-consent flow): the
64
+ * step's lede then does not make writing depend on the consent (community.md §9.8 q8, art. 9(2)(a) — freely given).
65
+ */
66
+ refusalOffered(): boolean;
67
+ /** The art. 9 step offers „Nesouhlasím" (`art9Refusal = public_write`, never in the give-consent flow). */
68
+ canRefuse(): boolean;
69
+ /** „Nesouhlasím": the step is answered without the consent; the next step, or — on the last one — `me/onboard`. */
70
+ refuse(): Promise<void>;
71
+ }
72
+ export interface OnboardingOptions {
73
+ client: Pick<CommunityClient, 'me' | 'legal'>;
74
+ lang: Lang;
75
+ /** Called with the member's state once kb-api took the onboarding. */
76
+ onDone(state: MyState): void;
77
+ /** The session cannot act as a member any more (signed out meanwhile). */
78
+ onSignIn(): void;
79
+ /** The age band was below the tenant's minimum (AGE_BELOW_MINIMUM): kb-api kept nothing, the element keeps this. */
80
+ onRefused?(minAge: number): void;
81
+ /**
82
+ * The give-consent flow („Moje komunita", M2 T9): the art. 9 step is asked also when `needs` does not name it — a
83
+ * member who refused it, or has not given it, where the tenant asks for it — and no refusal is offered.
84
+ */
85
+ offerArt9?: boolean;
86
+ signal?: AbortSignal;
87
+ }
88
+ /** kb-api's issue code for a refusal the tenant does not take (`art9Refusal = read_only`, §3.8). */
89
+ export declare const ART9_REFUSAL_READ_ONLY = "art9_refusal_read_only";
90
+ /** The sentence a refusal of the onboarding shows: a refused refusal says the consent is needed to write. */
91
+ export declare function onboardingFailureText(failure: Pick<CommunityFailure, 'code' | 'until' | 'fields' | 'issueCodes'>, lang: Lang): TextRef;
92
+ export declare function createOnboarding(o: OnboardingOptions): OnboardingModel;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * How a refusal reads (m1-contracts.md §10.2, §10.4): the client turns every kb-api and host code into one
3
+ * of COMMUNITY_UI_CODES; this turns that into the catalogue's sentence — `error.<code>`, or a sanction's
4
+ * own sentence with its end date — and says what the element does about it besides showing it.
5
+ */
6
+ import type { CommunityFailure, CommunityUiCode } from '@blogmcp/sdk/community';
7
+ import type { Lang, TextRef } from './i18n';
8
+ /** The sentence a refusal shows. Total over COMMUNITY_UI_CODES (test/outcomes.spec.ts). */
9
+ export declare function failureText(failure: Pick<CommunityFailure, 'code' | 'until'>, lang: Lang): TextRef;
10
+ /** Seconds a refusal asks to wait before the same write may go (the burst, a quota, the limiter). */
11
+ export declare const DEFAULT_WAIT_S: Partial<Record<CommunityUiCode, number>>;
12
+ /** The epoch milliseconds until which a write waits after `failure`, or null when it may go again at once. */
13
+ export declare function waitUntilOf(failure: Pick<CommunityFailure, 'code' | 'retryAfterS'>, now: number): number | null;
14
+ /** A countdown shows only for a short wait (a quota until midnight reads `error.quota` alone). */
15
+ export declare const COUNTDOWN_MAX_S = 120;
16
+ /**
17
+ * What the element does with a refusal of a write, besides showing its sentence:
18
+ * - `confirm` the author confirms the codes and sends again (the Russmedia confirmation);
19
+ * - `wait` the write waits (a cooldown);
20
+ * - `onboard` the inline onboarding opens (ONBOARDING_REQUIRED);
21
+ * - `reload` the discussion is read again (the viewer's state changed: a sanction, a session);
22
+ * - `notMoved` the discussion does not take writes yet: one sentence instead of the composer;
23
+ * - `hide` discussions are switched off: the element renders nothing (`realtimeOff`: only live updates are —
24
+ * the element keeps polling and never hides the discussion for it);
25
+ * - `retry` nothing was decided (no answer, the ledger): the same write may go again as it is;
26
+ * - `show` anything else: the sentence, the text stays for the author to change.
27
+ */
28
+ export type FailureEffect = 'confirm' | 'wait' | 'onboard' | 'reload' | 'notMoved' | 'hide' | 'retry' | 'show';
29
+ export declare function failureEffect(code: CommunityUiCode): FailureEffect;